summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2018-04-15 00:41:15 -0400
committerJustin Klaassen <justinklaassen@google.com>2018-04-15 00:41:15 -0400
commitb8042fc9b036db0a6692ca853428fc6ab1e60892 (patch)
tree82669ea5d75238758e22d379a42baeada526219e
parent4d01eeaffaa720e4458a118baa137a11614f00f7 (diff)
downloadandroid-28-androidx-autofill-release.tar.gz
/google/data/ro/projects/android/fetch_artifact \ --bid 4719250 \ --target sdk_phone_armv7-win_sdk \ sdk-repo-linux-sources-4719250.zip AndroidVersion.ApiLevel has been modified to appear as 28 Change-Id: I9ec0a12c9251b8449dba0d86b0cfdbcca16b0a7c
-rw-r--r--android/accessibilityservice/AccessibilityService.java2
-rw-r--r--android/app/ActivityOptions.java3
-rw-r--r--android/app/ActivityThread.java2
-rw-r--r--android/app/ApplicationPackageManager.java32
-rw-r--r--android/app/BroadcastOptions.java28
-rw-r--r--android/app/ContextImpl.java19
-rw-r--r--android/app/Notification.java275
-rw-r--r--android/app/NotificationChannel.java23
-rw-r--r--android/app/NotificationManager.java67
-rw-r--r--android/app/Person.java270
-rw-r--r--android/app/RemoteAction.java1
-rw-r--r--android/app/StatsManager.java213
-rw-r--r--android/app/SystemServiceRegistry.java152
-rw-r--r--android/app/UiAutomation.java11
-rw-r--r--android/app/WallpaperManager.java7
-rw-r--r--android/app/admin/DevicePolicyManager.java32
-rw-r--r--android/app/admin/FreezePeriod.java (renamed from android/app/admin/FreezeInterval.java)120
-rw-r--r--android/app/admin/SystemUpdatePolicy.java105
-rw-r--r--android/app/slice/Slice.java47
-rw-r--r--android/app/slice/SliceManager.java202
-rw-r--r--android/app/slice/SliceProvider.java9
-rw-r--r--android/app/usage/NetworkStats.java34
-rw-r--r--android/app/usage/NetworkStatsManager.java12
-rw-r--r--android/app/usage/TimeSparseArray.java2
-rw-r--r--android/app/usage/UsageEvents.java14
-rw-r--r--android/appwidget/AppWidgetHost.java6
-rw-r--r--android/bluetooth/BluetoothHearingAid.java20
-rw-r--r--android/bluetooth/BluetoothHidDevice.java22
-rw-r--r--android/content/ContentResolver.java84
-rw-r--r--android/content/Context.java2
-rw-r--r--android/content/Intent.java42
-rw-r--r--android/content/QuickViewConstants.java4
-rw-r--r--android/content/pm/ApplicationInfo.java102
-rw-r--r--android/content/pm/LauncherApps.java24
-rw-r--r--android/content/pm/PackageInfo.java55
-rw-r--r--android/content/pm/PackageManager.java89
-rw-r--r--android/content/pm/PackageManagerInternal.java38
-rw-r--r--android/content/pm/PackageParser.java24
-rw-r--r--android/content/pm/PackageSharedLibraryUpdater.java2
-rw-r--r--android/content/pm/PackageUserState.java6
-rw-r--r--android/content/pm/SigningInfo.java139
-rw-r--r--android/graphics/ImageDecoder.java1501
-rw-r--r--android/graphics/PostProcessor.java86
-rw-r--r--android/graphics/Typeface.java20
-rw-r--r--android/hardware/biometrics/BiometricPrompt.java (renamed from android/hardware/biometrics/BiometricDialog.java)38
-rw-r--r--android/hardware/camera2/CameraManager.java34
-rw-r--r--android/hardware/camera2/CaptureRequest.java6
-rw-r--r--android/hardware/camera2/CaptureResult.java6
-rw-r--r--android/hardware/display/BrightnessConfiguration.java8
-rw-r--r--android/hardware/display/Curve.java62
-rw-r--r--android/hardware/display/DisplayManager.java17
-rw-r--r--android/hardware/display/DisplayManagerGlobal.java19
-rw-r--r--android/hardware/fingerprint/FingerprintManager.java44
-rw-r--r--android/location/LocationManager.java95
-rw-r--r--android/media/AudioAttributes.java7
-rw-r--r--android/media/AudioFocusRequest.java2
-rw-r--r--android/media/AudioFormat.java5
-rw-r--r--android/media/AudioManager.java34
-rw-r--r--android/media/AudioPlaybackConfiguration.java2
-rw-r--r--android/media/AudioPresentation.java9
-rw-r--r--android/media/AudioRecord.java1
-rw-r--r--android/media/BufferingParams.java2
-rw-r--r--android/media/ExifInterface.java92
-rw-r--r--android/media/Image.java7
-rw-r--r--android/media/ImageReader.java13
-rw-r--r--android/media/ImageWriter.java17
-rw-r--r--android/media/MediaCodec.java7
-rw-r--r--android/media/MediaCodecInfo.java2
-rw-r--r--android/media/MediaMetadataRetriever.java9
-rw-r--r--android/media/MediaPlayer.java98
-rw-r--r--android/media/MediaRecorder.java1
-rw-r--r--android/media/MediaTimestamp.java9
-rw-r--r--android/media/PlaybackParams.java3
-rw-r--r--android/media/PlayerBase.java80
-rw-r--r--android/media/VolumeShaper.java2
-rw-r--r--android/media/audiofx/AudioEffect.java23
-rw-r--r--android/media/session/MediaController.java2
-rw-r--r--android/media/session/MediaSessionManager.java14
-rw-r--r--android/net/IpSecManager.java43
-rw-r--r--android/net/IpSecTransform.java8
-rw-r--r--android/net/NetworkCapabilities.java28
-rw-r--r--android/net/NetworkPolicy.java4
-rw-r--r--android/net/NetworkPolicyManager.java16
-rw-r--r--android/net/NetworkRequest.java17
-rw-r--r--android/net/NetworkState.java4
-rw-r--r--android/net/apf/ApfFilter.java108
-rw-r--r--android/net/dns/ResolvUtil.java65
-rw-r--r--android/net/http/X509TrustManagerExtensions.java3
-rw-r--r--android/net/ip/IpClient.java56
-rw-r--r--android/net/ip/IpManager.java29
-rw-r--r--android/net/metrics/ApfStats.java2
-rw-r--r--android/net/util/NetworkConstants.java3
-rw-r--r--android/net/wifi/WifiConfiguration.java82
-rw-r--r--android/net/wifi/WifiManager.java6
-rw-r--r--android/os/BatteryManager.java16
-rw-r--r--android/os/BatteryStats.java9
-rw-r--r--android/os/Build.java2
-rw-r--r--android/os/DeviceIdleManager.java69
-rw-r--r--android/os/Environment.java2
-rw-r--r--android/os/MessageQueue.java1
-rw-r--r--android/os/Parcel.java42
-rw-r--r--android/os/ServiceManager.java229
-rw-r--r--android/os/StrictMode.java44
-rw-r--r--android/os/SystemProperties.java3
-rw-r--r--android/os/UserHandle.java1
-rw-r--r--android/os/UserManager.java1
-rw-r--r--android/os/WorkSource.java8
-rw-r--r--android/os/ZygoteProcess.java48
-rw-r--r--android/os/storage/DiskInfo.java14
-rw-r--r--android/os/strictmode/NonSdkApiUsedViolation.java (renamed from android/security/keystore/SessionExpiredException.java)13
-rw-r--r--android/provider/Settings.java153
-rw-r--r--android/se/omapi/Channel.java18
-rw-r--r--android/se/omapi/Reader.java2
-rw-r--r--android/se/omapi/SEService.java46
-rw-r--r--android/se/omapi/Session.java56
-rw-r--r--android/security/ConfirmationCallback.java10
-rw-r--r--android/security/ConfirmationPrompt.java (renamed from android/security/ConfirmationDialog.java)57
-rw-r--r--android/security/KeyStoreException.java3
-rw-r--r--android/security/keystore/BackwardsCompat.java127
-rw-r--r--android/security/keystore/KeyDerivationParams.java113
-rw-r--r--android/security/keystore/KeyGenParameterSpec.java22
-rw-r--r--android/security/keystore/KeyProtection.java25
-rw-r--r--android/security/keystore/KeychainProtectionParams.java269
-rw-r--r--android/security/keystore/KeychainSnapshot.java276
-rw-r--r--android/security/keystore/RecoveryClaim.java53
-rw-r--r--android/security/keystore/RecoveryController.java467
-rw-r--r--android/security/keystore/RecoveryControllerException.java35
-rw-r--r--android/security/keystore/RecoverySession.java69
-rw-r--r--android/security/keystore/UserPresenceUnavailableException.java4
-rw-r--r--android/security/keystore/WrappedApplicationKey.java135
-rw-r--r--android/security/keystore/recovery/RecoveryController.java27
-rw-r--r--android/service/notification/NotificationListenerService.java5
-rw-r--r--android/service/notification/ZenModeConfig.java22
-rw-r--r--android/service/textclassifier/TextClassifierService.java74
-rw-r--r--android/service/wallpaper/WallpaperService.java2
-rw-r--r--android/support/v4/media/session/MediaControllerCompat.java25
-rw-r--r--android/support/v4/media/session/PlaybackStateCompat.java1
-rw-r--r--android/system/Os.java18
-rw-r--r--android/telecom/Connection.java11
-rw-r--r--android/telecom/InCallService.java7
-rw-r--r--android/telecom/PhoneAccount.java3
-rw-r--r--android/telephony/AccessNetworkConstants.java3
-rw-r--r--android/telephony/CarrierConfigManager.java31
-rw-r--r--android/telephony/NetworkRegistrationState.java2
-rw-r--r--android/telephony/NetworkService.java8
-rw-r--r--android/telephony/NetworkServiceCallback.java2
-rw-r--r--android/telephony/ServiceState.java8
-rw-r--r--android/telephony/SubscriptionManager.java14
-rw-r--r--android/telephony/SubscriptionPlan.java48
-rw-r--r--android/telephony/TelephonyManager.java17
-rw-r--r--android/telephony/data/DataCallResponse.java2
-rw-r--r--android/telephony/data/DataProfile.java2
-rw-r--r--android/telephony/data/DataService.java8
-rw-r--r--android/telephony/data/DataServiceCallback.java3
-rw-r--r--android/telephony/ims/stub/ImsFeatureConfiguration.java5
-rw-r--r--android/telephony/mbms/DownloadRequest.java2
-rw-r--r--android/text/MeasuredParagraph.java11
-rw-r--r--android/text/PrecomputedText.java63
-rw-r--r--android/text/Selection.java2
-rw-r--r--android/text/Spannable.java13
-rw-r--r--android/text/SpannableStringBuilder.java19
-rw-r--r--android/text/SpannableStringInternal.java13
-rw-r--r--android/text/format/Formatter.java30
-rw-r--r--android/text/method/LinkMovementMethod.java2
-rw-r--r--android/text/util/Linkify.java9
-rw-r--r--android/util/LruCache.java25
-rw-r--r--android/util/RecurrenceRule.java20
-rw-r--r--android/view/DisplayCutout.java34
-rw-r--r--android/view/HapticFeedbackConstants.java49
-rw-r--r--android/view/SurfaceControl.java14
-rw-r--r--android/view/SurfaceView.java1164
-rw-r--r--android/view/ThreadedRenderer.java15
-rw-r--r--android/view/View.java88
-rw-r--r--android/view/ViewGroup.java1
-rw-r--r--android/view/ViewRootImpl.java30
-rw-r--r--android/view/WindowManager.java13
-rw-r--r--android/view/WindowManagerGlobal.java4
-rw-r--r--android/view/accessibility/AccessibilityEvent.java64
-rw-r--r--android/view/accessibility/AccessibilityInteractionClient.java11
-rw-r--r--android/view/accessibility/AccessibilityManager.java964
-rw-r--r--android/view/accessibility/AccessibilityNodeInfo.java18
-rw-r--r--android/view/autofill/AutofillPopupWindow.java5
-rw-r--r--android/view/inputmethod/BaseInputConnection.java2
-rw-r--r--android/view/textclassifier/GenerateLinksLogger.java10
-rw-r--r--android/view/textclassifier/Logger.java397
-rw-r--r--android/view/textclassifier/SelectionEvent.java17
-rw-r--r--android/view/textclassifier/SelectionSessionLogger.java (renamed from android/view/textclassifier/DefaultLogger.java)58
-rw-r--r--android/view/textclassifier/SystemTextClassifier.java29
-rw-r--r--android/view/textclassifier/TextClassification.java73
-rw-r--r--android/view/textclassifier/TextClassificationSession.java4
-rw-r--r--android/view/textclassifier/TextClassifier.java105
-rw-r--r--android/view/textclassifier/TextClassifierImpl.java28
-rw-r--r--android/view/textclassifier/TextLinks.java124
-rw-r--r--android/view/textclassifier/TextSelection.java52
-rw-r--r--android/view/textservice/TextServicesManager.java206
-rw-r--r--android/webkit/FindAddress.java27
-rw-r--r--android/webkit/TracingConfig.java97
-rw-r--r--android/webkit/TracingController.java29
-rw-r--r--android/webkit/WebView.java3013
-rw-r--r--android/widget/Editor.java63
-rw-r--r--android/widget/ImageView.java2
-rw-r--r--android/widget/LinearLayout.java26
-rw-r--r--android/widget/Magnifier.java18
-rw-r--r--android/widget/SelectionActionModeHelper.java28
-rw-r--r--android/widget/TextClock.java9
-rw-r--r--android/widget/TextView.java33
-rw-r--r--androidx/car/app/CarAlertDialog.java14
-rw-r--r--androidx/car/utils/CarUxRestrictionsTestUtils.java6
-rw-r--r--androidx/car/widget/DayNightStyle.java5
-rw-r--r--androidx/car/widget/PagedListView.java14
-rw-r--r--androidx/car/widget/SeekbarListItem.java55
-rw-r--r--androidx/car/widget/SeekbarListItemTest.java13
-rw-r--r--androidx/car/widget/SubheaderListItem.java10
-rw-r--r--androidx/car/widget/TextListItem.java47
-rw-r--r--androidx/car/widget/TextListItemTest.java12
-rw-r--r--androidx/contentpager/content/Query.java2
-rw-r--r--androidx/core/app/ActivityCompat.java1
-rw-r--r--androidx/core/app/NotificationCompat.java128
-rw-r--r--androidx/core/app/NotificationCompatTest.java57
-rw-r--r--androidx/core/app/Person.java15
-rw-r--r--androidx/core/app/PersonTest.java32
-rw-r--r--androidx/core/content/pm/PackageInfoCompat.java43
-rw-r--r--androidx/core/content/pm/PackageInfoCompatTest.java47
-rw-r--r--androidx/core/content/pm/ShortcutInfoCompat.java2
-rw-r--r--androidx/core/text/HtmlCompat.java6
-rw-r--r--androidx/core/text/util/FindAddress.java3
-rw-r--r--androidx/core/view/ViewCompat.java1
-rw-r--r--androidx/core/view/WindowCompat.java1
-rw-r--r--androidx/leanback/system/Settings.java2
-rw-r--r--androidx/lifecycle/ComputableLiveData.java151
-rw-r--r--androidx/lifecycle/LiveData.java441
-rw-r--r--androidx/media/DataSourceDesc.java469
-rw-r--r--androidx/media/Media2DataSource.java68
-rw-r--r--androidx/media/MediaBrowser2.java495
-rw-r--r--androidx/media/MediaBrowser2Test.java703
-rw-r--r--androidx/media/MediaBrowserServiceCompat.java13
-rw-r--r--androidx/media/MediaConstants2.java103
-rw-r--r--androidx/media/MediaController2.java1770
-rw-r--r--androidx/media/MediaController2Test.java1306
-rw-r--r--androidx/media/MediaInterface2.java80
-rw-r--r--androidx/media/MediaItem2.java304
-rw-r--r--androidx/media/MediaLibraryService2.java589
-rw-r--r--androidx/media/MediaLibrarySessionImplBase.java61
-rw-r--r--androidx/media/MediaMetadata2.java1097
-rw-r--r--androidx/media/MediaMetadata2Test.java54
-rw-r--r--androidx/media/MediaPlayer2.java2011
-rw-r--r--androidx/media/MediaPlayer2Impl.java1982
-rw-r--r--androidx/media/MediaPlayer2Test.java2262
-rw-r--r--androidx/media/MediaPlayer2TestBase.java584
-rw-r--r--androidx/media/MediaPlayerBase.java352
-rw-r--r--androidx/media/MediaPlaylistAgent.java469
-rw-r--r--androidx/media/MediaSession2.java1670
-rw-r--r--androidx/media/MediaSession2ImplBase.java1220
-rw-r--r--androidx/media/MediaSession2StubImplBase.java929
-rw-r--r--androidx/media/MediaSession2Test.java1053
-rw-r--r--androidx/media/MediaSession2TestBase.java364
-rw-r--r--androidx/media/MediaSession2_PermissionTest.java680
-rw-r--r--androidx/media/MediaSessionManager_MediaSession2Test.java327
-rw-r--r--androidx/media/MediaSessionService2.java329
-rw-r--r--androidx/media/MediaStubActivity.java61
-rw-r--r--androidx/media/MediaUtils2.java431
-rw-r--r--androidx/media/MockActivity.java (renamed from android/security/keystore/DecryptionFailedException.java)14
-rw-r--r--androidx/media/MockMediaLibraryService2.java237
-rw-r--r--androidx/media/MockMediaSessionService2.java94
-rw-r--r--androidx/media/MockPlayer.java290
-rw-r--r--androidx/media/MockPlaylistAgent.java145
-rw-r--r--androidx/media/Rating2.java325
-rw-r--r--androidx/media/SessionCommand2.java450
-rw-r--r--androidx/media/SessionCommandGroup2.java234
-rw-r--r--androidx/media/SessionPlaylistAgentImplBase.java523
-rw-r--r--androidx/media/SessionToken2.java348
-rw-r--r--androidx/media/SessionToken2Test.java66
-rw-r--r--androidx/media/TestMedia2DataSource.java123
-rw-r--r--androidx/media/TestServiceRegistry.java132
-rw-r--r--androidx/media/TestUtils.java160
-rw-r--r--androidx/paging/DataSource.java388
-rw-r--r--androidx/paging/LivePagedListBuilder.java2
-rw-r--r--androidx/paging/PositionalDataSource.java543
-rw-r--r--androidx/paging/RxPagedListBuilder.java330
-rw-r--r--androidx/paging/WrapperItemKeyedDataSource.java28
-rw-r--r--androidx/paging/WrapperPageKeyedDataSource.java28
-rw-r--r--androidx/paging/WrapperPositionalDataSource.java29
-rw-r--r--androidx/preference/CollapsiblePreferenceGroupController.java16
-rw-r--r--androidx/recyclerview/selection/BandSelectionHelper.java2
-rw-r--r--androidx/recyclerview/selection/DefaultSelectionTracker.java47
-rw-r--r--androidx/recyclerview/selection/DefaultSelectionTrackerTest.java12
-rw-r--r--androidx/recyclerview/selection/EventBridge.java48
-rw-r--r--androidx/recyclerview/selection/GestureSelectionHelper.java8
-rw-r--r--androidx/recyclerview/selection/GestureSelectionHelperTest.java15
-rw-r--r--androidx/recyclerview/selection/GridModel.java2
-rw-r--r--androidx/recyclerview/selection/MotionEvents.java10
-rw-r--r--androidx/recyclerview/selection/OnDragInitiatedListener.java45
-rw-r--r--androidx/recyclerview/selection/PointerDragEventInterceptor.java75
-rw-r--r--androidx/recyclerview/selection/SelectionTest.java14
-rw-r--r--androidx/recyclerview/selection/SelectionTracker.java31
-rw-r--r--androidx/recyclerview/selection/testing/SelectionProbe.java2
-rw-r--r--androidx/recyclerview/widget/LinearLayoutManagerTest.java11
-rw-r--r--androidx/room/ForeignKey.java6
-rw-r--r--androidx/room/integration/testapp/CustomerViewModel.java52
-rw-r--r--androidx/room/integration/testapp/RoomPagedListRxActivity.java69
-rw-r--r--androidx/room/integration/testapp/database/CustomerDao.java6
-rw-r--r--androidx/room/integration/testapp/test/ClearAllTablesTest.java31
-rw-r--r--androidx/room/integration/testapp/test/RxJava2Test.java100
-rw-r--r--androidx/slice/SliceManager.java13
-rw-r--r--androidx/slice/SliceManagerCompat.java6
-rw-r--r--androidx/slice/SliceManagerTest.java30
-rw-r--r--androidx/slice/SliceManagerWrapper.java12
-rw-r--r--androidx/slice/SliceMetadata.java37
-rw-r--r--androidx/slice/SliceMetadataTest.java15
-rw-r--r--androidx/slice/SliceProvider.java359
-rw-r--r--androidx/slice/SliceTestProvider.java1
-rw-r--r--androidx/slice/SliceUtils.java4
-rw-r--r--androidx/slice/builders/ListBuilder.java13
-rw-r--r--androidx/slice/builders/impl/ListBuilder.java5
-rw-r--r--androidx/slice/builders/impl/ListBuilderV1Impl.java14
-rw-r--r--androidx/slice/compat/ContentProviderWrapper.java123
-rw-r--r--androidx/slice/compat/SliceProviderCompat.java299
-rw-r--r--androidx/slice/compat/SliceProviderWrapperContainer.java16
-rw-r--r--androidx/slice/core/SliceHints.java23
-rw-r--r--androidx/slice/render/SliceCreator.java11
-rw-r--r--androidx/slice/widget/EventInfo.java6
-rw-r--r--androidx/slice/widget/GridRowView.java29
-rw-r--r--androidx/slice/widget/LargeSliceAdapter.java87
-rw-r--r--androidx/slice/widget/LargeTemplateView.java92
-rw-r--r--androidx/slice/widget/ListContent.java47
-rw-r--r--androidx/slice/widget/RowContent.java24
-rw-r--r--androidx/slice/widget/RowView.java303
-rw-r--r--androidx/slice/widget/ShortcutView.java3
-rw-r--r--androidx/slice/widget/SliceActionView.java243
-rw-r--r--androidx/slice/widget/SliceChildView.java4
-rw-r--r--androidx/slice/widget/SliceView.java137
-rw-r--r--androidx/webkit/WebViewClientCompat.java103
-rw-r--r--androidx/webkit/WebViewCompat.java2
-rw-r--r--androidx/widget/BaseLayout.java236
-rw-r--r--androidx/widget/MediaControlView2.java1901
-rw-r--r--androidx/widget/MediaUtils2.java83
-rw-r--r--androidx/widget/VideoSurfaceView.java203
-rw-r--r--androidx/widget/VideoTextureView.java198
-rw-r--r--androidx/widget/VideoView2.java1785
-rw-r--r--androidx/widget/VideoView2Test.java235
-rw-r--r--androidx/widget/VideoView2TestActivity.java (renamed from android/security/keystore/LockScreenRequiredException.java)23
-rw-r--r--androidx/widget/VideoViewInterface.java66
-rw-r--r--com/android/captiveportallogin/CaptivePortalLoginActivity.java3
-rw-r--r--com/android/carrierdefaultapp/CaptivePortalLoginActivity.java3
-rw-r--r--com/android/clockwork/bluetooth/BluetoothShardRunner.java2
-rw-r--r--com/android/clockwork/bluetooth/CompanionProxyShard.java47
-rw-r--r--com/android/clockwork/bluetooth/CompanionProxyShardTest.java9
-rw-r--r--com/android/clockwork/bluetooth/WearBluetoothMediator.java1
-rw-r--r--com/android/ex/photo/ActionBarWrapper.java3
-rw-r--r--com/android/ex/photo/PhotoViewActivity.java8
-rw-r--r--com/android/ims/MmTelFeatureConnection.java6
-rw-r--r--com/android/internal/app/ColorDisplayController.java33
-rw-r--r--com/android/internal/app/SuspendedAppActivity.java102
-rw-r--r--com/android/internal/os/BatteryStatsHelper.java2
-rw-r--r--com/android/internal/os/BatteryStatsImpl.java35
-rw-r--r--com/android/internal/os/RuntimeInit.java62
-rw-r--r--com/android/internal/os/ZygoteConnection.java34
-rw-r--r--com/android/internal/os/ZygoteInit.java12
-rw-r--r--com/android/internal/telephony/CarrierIdentifier.java5
-rw-r--r--com/android/internal/telephony/InboundSmsTracker.java4
-rw-r--r--com/android/internal/telephony/PhoneSubInfoController.java4
-rw-r--r--com/android/internal/telephony/ServiceStateTracker.java33
-rw-r--r--com/android/internal/telephony/SubscriptionController.java121
-rw-r--r--com/android/internal/telephony/dataconnection/DataServiceManager.java118
-rw-r--r--com/android/internal/telephony/dataconnection/DcTracker.java4
-rw-r--r--com/android/internal/telephony/ims/ImsResolver.java110
-rw-r--r--com/android/internal/telephony/ims/ImsServiceController.java18
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java8
-rw-r--r--com/android/internal/telephony/metrics/TelephonyMetrics.java8
-rw-r--r--com/android/internal/telephony/uicc/IccIoResult.java11
-rw-r--r--com/android/internal/telephony/uicc/UiccProfile.java16
-rw-r--r--com/android/internal/telephony/uicc/UiccSlot.java12
-rw-r--r--com/android/internal/util/StatLogger.java (renamed from com/android/server/StatLogger.java)71
-rw-r--r--com/android/internal/widget/FloatingToolbar.java1
-rw-r--r--com/android/internal/widget/MessagingGroup.java18
-rw-r--r--com/android/internal/widget/MessagingLayout.java27
-rw-r--r--com/android/internal/widget/MessagingMessage.java3
-rw-r--r--com/android/keyguard/KeyguardAbsKeyInputView.java2
-rw-r--r--com/android/keyguard/KeyguardPINView.java7
-rw-r--r--com/android/keyguard/KeyguardPasswordView.java7
-rw-r--r--com/android/keyguard/KeyguardPatternView.java7
-rw-r--r--com/android/keyguard/KeyguardSimPinView.java5
-rw-r--r--com/android/keyguard/KeyguardStatusView.java3
-rw-r--r--com/android/providers/settings/SettingsBackupAgent.java1
-rw-r--r--com/android/providers/settings/SettingsHelper.java9
-rw-r--r--com/android/providers/settings/SettingsProtoDumpUtil.java17
-rw-r--r--com/android/providers/settings/SettingsProvider.java17
-rw-r--r--com/android/server/AlarmManagerService.java1
-rw-r--r--com/android/server/AppStateTracker.java1
-rw-r--r--com/android/server/ConnectivityService.java200
-rw-r--r--com/android/server/InputMethodManagerService.java4
-rw-r--r--com/android/server/IpSecService.java59
-rw-r--r--com/android/server/LocationManagerService.java215
-rw-r--r--com/android/server/LockGuard.java60
-rw-r--r--com/android/server/NetworkScoreService.java24
-rw-r--r--com/android/server/SystemServer.java23
-rw-r--r--com/android/server/ThreadPriorityBooster.java4
-rw-r--r--com/android/server/VibratorService.java15
-rw-r--r--com/android/server/accessibility/AbstractAccessibilityServiceConnection.java3
-rw-r--r--com/android/server/accessibility/AccessibilityManagerService.java27
-rw-r--r--com/android/server/am/ActiveServices.java14
-rw-r--r--com/android/server/am/ActivityDisplay.java59
-rw-r--r--com/android/server/am/ActivityManagerService.java150
-rw-r--r--com/android/server/am/ActivityRecord.java50
-rw-r--r--com/android/server/am/ActivityStack.java102
-rw-r--r--com/android/server/am/ActivityStackSupervisor.java47
-rw-r--r--com/android/server/am/ActivityStartController.java4
-rw-r--r--com/android/server/am/ActivityStartInterceptor.java75
-rw-r--r--com/android/server/am/ActivityStarter.java44
-rw-r--r--com/android/server/am/AppErrors.java4
-rw-r--r--com/android/server/am/AppWarnings.java5
-rw-r--r--com/android/server/am/BatteryStatsService.java50
-rw-r--r--com/android/server/am/ProcessRecord.java12
-rw-r--r--com/android/server/am/RecentTasks.java10
-rw-r--r--com/android/server/am/RecentsAnimation.java179
-rw-r--r--com/android/server/am/SafeActivityOptions.java2
-rw-r--r--com/android/server/am/TaskRecord.java31
-rw-r--r--com/android/server/am/UserController.java7
-rw-r--r--com/android/server/audio/AudioService.java44
-rw-r--r--com/android/server/autofill/Session.java22
-rw-r--r--com/android/server/autofill/ViewState.java7
-rw-r--r--com/android/server/autofill/ui/FillUi.java12
-rw-r--r--com/android/server/backup/BackupUtils.java14
-rw-r--r--com/android/server/backup/KeyValueAdbRestoreEngine.java11
-rw-r--r--com/android/server/backup/PackageManagerBackupAgent.java16
-rw-r--r--com/android/server/backup/restore/PerformAdbRestoreTask.java6
-rw-r--r--com/android/server/backup/transport/TransportClient.java13
-rw-r--r--com/android/server/backup/utils/AppBackupUtils.java14
-rw-r--r--com/android/server/backup/utils/FullBackupUtils.java8
-rw-r--r--com/android/server/connectivity/DnsManager.java220
-rw-r--r--com/android/server/connectivity/MultipathPolicyTracker.java262
-rw-r--r--com/android/server/connectivity/NetworkMonitor.java273
-rw-r--r--com/android/server/connectivity/Tethering.java56
-rw-r--r--com/android/server/connectivity/tethering/TetheringConfiguration.java62
-rw-r--r--com/android/server/content/ContentService.java2
-rw-r--r--com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java1397
-rw-r--r--com/android/server/devicepolicy/DevicePolicyManagerService.java743
-rw-r--r--com/android/server/display/AutomaticBrightnessController.java128
-rw-r--r--com/android/server/display/BrightnessMappingStrategy.java313
-rw-r--r--com/android/server/display/BrightnessTracker.java13
-rw-r--r--com/android/server/display/ColorDisplayService.java5
-rw-r--r--com/android/server/display/DisplayManagerService.java92
-rw-r--r--com/android/server/display/DisplayManagerShellCommand.java92
-rw-r--r--com/android/server/display/DisplayPowerController.java5
-rw-r--r--com/android/server/display/LocalDisplayAdapter.java4
-rw-r--r--com/android/server/display/utils/Plog.java141
-rw-r--r--com/android/server/fingerprint/AuthenticationClient.java16
-rw-r--r--com/android/server/fingerprint/FingerprintService.java13
-rw-r--r--com/android/server/input/InputWindowHandle.java2
-rw-r--r--com/android/server/job/JobSchedulerService.java6
-rw-r--r--com/android/server/job/controllers/ConnectivityController.java107
-rw-r--r--com/android/server/job/controllers/StateController.java2
-rw-r--r--com/android/server/location/ExponentialBackOff.java32
-rw-r--r--com/android/server/location/GnssLocationProvider.java176
-rw-r--r--com/android/server/location/GnssSatelliteBlacklistHelper.java102
-rw-r--r--com/android/server/location/GnssSatelliteBlacklistHelperTest.java130
-rw-r--r--com/android/server/location/NtpTimeHelper.java191
-rw-r--r--com/android/server/location/NtpTimeHelperTest.java100
-rw-r--r--com/android/server/locksettings/SyntheticPasswordManager.java37
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeySyncTask.java56
-rw-r--r--com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java4
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java1
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java25
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java3
-rw-r--r--com/android/server/media/MediaSessionService.java18
-rw-r--r--com/android/server/net/NetworkPolicyManagerService.java262
-rw-r--r--com/android/server/net/NetworkStatsCollection.java10
-rw-r--r--com/android/server/net/NetworkStatsObservers.java4
-rw-r--r--com/android/server/net/watchlist/NetworkWatchlistShellCommand.java2
-rw-r--r--com/android/server/net/watchlist/ReportEncoder.java1
-rw-r--r--com/android/server/net/watchlist/WatchlistConfig.java18
-rw-r--r--com/android/server/net/watchlist/WatchlistLoggingHandler.java3
-rw-r--r--com/android/server/net/watchlist/WatchlistReportDbHelper.java1
-rw-r--r--com/android/server/net/watchlist/WatchlistSettings.java2
-rw-r--r--com/android/server/notification/ManagedServices.java2
-rw-r--r--com/android/server/notification/NotificationDelegate.java2
-rw-r--r--com/android/server/notification/NotificationManagerService.java84
-rw-r--r--com/android/server/notification/NotificationRecord.java18
-rw-r--r--com/android/server/notification/RankingHelper.java27
-rw-r--r--com/android/server/notification/ValidateNotificationPeople.java5
-rw-r--r--com/android/server/notification/ZenModeHelper.java10
-rw-r--r--com/android/server/om/OverlayManagerSettings.java8
-rw-r--r--com/android/server/pm/Installer.java2
-rw-r--r--com/android/server/pm/InstantAppResolver.java2
-rw-r--r--com/android/server/pm/LauncherAppsService.java20
-rw-r--r--com/android/server/pm/OtaDexoptService.java2
-rw-r--r--com/android/server/pm/PackageDexOptimizer.java14
-rw-r--r--com/android/server/pm/PackageInstallerSession.java16
-rw-r--r--com/android/server/pm/PackageManagerService.java216
-rw-r--r--com/android/server/pm/PackageManagerShellCommand.java6
-rw-r--r--com/android/server/pm/PackageSettingBase.java12
-rw-r--r--com/android/server/pm/Settings.java21
-rw-r--r--com/android/server/pm/ShortcutPackageInfo.java17
-rw-r--r--com/android/server/pm/ShortcutService.java3
-rw-r--r--com/android/server/pm/permission/BasePermission.java28
-rw-r--r--com/android/server/pm/permission/DefaultPermissionGrantPolicy.java5
-rw-r--r--com/android/server/policy/PhoneWindowManager.java60
-rw-r--r--com/android/server/slice/DirtyTracker.java35
-rw-r--r--com/android/server/slice/SliceClientPermissions.java354
-rw-r--r--com/android/server/slice/SliceManagerService.java225
-rw-r--r--com/android/server/slice/SlicePermissionManager.java432
-rw-r--r--com/android/server/slice/SliceProviderPermissions.java204
-rw-r--r--com/android/server/soundtrigger/SoundTriggerService.java31
-rw-r--r--com/android/server/stats/StatsCompanionService.java11
-rw-r--r--com/android/server/statusbar/StatusBarManagerService.java28
-rw-r--r--com/android/server/usb/UsbDebuggingManager.java2
-rw-r--r--com/android/server/wifi/ScanRequestProxy.java61
-rw-r--r--com/android/server/wifi/ScoredNetworkEvaluator.java7
-rw-r--r--com/android/server/wifi/ScoringParams.java31
-rw-r--r--com/android/server/wifi/SelfRecovery.java29
-rw-r--r--com/android/server/wifi/VelocityBasedConnectedScore.java6
-rw-r--r--com/android/server/wifi/WakeupConfigStoreData.java12
-rw-r--r--com/android/server/wifi/WakeupController.java114
-rw-r--r--com/android/server/wifi/WakeupLock.java172
-rw-r--r--com/android/server/wifi/WakeupNotificationFactory.java4
-rw-r--r--com/android/server/wifi/WakeupOnboarding.java88
-rw-r--r--com/android/server/wifi/WifiConfigStoreLegacy.java23
-rw-r--r--com/android/server/wifi/WifiController.java49
-rw-r--r--com/android/server/wifi/WifiInjector.java7
-rw-r--r--com/android/server/wifi/WifiMetrics.java6
-rw-r--r--com/android/server/wifi/WifiNative.java9
-rw-r--r--com/android/server/wifi/WifiScoreReport.java101
-rw-r--r--com/android/server/wifi/WifiServiceImpl.java64
-rw-r--r--com/android/server/wifi/WifiStateMachine.java209
-rw-r--r--com/android/server/wifi/WifiWakeMetrics.java90
-rw-r--r--com/android/server/wifi/p2p/WifiP2pServiceImpl.java10
-rw-r--r--com/android/server/wifi/util/WifiPermissionsUtil.java16
-rw-r--r--com/android/server/wm/AccessibilityController.java46
-rw-r--r--com/android/server/wm/AnimatingAppWindowTokenRegistry.java35
-rw-r--r--com/android/server/wm/AppWindowContainerController.java118
-rw-r--r--com/android/server/wm/AppWindowToken.java8
-rw-r--r--com/android/server/wm/BoundsAnimationController.java7
-rw-r--r--com/android/server/wm/BoundsAnimationTarget.java6
-rw-r--r--com/android/server/wm/DisplayContent.java11
-rw-r--r--com/android/server/wm/DockedStackDividerController.java5
-rw-r--r--com/android/server/wm/LocalAnimationAdapter.java7
-rw-r--r--com/android/server/wm/RecentsAnimationController.java272
-rw-r--r--com/android/server/wm/RemoteAnimationController.java65
-rw-r--r--com/android/server/wm/RootWindowContainer.java8
-rw-r--r--com/android/server/wm/Session.java16
-rw-r--r--com/android/server/wm/SurfaceAnimationRunner.java3
-rw-r--r--com/android/server/wm/Task.java18
-rw-r--r--com/android/server/wm/TaskSnapshotSurface.java6
-rw-r--r--com/android/server/wm/TaskStack.java49
-rw-r--r--com/android/server/wm/WallpaperController.java4
-rw-r--r--com/android/server/wm/WindowAnimationSpec.java12
-rw-r--r--com/android/server/wm/WindowManagerDebugConfig.java3
-rw-r--r--com/android/server/wm/WindowManagerService.java72
-rw-r--r--com/android/server/wm/WindowState.java53
-rw-r--r--com/android/server/wm/WindowStateAnimator.java51
-rw-r--r--com/android/settingslib/TwoTargetPreference.java35
-rw-r--r--com/android/settingslib/bluetooth/BluetoothCallback.java2
-rw-r--r--com/android/settingslib/bluetooth/BluetoothEventManager.java28
-rw-r--r--com/android/settingslib/bluetooth/CachedBluetoothDevice.java2
-rw-r--r--com/android/settingslib/bluetooth/HearingAidProfile.java9
-rw-r--r--com/android/settingslib/bluetooth/HidDeviceProfile.java200
-rw-r--r--com/android/settingslib/bluetooth/HidProfile.java4
-rw-r--r--com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java66
-rw-r--r--com/android/settingslib/fuelgauge/BatterySaverUtils.java21
-rw-r--r--com/android/settingslib/fuelgauge/PowerWhitelistBackend.java16
-rw-r--r--com/android/settingslib/users/UserManagerHelper.java202
-rw-r--r--com/android/settingslib/utils/PowerUtil.java3
-rw-r--r--com/android/settingslib/wifi/WifiStatusTracker.java6
-rw-r--r--com/android/settingslib/wifi/WifiTracker.java5
-rw-r--r--com/android/setupwizardlib/span/LinkSpan.java11
-rw-r--r--com/android/setupwizardlib/span/LinkSpanTest.java32
-rw-r--r--com/android/setupwizardlib/view/RichTextView.java25
-rw-r--r--com/android/setupwizardlib/view/RichTextViewTest.java (renamed from com/android/setupwizardlib/test/RichTextViewTest.java)81
-rw-r--r--com/android/setupwizardlib/view/TouchableMovementMethod.java83
-rw-r--r--com/android/systemui/BatteryMeterView.java56
-rw-r--r--com/android/systemui/ForegroundServiceController.java2
-rw-r--r--com/android/systemui/ImageWallpaper.java8
-rw-r--r--com/android/systemui/OverviewProxyService.java37
-rw-r--r--com/android/systemui/Prefs.java4
-rw-r--r--com/android/systemui/ScreenDecorations.java48
-rw-r--r--com/android/systemui/SystemUIFactory.java14
-rw-r--r--com/android/systemui/classifier/AnglesClassifier.java27
-rw-r--r--com/android/systemui/classifier/AnglesVarianceEvaluator.java9
-rw-r--r--com/android/systemui/classifier/SpeedAnglesClassifier.java22
-rw-r--r--com/android/systemui/doze/DozeUi.java4
-rw-r--r--com/android/systemui/fingerprint/FingerprintDialogImpl.java25
-rw-r--r--com/android/systemui/fingerprint/FingerprintDialogView.java146
-rw-r--r--com/android/systemui/globalactions/GlobalActionsDialog.java29
-rw-r--r--com/android/systemui/keyboard/KeyboardUI.java2
-rw-r--r--com/android/systemui/keyguard/KeyguardViewMediator.java35
-rw-r--r--com/android/systemui/pip/phone/PipTouchHandler.java24
-rw-r--r--com/android/systemui/power/PowerNotificationWarnings.java23
-rw-r--r--com/android/systemui/qs/QSContainerImpl.java69
-rw-r--r--com/android/systemui/qs/QSFooter.java2
-rw-r--r--com/android/systemui/qs/QSFooterImpl.java14
-rw-r--r--com/android/systemui/qs/QSFragment.java28
-rw-r--r--com/android/systemui/qs/QuickStatusBarHeader.java56
-rw-r--r--com/android/systemui/qs/car/CarQSFooter.java1
-rw-r--r--com/android/systemui/qs/car/CarQSFragment.java72
-rw-r--r--com/android/systemui/qs/customize/CustomizeTileView.java5
-rw-r--r--com/android/systemui/qs/tileimpl/QSIconViewImpl.java21
-rw-r--r--com/android/systemui/qs/tileimpl/QSTileBaseView.java6
-rw-r--r--com/android/systemui/qs/tiles/CellularTile.java28
-rw-r--r--com/android/systemui/qs/tiles/DndTile.java57
-rw-r--r--com/android/systemui/qs/tiles/WifiTile.java25
-rw-r--r--com/android/systemui/recents/Recents.java32
-rw-r--r--com/android/systemui/recents/RecentsImpl.java4
-rw-r--r--com/android/systemui/recents/RecentsOnboarding.java72
-rw-r--r--com/android/systemui/recents/ScreenPinningRequest.java2
-rw-r--r--com/android/systemui/recents/TriangleShape.java56
-rw-r--r--com/android/systemui/shared/recents/model/IconLoader.java13
-rw-r--r--com/android/systemui/shared/system/ActivityManagerWrapper.java28
-rw-r--r--com/android/systemui/shared/system/NavigationBarCompat.java22
-rw-r--r--com/android/systemui/shared/system/PackageManagerWrapper.java42
-rw-r--r--com/android/systemui/shared/system/RecentsAnimationControllerCompat.java8
-rw-r--r--com/android/systemui/shared/system/ThreadedRendererCompat.java (renamed from android/security/keystore/InternalRecoveryServiceException.java)22
-rw-r--r--com/android/systemui/shared/system/TransactionCompat.java5
-rw-r--r--com/android/systemui/shared/system/WindowCallbacksCompat.java109
-rw-r--r--com/android/systemui/statusbar/AppOpsListener.java2
-rw-r--r--com/android/systemui/statusbar/CommandQueue.java8
-rw-r--r--com/android/systemui/statusbar/ExpandableNotificationRow.java106
-rw-r--r--com/android/systemui/statusbar/NotificationContentView.java61
-rw-r--r--com/android/systemui/statusbar/NotificationData.java19
-rw-r--r--com/android/systemui/statusbar/NotificationEntryManager.java24
-rw-r--r--com/android/systemui/statusbar/NotificationInfo.java144
-rw-r--r--com/android/systemui/statusbar/NotificationLockscreenUserManager.java20
-rw-r--r--com/android/systemui/statusbar/NotificationMediaManager.java15
-rw-r--r--com/android/systemui/statusbar/NotificationShelf.java32
-rw-r--r--com/android/systemui/statusbar/NotificationViewHierarchyManager.java3
-rw-r--r--com/android/systemui/statusbar/SmartReplyLogger.java52
-rw-r--r--com/android/systemui/statusbar/StatusBarMobileView.java18
-rw-r--r--com/android/systemui/statusbar/StatusBarWifiView.java4
-rw-r--r--com/android/systemui/statusbar/car/CarFacetButton.java48
-rw-r--r--com/android/systemui/statusbar/car/CarFacetButtonController.java32
-rw-r--r--com/android/systemui/statusbar/car/CarNavigationBarView.java13
-rw-r--r--com/android/systemui/statusbar/car/CarNavigationButton.java53
-rw-r--r--com/android/systemui/statusbar/car/CarStatusBar.java3
-rw-r--r--com/android/systemui/statusbar/car/FullscreenUserSwitcher.java84
-rw-r--r--com/android/systemui/statusbar/car/UserGridRecyclerView.java336
-rw-r--r--com/android/systemui/statusbar/car/UserGridView.java364
-rw-r--r--com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java1
-rw-r--r--com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java18
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardBouncer.java8
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java27
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardDismissHandler.java (renamed from android/security/keystore/BadCertificateFormatException.java)22
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardDismissUtil.java53
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarFragment.java8
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarInflaterView.java19
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarView.java35
-rw-r--r--com/android/systemui/statusbar/phone/NotificationIconAreaController.java9
-rw-r--r--com/android/systemui/statusbar/phone/NotificationIconContainer.java22
-rw-r--r--com/android/systemui/statusbar/phone/NotificationPanelView.java19
-rw-r--r--com/android/systemui/statusbar/phone/PanelView.java2
-rw-r--r--com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java7
-rw-r--r--com/android/systemui/statusbar/phone/QuickStepController.java100
-rw-r--r--com/android/systemui/statusbar/phone/ScrimController.java42
-rw-r--r--com/android/systemui/statusbar/phone/ScrimState.java7
-rw-r--r--com/android/systemui/statusbar/phone/StatusBar.java87
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java11
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java28
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java2
-rw-r--r--com/android/systemui/statusbar/phone/SystemUIDialog.java4
-rw-r--r--com/android/systemui/statusbar/policy/BluetoothControllerImpl.java2
-rw-r--r--com/android/systemui/statusbar/policy/Clock.java60
-rw-r--r--com/android/systemui/statusbar/policy/DateView.java32
-rw-r--r--com/android/systemui/statusbar/policy/DeadZone.java78
-rw-r--r--com/android/systemui/statusbar/policy/KeyButtonView.java38
-rw-r--r--com/android/systemui/statusbar/policy/SmartReplyView.java29
-rw-r--r--com/android/systemui/statusbar/policy/TelephonyIcons.java2
-rw-r--r--com/android/systemui/statusbar/stack/AmbientState.java21
-rw-r--r--com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java66
-rw-r--r--com/android/systemui/statusbar/stack/StackScrollAlgorithm.java2
-rw-r--r--com/android/systemui/util/wakelock/DelayedWakeLock.java2
-rw-r--r--com/android/systemui/volume/Events.java49
-rw-r--r--com/android/systemui/volume/VolumeDialogImpl.java21
-rw-r--r--com/android/uiautomator/testrunner/UiAutomatorTestCase.java106
-rw-r--r--foo/bar/ComplexDatabase.java5
-rw-r--r--java/lang/String.java2
-rw-r--r--java/lang/invoke/CallSite.java350
-rw-r--r--java/lang/invoke/MethodHandle.java1349
-rw-r--r--java/lang/invoke/MethodHandles.java3448
-rw-r--r--java/lang/invoke/MethodType.java1205
-rw-r--r--java/lang/ref/Reference.java16
-rw-r--r--java/net/Inet6AddressImpl.java8
-rw-r--r--java/security/KeyStore.java2
-rw-r--r--java/security/cert/package-info.java2
-rw-r--r--java/security/package-info.java22
-rw-r--r--java/util/LinkedHashMap.java2
-rw-r--r--java/util/Locale.java4
-rw-r--r--javax/security/auth/login/package-info.java2
685 files changed, 55650 insertions, 17027 deletions
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
index 0a4541ba..829a9444 100644
--- a/android/accessibilityservice/AccessibilityService.java
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -537,7 +537,7 @@ public abstract class AccessibilityService extends Service {
* anything behind it, then only the modal window will be reported
* (assuming it is the top one). For convenience the returned windows
* are ordered in a descending layer order, which is the windows that
- * are higher in the Z-order are reported first. Since the user can always
+ * are on top are reported first. Since the user can always
* interact with the window that has input focus by typing, the focused
* window is always returned (even if covered by a modal window).
* <p>
diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java
index 09dcbf21..ecd99a7b 100644
--- a/android/app/ActivityOptions.java
+++ b/android/app/ActivityOptions.java
@@ -1139,7 +1139,8 @@ public class ActivityOptions {
* {@link android.app.admin.DevicePolicyManager} can run in LockTask mode. Therefore, if
* {@link android.app.admin.DevicePolicyManager#isLockTaskPermitted(String)} returns
* {@code false} for the package of the target activity, a {@link SecurityException} will be
- * thrown during {@link Context#startActivity(Intent, Bundle)}.
+ * thrown during {@link Context#startActivity(Intent, Bundle)}. This method doesn't affect
+ * activities that are already running — relaunch the activity to run in lock task mode.
*
* Defaults to {@code false} if not set.
*
diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java
index 50a43989..82c3383d 100644
--- a/android/app/ActivityThread.java
+++ b/android/app/ActivityThread.java
@@ -5873,7 +5873,7 @@ public final class ActivityThread extends ClientTransactionHandler {
} finally {
// If the app targets < O-MR1, or doesn't change the thread policy
// during startup, clobber the policy to maintain behavior of b/36951662
- if (data.appInfo.targetSdkVersion <= Build.VERSION_CODES.O
+ if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
StrictMode.setThreadPolicy(savedPolicy);
}
diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java
index a68136b5..1084b425 100644
--- a/android/app/ApplicationPackageManager.java
+++ b/android/app/ApplicationPackageManager.java
@@ -2155,40 +2155,28 @@ public class ApplicationPackageManager extends PackageManager {
public String[] setPackagesSuspended(String[] packageNames, boolean suspended,
PersistableBundle appExtras, PersistableBundle launcherExtras,
String dialogMessage) {
- // TODO (b/75332201): Pass in the dialogMessage and use it in the interceptor dialog
try {
return mPM.setPackagesSuspendedAsUser(packageNames, suspended, appExtras,
- launcherExtras, mContext.getOpPackageName(), mContext.getUserId());
+ launcherExtras, dialogMessage, mContext.getOpPackageName(),
+ mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@Override
- public PersistableBundle getSuspendedPackageAppExtras(String packageName) {
+ public Bundle getSuspendedPackageAppExtras() {
+ final PersistableBundle extras;
try {
- return mPM.getSuspendedPackageAppExtras(packageName, mContext.getUserId());
+ extras = mPM.getSuspendedPackageAppExtras(mContext.getOpPackageName(),
+ mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- }
-
- @Override
- public Bundle getSuspendedPackageAppExtras() {
- final PersistableBundle extras = getSuspendedPackageAppExtras(mContext.getOpPackageName());
return extras != null ? new Bundle(extras.deepCopy()) : null;
}
@Override
- public void setSuspendedPackageAppExtras(String packageName, PersistableBundle appExtras) {
- try {
- mPM.setSuspendedPackageAppExtras(packageName, appExtras, mContext.getUserId());
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- }
- }
-
- @Override
public boolean isPackageSuspendedForUser(String packageName, int userId) {
try {
return mPM.isPackageSuspendedForUser(packageName, userId);
@@ -2199,8 +2187,12 @@ public class ApplicationPackageManager extends PackageManager {
/** @hide */
@Override
- public boolean isPackageSuspended(String packageName) {
- return isPackageSuspendedForUser(packageName, mContext.getUserId());
+ public boolean isPackageSuspended(String packageName) throws NameNotFoundException {
+ try {
+ return isPackageSuspendedForUser(packageName, mContext.getUserId());
+ } catch (IllegalArgumentException ie) {
+ throw new NameNotFoundException(packageName);
+ }
}
@Override
diff --git a/android/app/BroadcastOptions.java b/android/app/BroadcastOptions.java
index b6cff385..69c3632b 100644
--- a/android/app/BroadcastOptions.java
+++ b/android/app/BroadcastOptions.java
@@ -32,6 +32,7 @@ public class BroadcastOptions {
private long mTemporaryAppWhitelistDuration;
private int mMinManifestReceiverApiLevel = 0;
private int mMaxManifestReceiverApiLevel = Build.VERSION_CODES.CUR_DEVELOPMENT;
+ private boolean mDontSendToRestrictedApps = false;
/**
* How long to temporarily put an app on the power whitelist when executing this broadcast
@@ -52,6 +53,12 @@ public class BroadcastOptions {
static final String KEY_MAX_MANIFEST_RECEIVER_API_LEVEL
= "android:broadcast.maxManifestReceiverApiLevel";
+ /**
+ * Corresponds to {@link #setMaxManifestReceiverApiLevel}.
+ */
+ static final String KEY_DONT_SEND_TO_RESTRICTED_APPS =
+ "android:broadcast.dontSendToRestrictedApps";
+
public static BroadcastOptions makeBasic() {
BroadcastOptions opts = new BroadcastOptions();
return opts;
@@ -66,6 +73,7 @@ public class BroadcastOptions {
mMinManifestReceiverApiLevel = opts.getInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, 0);
mMaxManifestReceiverApiLevel = opts.getInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL,
Build.VERSION_CODES.CUR_DEVELOPMENT);
+ mDontSendToRestrictedApps = opts.getBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, false);
}
/**
@@ -123,6 +131,23 @@ public class BroadcastOptions {
}
/**
+ * Sets whether pending intent can be sent for an application with background restrictions
+ * @param dontSendToRestrictedApps if true, pending intent will not be sent for an application
+ * with background restrictions. Default value is {@code false}
+ */
+ public void setDontSendToRestrictedApps(boolean dontSendToRestrictedApps) {
+ mDontSendToRestrictedApps = dontSendToRestrictedApps;
+ }
+
+ /**
+ * @hide
+ * @return #setDontSendToRestrictedApps
+ */
+ public boolean isDontSendToRestrictedApps() {
+ return mDontSendToRestrictedApps;
+ }
+
+ /**
* Returns the created options as a Bundle, which can be passed to
* {@link android.content.Context#sendBroadcast(android.content.Intent)
* Context.sendBroadcast(Intent)} and related methods.
@@ -141,6 +166,9 @@ public class BroadcastOptions {
if (mMaxManifestReceiverApiLevel != Build.VERSION_CODES.CUR_DEVELOPMENT) {
b.putInt(KEY_MAX_MANIFEST_RECEIVER_API_LEVEL, mMaxManifestReceiverApiLevel);
}
+ if (mDontSendToRestrictedApps) {
+ b.putBoolean(KEY_DONT_SEND_TO_RESTRICTED_APPS, true);
+ }
return b.isEmpty() ? null : b;
}
}
diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java
index 71b88fa4..9a491bc3 100644
--- a/android/app/ContextImpl.java
+++ b/android/app/ContextImpl.java
@@ -16,6 +16,7 @@
package android.app;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -88,11 +89,12 @@ import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicInteger;
class ReceiverRestrictedContext extends ContextWrapper {
ReceiverRestrictedContext(Context base) {
@@ -212,13 +214,24 @@ class ContextImpl extends Context {
static final int STATE_UNINITIALIZED = 0;
static final int STATE_INITIALIZING = 1;
static final int STATE_READY = 2;
+ static final int STATE_NOT_FOUND = 3;
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_UNINITIALIZED,
+ STATE_INITIALIZING,
+ STATE_READY,
+ STATE_NOT_FOUND,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ServiceInitializationState {}
/**
* Initialization state for each service. Any of {@link #STATE_UNINITIALIZED},
* {@link #STATE_INITIALIZING} or {@link #STATE_READY},
*/
- final AtomicInteger[] mServiceInitializationStateArray =
- SystemServiceRegistry.createServiceInitializationStateArray();
+ @ServiceInitializationState
+ final int[] mServiceInitializationStateArray = new int[mServiceCache.length];
static ContextImpl getImpl(Context context) {
Context nextContext;
diff --git a/android/app/Notification.java b/android/app/Notification.java
index 4326ee3e..2b4f4206 100644
--- a/android/app/Notification.java
+++ b/android/app/Notification.java
@@ -3902,7 +3902,7 @@ public class Notification implements Parcelable
* @deprecated use {@link #addPerson(Person)}
*/
public Builder addPerson(String uri) {
- addPerson(new Person().setUri(uri));
+ addPerson(new Person.Builder().setUri(uri).build());
return this;
}
@@ -4588,10 +4588,18 @@ public class Notification implements Parcelable
bindHeaderChronometerAndTime(contentView);
bindProfileBadge(contentView);
}
+ bindActivePermissions(contentView);
bindExpandButton(contentView);
mN.mUsesStandardHeader = true;
}
+ private void bindActivePermissions(RemoteViews contentView) {
+ int color = isColorized() ? getPrimaryTextColor() : getSecondaryTextColor();
+ contentView.setDrawableTint(R.id.camera, false, color, PorterDuff.Mode.SRC_ATOP);
+ contentView.setDrawableTint(R.id.mic, false, color, PorterDuff.Mode.SRC_ATOP);
+ contentView.setDrawableTint(R.id.overlay, false, color, PorterDuff.Mode.SRC_ATOP);
+ }
+
private void bindExpandButton(RemoteViews contentView) {
int color = isColorized() ? getPrimaryTextColor() : getSecondaryTextColor();
contentView.setDrawableTint(R.id.expand_button, false, color,
@@ -6384,7 +6392,7 @@ public class Notification implements Parcelable
* @deprecated use {@code MessagingStyle(Person)}
*/
public MessagingStyle(@NonNull CharSequence userDisplayName) {
- this(new Person().setName(userDisplayName));
+ this(new Person.Builder().setName(userDisplayName).build());
}
/**
@@ -6431,6 +6439,7 @@ public class Notification implements Parcelable
/**
* @return the user to be displayed for any replies sent by the user
*/
+ @NonNull
public Person getUser() {
return mUser;
}
@@ -6489,7 +6498,7 @@ public class Notification implements Parcelable
*/
public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
return addMessage(text, timestamp,
- sender == null ? null : new Person().setName(sender));
+ sender == null ? null : new Person.Builder().setName(sender).build());
}
/**
@@ -6505,7 +6514,8 @@ public class Notification implements Parcelable
*
* @return this object for method chaining
*/
- public MessagingStyle addMessage(CharSequence text, long timestamp, Person sender) {
+ public MessagingStyle addMessage(@NonNull CharSequence text, long timestamp,
+ @Nullable Person sender) {
return addMessage(new Message(text, timestamp, sender));
}
@@ -6661,7 +6671,7 @@ public class Notification implements Parcelable
mUser = extras.getParcelable(EXTRA_MESSAGING_PERSON);
if (mUser == null) {
CharSequence displayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
- mUser = new Person().setName(displayName);
+ mUser = new Person.Builder().setName(displayName).build();
}
mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
@@ -6678,7 +6688,8 @@ public class Notification implements Parcelable
public RemoteViews makeContentView(boolean increasedHeight) {
mBuilder.mOriginalActions = mBuilder.mActions;
mBuilder.mActions = new ArrayList<>();
- RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */);
+ RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */,
+ true /* showReplyIcon */);
mBuilder.mActions = mBuilder.mOriginalActions;
mBuilder.mOriginalActions = null;
return remoteViews;
@@ -6765,11 +6776,19 @@ public class Notification implements Parcelable
*/
@Override
public RemoteViews makeBigContentView() {
- return makeMessagingView(false /* isCollapsed */);
+ return makeMessagingView(false /* displayImagesAtEnd */, false /* showReplyIcon */);
}
+ /**
+ * Create a messaging layout.
+ *
+ * @param displayImagesAtEnd should images be displayed at the end of the content instead
+ * of inline.
+ * @param showReplyIcon Should the reply affordance be shown at the end of the notification
+ * @return the created remoteView.
+ */
@NonNull
- private RemoteViews makeMessagingView(boolean isCollapsed) {
+ private RemoteViews makeMessagingView(boolean displayImagesAtEnd, boolean showReplyIcon) {
CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
? super.mBigContentTitle
: mConversationTitle;
@@ -6780,24 +6799,24 @@ public class Notification implements Parcelable
nameReplacement = conversationTitle;
conversationTitle = null;
}
- boolean hideLargeIcon = !isCollapsed || isOneToOne;
+ boolean hideLargeIcon = !showReplyIcon || isOneToOne;
RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
mBuilder.getMessagingLayoutResource(),
mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null)
.hideLargeIcon(hideLargeIcon)
.headerTextSecondary(conversationTitle)
- .alwaysShowReply(isCollapsed));
+ .alwaysShowReply(showReplyIcon));
addExtras(mBuilder.mN.extras);
// also update the end margin if there is an image
int endMargin = R.dimen.notification_content_margin_end;
- if (isCollapsed) {
+ if (showReplyIcon) {
endMargin = R.dimen.notification_content_plus_picture_margin_end;
}
contentView.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin);
contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
mBuilder.resolveContrastColor());
- contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsCollapsed",
- isCollapsed);
+ contentView.setBoolean(R.id.status_bar_latest_event_content, "setDisplayImagesAtEnd",
+ displayImagesAtEnd);
contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
mBuilder.mN.mLargeIcon);
contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement",
@@ -6864,7 +6883,8 @@ public class Notification implements Parcelable
*/
@Override
public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
- RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */);
+ RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */,
+ false /* showReplyIcon */);
remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
return remoteViews;
}
@@ -6906,7 +6926,8 @@ public class Notification implements Parcelable
* @deprecated use {@code Message(CharSequence, long, Person)}
*/
public Message(CharSequence text, long timestamp, CharSequence sender){
- this(text, timestamp, sender == null ? null : new Person().setName(sender));
+ this(text, timestamp, sender == null ? null
+ : new Person.Builder().setName(sender).build());
}
/**
@@ -6917,13 +6938,14 @@ public class Notification implements Parcelable
* Should be <code>null</code> for messages by the current user, in which case
* the platform will insert the user set in {@code MessagingStyle(Person)}.
* <p>
- * The person provided should contain an Icon, set with {@link Person#setIcon(Icon)}
- * and also have a name provided with {@link Person#setName(CharSequence)}. If multiple
- * users have the same name, consider providing a key with {@link Person#setKey(String)}
- * in order to differentiate between the different users.
+ * The person provided should contain an Icon, set with
+ * {@link Person.Builder#setIcon(Icon)} and also have a name provided
+ * with {@link Person.Builder#setName(CharSequence)}. If multiple users have the same
+ * name, consider providing a key with {@link Person.Builder#setKey(String)} in order
+ * to differentiate between the different users.
* </p>
*/
- public Message(CharSequence text, long timestamp, @Nullable Person sender){
+ public Message(@NonNull CharSequence text, long timestamp, @Nullable Person sender) {
mText = text;
mTimestamp = timestamp;
mSender = sender;
@@ -7082,7 +7104,7 @@ public class Notification implements Parcelable
// the native api instead
CharSequence senderName = bundle.getCharSequence(KEY_SENDER);
if (senderName != null) {
- senderPerson = new Person().setName(senderName);
+ senderPerson = new Person.Builder().setName(senderName).build();
}
}
Message message = new Message(bundle.getCharSequence(KEY_TEXT),
@@ -7777,217 +7799,6 @@ public class Notification implements Parcelable
}
}
- /**
- * A Person associated with this Notification.
- */
- public static final class Person implements Parcelable {
- @Nullable private CharSequence mName;
- @Nullable private Icon mIcon;
- @Nullable private String mUri;
- @Nullable private String mKey;
- private boolean mBot;
- private boolean mImportant;
-
- protected Person(Parcel in) {
- mName = in.readCharSequence();
- if (in.readInt() != 0) {
- mIcon = Icon.CREATOR.createFromParcel(in);
- }
- mUri = in.readString();
- mKey = in.readString();
- mImportant = in.readBoolean();
- mBot = in.readBoolean();
- }
-
- /**
- * Create a new person.
- */
- public Person() {
- }
-
- /**
- * Give this person a name.
- *
- * @param name the name of this person
- */
- public Person setName(@Nullable CharSequence name) {
- this.mName = name;
- return this;
- }
-
- /**
- * Add an icon for this person.
- * <br />
- * This is currently only used for {@link MessagingStyle} notifications and should not be
- * provided otherwise, in order to save memory. The system will prefer this icon over any
- * images that are resolved from the URI.
- *
- * @param icon the icon of the person
- */
- public Person setIcon(@Nullable Icon icon) {
- this.mIcon = icon;
- return this;
- }
-
- /**
- * Set a URI associated with this person.
- *
- * <P>
- * Depending on user preferences, adding a URI to a Person may allow the notification to
- * pass through interruption filters, if this notification is of
- * category {@link #CATEGORY_CALL} or {@link #CATEGORY_MESSAGE}.
- * The addition of people may also cause this notification to appear more prominently in
- * the user interface.
- * </P>
- *
- * <P>
- * The person should be specified by the {@code String} representation of a
- * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
- * </P>
- *
- * <P>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}.
- * </P>
- *
- * @param uri a URI for the person
- */
- public Person setUri(@Nullable String uri) {
- mUri = uri;
- return this;
- }
-
- /**
- * Add a key to this person in order to uniquely identify it.
- * This is especially useful if the name doesn't uniquely identify this person or if the
- * display name is a short handle of the actual name.
- *
- * <P>If no key is provided, the name serves as as the key for the purpose of
- * identification.</P>
- *
- * @param key the key that uniquely identifies this person
- */
- public Person setKey(@Nullable String key) {
- mKey = key;
- return this;
- }
-
- /**
- * Sets whether this is an important person. Use this method to denote users who frequently
- * interact with the user of this device, when it is not possible to refer to the user
- * by {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
- *
- * @param isImportant {@code true} if this is an important person, {@code false} otherwise.
- */
- public Person setImportant(boolean isImportant) {
- mImportant = isImportant;
- return this;
- }
-
- /**
- * Sets whether this person is a machine rather than a human.
- *
- * @param isBot {@code true} if this person is a machine, {@code false} otherwise.
- */
- public Person setBot(boolean isBot) {
- mBot = isBot;
- return this;
- }
-
- /**
- * @return the uri provided for this person or {@code null} if no Uri was provided
- */
- @Nullable
- public String getUri() {
- return mUri;
- }
-
- /**
- * @return the name provided for this person or {@code null} if no name was provided
- */
- @Nullable
- public CharSequence getName() {
- return mName;
- }
-
- /**
- * @return the icon provided for this person or {@code null} if no icon was provided
- */
- @Nullable
- public Icon getIcon() {
- return mIcon;
- }
-
- /**
- * @return the key provided for this person or {@code null} if no key was provided
- */
- @Nullable
- public String getKey() {
- return mKey;
- }
-
- /**
- * @return whether this Person is a machine.
- */
- public boolean isBot() {
- return mBot;
- }
-
- /**
- * @return whether this Person is important.
- */
- public boolean isImportant() {
- return mImportant;
- }
-
- /**
- * @return the URI associated with this person, or "name:mName" otherwise
- * @hide
- */
- public String resolveToLegacyUri() {
- if (mUri != null) {
- return mUri;
- }
- if (mName != null) {
- return "name:" + mName;
- }
- return "";
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, @WriteFlags int flags) {
- dest.writeCharSequence(mName);
- if (mIcon != null) {
- dest.writeInt(1);
- mIcon.writeToParcel(dest, 0);
- } else {
- dest.writeInt(0);
- }
- dest.writeString(mUri);
- dest.writeString(mKey);
- dest.writeBoolean(mImportant);
- dest.writeBoolean(mBot);
- }
-
- public static final Creator<Person> CREATOR = new Creator<Person>() {
- @Override
- public Person createFromParcel(Parcel in) {
- return new Person(in);
- }
-
- @Override
- public Person[] newArray(int size) {
- return new Person[size];
- }
- };
- }
-
// When adding a new Style subclass here, don't forget to update
// Builder.getNotificationStyleClass.
diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java
index 4a7cf623..9e47ced4 100644
--- a/android/app/NotificationChannel.java
+++ b/android/app/NotificationChannel.java
@@ -328,7 +328,8 @@ public final class NotificationChannel implements Parcelable {
* Group information is only used for presentation, not for behavior.
*
* Only modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}, unless the
+ * channel is not currently part of a group.
*
* @param groupId the id of a group created by
* {@link NotificationManager#createNotificationChannelGroup(NotificationChannelGroup)}.
@@ -341,6 +342,9 @@ public final class NotificationChannel implements Parcelable {
* Sets whether notifications posted to this channel can appear as application icon badges
* in a Launcher.
*
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
+ *
* @param showBadge true if badges should be allowed to be shown.
*/
public void setShowBadge(boolean showBadge) {
@@ -353,7 +357,7 @@ public final class NotificationChannel implements Parcelable {
* least {@link NotificationManager#IMPORTANCE_DEFAULT} should have a sound.
*
* Only modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
*/
public void setSound(Uri sound, AudioAttributes audioAttributes) {
this.mSound = sound;
@@ -365,7 +369,7 @@ public final class NotificationChannel implements Parcelable {
* on devices that support that feature.
*
* Only modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
*/
public void enableLights(boolean lights) {
this.mLights = lights;
@@ -376,7 +380,7 @@ public final class NotificationChannel implements Parcelable {
* {@link #enableLights(boolean) enabled} on this channel and the device supports that feature.
*
* Only modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
*/
public void setLightColor(int argb) {
this.mLightColor = argb;
@@ -387,7 +391,7 @@ public final class NotificationChannel implements Parcelable {
* be set with {@link #setVibrationPattern(long[])}.
*
* Only modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
*/
public void enableVibration(boolean vibration) {
this.mVibrationEnabled = vibration;
@@ -399,7 +403,7 @@ public final class NotificationChannel implements Parcelable {
* vibration} as well. Otherwise, vibration will be disabled.
*
* Only modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
*/
public void setVibrationPattern(long[] vibrationPattern) {
this.mVibrationEnabled = vibrationPattern != null && vibrationPattern.length > 0;
@@ -407,9 +411,10 @@ public final class NotificationChannel implements Parcelable {
}
/**
- * Sets the level of interruption of this notification channel. Only
- * modifiable before the channel is submitted to
- * {@link NotificationManager#notify(String, int, Notification)}.
+ * Sets the level of interruption of this notification channel.
+ *
+ * Only modifiable before the channel is submitted to
+ * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
*
* @param importance the amount the user should be interrupted by
* notifications from this channel.
diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java
index 46d1264f..757fc643 100644
--- a/android/app/NotificationManager.java
+++ b/android/app/NotificationManager.java
@@ -1145,6 +1145,21 @@ public class NotificationManager {
SUPPRESSED_EFFECT_NOTIFICATION_LIST
};
+ private static final int[] SCREEN_OFF_SUPPRESSED_EFFECTS = {
+ SUPPRESSED_EFFECT_SCREEN_OFF,
+ SUPPRESSED_EFFECT_FULL_SCREEN_INTENT,
+ SUPPRESSED_EFFECT_LIGHTS,
+ SUPPRESSED_EFFECT_AMBIENT,
+ };
+
+ private static final int[] SCREEN_ON_SUPPRESSED_EFFECTS = {
+ SUPPRESSED_EFFECT_SCREEN_ON,
+ SUPPRESSED_EFFECT_PEEK,
+ SUPPRESSED_EFFECT_STATUS_BAR,
+ SUPPRESSED_EFFECT_BADGE,
+ SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ };
+
/**
* Visual effects to suppress for a notification that is filtered by Do Not Disturb mode.
* Bitmask of SUPPRESSED_EFFECT_* constants.
@@ -1297,6 +1312,58 @@ public class NotificationManager {
return true;
}
+ /**
+ * @hide
+ */
+ public static boolean areAnyScreenOffEffectsSuppressed(int effects) {
+ for (int i = 0; i < SCREEN_OFF_SUPPRESSED_EFFECTS.length; i++) {
+ final int effect = SCREEN_OFF_SUPPRESSED_EFFECTS[i];
+ if ((effects & effect) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean areAnyScreenOnEffectsSuppressed(int effects) {
+ for (int i = 0; i < SCREEN_ON_SUPPRESSED_EFFECTS.length; i++) {
+ final int effect = SCREEN_ON_SUPPRESSED_EFFECTS[i];
+ if ((effects & effect) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public static int toggleScreenOffEffectsSuppressed(int currentEffects, boolean suppress) {
+ return toggleEffects(currentEffects, SCREEN_OFF_SUPPRESSED_EFFECTS, suppress);
+ }
+
+ /**
+ * @hide
+ */
+ public static int toggleScreenOnEffectsSuppressed(int currentEffects, boolean suppress) {
+ return toggleEffects(currentEffects, SCREEN_ON_SUPPRESSED_EFFECTS, suppress);
+ }
+
+ private static int toggleEffects(int currentEffects, int[] effects, boolean suppress) {
+ for (int i = 0; i < effects.length; i++) {
+ final int effect = effects[i];
+ if (suppress) {
+ currentEffects |= effect;
+ } else {
+ currentEffects &= ~effect;
+ }
+ }
+ return currentEffects;
+ }
+
public static String suppressedEffectsToString(int effects) {
if (effects <= 0) return "";
final StringBuilder sb = new StringBuilder();
diff --git a/android/app/Person.java b/android/app/Person.java
new file mode 100644
index 00000000..3884a8d4
--- /dev/null
+++ b/android/app/Person.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
+ * platform. For example, this could represent the sender of a message.
+ */
+public final class Person implements Parcelable {
+
+ @Nullable private CharSequence mName;
+ @Nullable private Icon mIcon;
+ @Nullable private String mUri;
+ @Nullable private String mKey;
+ private boolean mIsBot;
+ private boolean mIsImportant;
+
+ private Person(Parcel in) {
+ mName = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ }
+ mUri = in.readString();
+ mKey = in.readString();
+ mIsImportant = in.readBoolean();
+ mIsBot = in.readBoolean();
+ }
+
+ private Person(Builder builder) {
+ mName = builder.mName;
+ mIcon = builder.mIcon;
+ mUri = builder.mUri;
+ mKey = builder.mKey;
+ mIsBot = builder.mIsBot;
+ mIsImportant = builder.mIsImportant;
+ }
+
+ /** Creates and returns a new {@link Builder} initialized with this Person's data. */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * @return the uri provided for this person or {@code null} if no Uri was provided.
+ */
+ @Nullable
+ public String getUri() {
+ return mUri;
+ }
+
+ /**
+ * @return the name provided for this person or {@code null} if no name was provided.
+ */
+ @Nullable
+ public CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * @return the icon provided for this person or {@code null} if no icon was provided.
+ */
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * @return the key provided for this person or {@code null} if no key was provided.
+ */
+ @Nullable
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * @return whether this Person is a machine.
+ */
+ public boolean isBot() {
+ return mIsBot;
+ }
+
+ /**
+ * @return whether this Person is important.
+ */
+ public boolean isImportant() {
+ return mIsImportant;
+ }
+
+ /**
+ * @return the URI associated with this person, or "name:mName" otherwise
+ * @hide
+ */
+ public String resolveToLegacyUri() {
+ if (mUri != null) {
+ return mUri;
+ }
+ if (mName != null) {
+ return "name:" + mName;
+ }
+ return "";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, @WriteFlags int flags) {
+ dest.writeCharSequence(mName);
+ if (mIcon != null) {
+ dest.writeInt(1);
+ mIcon.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeString(mUri);
+ dest.writeString(mKey);
+ dest.writeBoolean(mIsImportant);
+ dest.writeBoolean(mIsBot);
+ }
+
+ /** Builder for the immutable {@link Person} class. */
+ public static class Builder {
+ @Nullable private CharSequence mName;
+ @Nullable private Icon mIcon;
+ @Nullable private String mUri;
+ @Nullable private String mKey;
+ private boolean mIsBot;
+ private boolean mIsImportant;
+
+ /** Creates a new, empty {@link Builder}. */
+ public Builder() {
+ }
+
+ private Builder(Person person) {
+ mName = person.mName;
+ mIcon = person.mIcon;
+ mUri = person.mUri;
+ mKey = person.mKey;
+ mIsBot = person.mIsBot;
+ mIsImportant = person.mIsImportant;
+ }
+
+ /**
+ * Give this person a name.
+ *
+ * @param name the name of this person.
+ */
+ @NonNull
+ public Person.Builder setName(@Nullable CharSequence name) {
+ this.mName = name;
+ return this;
+ }
+
+ /**
+ * Add an icon for this person.
+ * <br />
+ * The system will prefer this icon over any images that are resolved from the URI.
+ *
+ * @param icon the icon of the person.
+ */
+ @NonNull
+ public Person.Builder setIcon(@Nullable Icon icon) {
+ this.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set a URI associated with this person.
+ *
+ * <P>
+ * The person should be specified by the {@code String} representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * </P>
+ *
+ * <P>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}.
+ * </P>
+ *
+ * @param uri a URI for the person.
+ */
+ @NonNull
+ public Person.Builder setUri(@Nullable String uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Add a key to this person in order to uniquely identify it.
+ * This is especially useful if the name doesn't uniquely identify this person or if the
+ * display name is a short handle of the actual name.
+ *
+ * <P>If no key is provided, the name serves as the key for the purpose of
+ * identification.</P>
+ *
+ * @param key the key that uniquely identifies this person.
+ */
+ @NonNull
+ public Person.Builder setKey(@Nullable String key) {
+ mKey = key;
+ return this;
+ }
+
+ /**
+ * Sets whether this is an important person. Use this method to denote users who frequently
+ * interact with the user of this device when {@link #setUri(String)} isn't provided with
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, and instead with
+ * the {@code mailto:} or {@code tel:} schemas.
+ *
+ * @param isImportant {@code true} if this is an important person, {@code false} otherwise.
+ */
+ @NonNull
+ public Person.Builder setImportant(boolean isImportant) {
+ mIsImportant = isImportant;
+ return this;
+ }
+
+ /**
+ * Sets whether this person is a machine rather than a human.
+ *
+ * @param isBot {@code true} if this person is a machine, {@code false} otherwise.
+ */
+ @NonNull
+ public Person.Builder setBot(boolean isBot) {
+ mIsBot = isBot;
+ return this;
+ }
+
+ /** Creates and returns the {@link Person} this builder represents. */
+ @NonNull
+ public Person build() {
+ return new Person(this);
+ }
+ }
+
+ public static final Creator<Person> CREATOR = new Creator<Person>() {
+ @Override
+ public Person createFromParcel(Parcel in) {
+ return new Person(in);
+ }
+
+ @Override
+ public Person[] newArray(int size) {
+ return new Person[size];
+ }
+ };
+}
diff --git a/android/app/RemoteAction.java b/android/app/RemoteAction.java
index 47741c02..c1746655 100644
--- a/android/app/RemoteAction.java
+++ b/android/app/RemoteAction.java
@@ -122,6 +122,7 @@ public final class RemoteAction implements Parcelable {
public RemoteAction clone() {
RemoteAction action = new RemoteAction(mIcon, mTitle, mContentDescription, mActionIntent);
action.setEnabled(mEnabled);
+ action.setShouldShowIcon(mShouldShowIcon);
return action;
}
diff --git a/android/app/StatsManager.java b/android/app/StatsManager.java
index 4a6fa8c2..8783d945 100644
--- a/android/app/StatsManager.java
+++ b/android/app/StatsManager.java
@@ -23,6 +23,7 @@ import android.os.IBinder;
import android.os.IStatsManager;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.AndroidException;
import android.util.Slog;
/**
@@ -82,55 +83,74 @@ public final class StatsManager {
}
/**
- * Clients can send a configuration and simultaneously registers the name of a broadcast
- * receiver that listens for when it should request data.
+ * Adds the given configuration and associates it with the given configKey. If a config with the
+ * given configKey already exists for the caller's uid, it is replaced with the new one.
*
* @param configKey An arbitrary integer that allows clients to track the configuration.
- * @param config Wire-encoded StatsDConfig proto that specifies metrics (and all
+ * @param config Wire-encoded StatsdConfig proto that specifies metrics (and all
* dependencies eg, conditions and matchers).
- * @return true if successful
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
+ * @throws IllegalArgumentException if config is not a wire-encoded StatsdConfig proto
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean addConfiguration(long configKey, byte[] config) {
+ public void addConfig(long configKey, byte[] config) throws StatsUnavailableException {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
- if (service == null) {
- Slog.e(TAG, "Failed to find statsd when adding configuration");
- return false;
- }
- return service.addConfiguration(configKey, config);
+ service.addConfiguration(configKey, config); // can throw IllegalArgumentException
} catch (RemoteException e) {
Slog.e(TAG, "Failed to connect to statsd when adding configuration");
- return false;
+ throw new StatsUnavailableException("could not connect", e);
}
}
}
/**
+ * TODO: Temporary for backwards compatibility. Remove.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean addConfiguration(long configKey, byte[] config) {
+ try {
+ addConfig(configKey, config);
+ return true;
+ } catch (StatsUnavailableException | IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
* Remove a configuration from logging.
*
* @param configKey Configuration key to remove.
- * @return true if successful
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean removeConfiguration(long configKey) {
+ public void removeConfig(long configKey) throws StatsUnavailableException {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
- if (service == null) {
- Slog.e(TAG, "Failed to find statsd when removing configuration");
- return false;
- }
- return service.removeConfiguration(configKey);
+ service.removeConfiguration(configKey);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to connect to statsd when removing configuration");
- return false;
+ throw new StatsUnavailableException("could not connect", e);
}
}
}
/**
+ * TODO: Temporary for backwards compatibility. Remove.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean removeConfiguration(long configKey) {
+ try {
+ removeConfig(configKey);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
* Set the PendingIntent to be used when broadcasting subscriber information to the given
* subscriberId within the given config.
* <p>
@@ -150,126 +170,168 @@ public final class StatsManager {
* {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
* <p>
* This function can only be called by the owner (uid) of the config. It must be called each
- * time statsd starts. The config must have been added first (via addConfiguration()).
+ * time statsd starts. The config must have been added first (via {@link #addConfig}).
*
- * @param configKey The integer naming the config to which this subscriber is attached.
- * @param subscriberId ID of the subscriber, as used in the config.
* @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
* associated with the given subscriberId. May be null, in which case
* it undoes any previous setting of this subscriberId.
- * @return true if successful
+ * @param configKey The integer naming the config to which this subscriber is attached.
+ * @param subscriberId ID of the subscriber, as used in the config.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean setBroadcastSubscriber(
- long configKey, long subscriberId, PendingIntent pendingIntent) {
+ public void setBroadcastSubscriber(
+ PendingIntent pendingIntent, long configKey, long subscriberId)
+ throws StatsUnavailableException {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
- if (service == null) {
- Slog.e(TAG, "Failed to find statsd when adding broadcast subscriber");
- return false;
- }
if (pendingIntent != null) {
// Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
IBinder intentSender = pendingIntent.getTarget().asBinder();
- return service.setBroadcastSubscriber(configKey, subscriberId, intentSender);
+ service.setBroadcastSubscriber(configKey, subscriberId, intentSender);
} else {
- return service.unsetBroadcastSubscriber(configKey, subscriberId);
+ service.unsetBroadcastSubscriber(configKey, subscriberId);
}
} catch (RemoteException e) {
Slog.e(TAG, "Failed to connect to statsd when adding broadcast subscriber", e);
- return false;
+ throw new StatsUnavailableException("could not connect", e);
}
}
}
/**
+ * TODO: Temporary for backwards compatibility. Remove.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean setBroadcastSubscriber(
+ long configKey, long subscriberId, PendingIntent pendingIntent) {
+ try {
+ setBroadcastSubscriber(pendingIntent, configKey, subscriberId);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
* Registers the operation that is called to retrieve the metrics data. This must be called
- * each time statsd starts. The config must have been added first (via addConfiguration(),
- * although addConfiguration could have been called on a previous boot). This operation allows
+ * each time statsd starts. The config must have been added first (via {@link #addConfig},
+ * although addConfig could have been called on a previous boot). This operation allows
* statsd to send metrics data whenever statsd determines that the metrics in memory are
- * approaching the memory limits. The fetch operation should call {@link #getData} to fetch the
- * data, which also deletes the retrieved metrics from statsd's memory.
+ * approaching the memory limits. The fetch operation should call {@link #getReports} to fetch
+ * the data, which also deletes the retrieved metrics from statsd's memory.
*
- * @param configKey The integer naming the config to which this operation is attached.
* @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
* associated with the given subscriberId. May be null, in which case
* it removes any associated pending intent with this configKey.
- * @return true if successful
+ * @param configKey The integer naming the config to which this operation is attached.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
+ public void setFetchReportsOperation(PendingIntent pendingIntent, long configKey)
+ throws StatsUnavailableException {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
- if (service == null) {
- Slog.e(TAG, "Failed to find statsd when registering data listener.");
- return false;
- }
if (pendingIntent == null) {
- return service.removeDataFetchOperation(configKey);
+ service.removeDataFetchOperation(configKey);
} else {
// Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
IBinder intentSender = pendingIntent.getTarget().asBinder();
- return service.setDataFetchOperation(configKey, intentSender);
+ service.setDataFetchOperation(configKey, intentSender);
}
} catch (RemoteException e) {
Slog.e(TAG, "Failed to connect to statsd when registering data listener.");
- return false;
+ throw new StatsUnavailableException("could not connect", e);
}
}
}
/**
- * Clients can request data with a binder call. This getter is destructive and also clears
- * the retrieved metrics from statsd memory.
+ * TODO: Temporary for backwards compatibility. Remove.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean setDataFetchOperation(long configKey, PendingIntent pendingIntent) {
+ try {
+ setFetchReportsOperation(pendingIntent, configKey);
+ return true;
+ } catch (StatsUnavailableException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Request the data collected for the given configKey.
+ * This getter is destructive - it also clears the retrieved metrics from statsd's memory.
*
* @param configKey Configuration key to retrieve data from.
- * @return Serialized ConfigMetricsReportList proto. Returns null on failure (eg, if statsd
- * crashed).
+ * @return Serialized ConfigMetricsReportList proto.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
*/
@RequiresPermission(Manifest.permission.DUMP)
- public @Nullable byte[] getData(long configKey) {
+ public byte[] getReports(long configKey) throws StatsUnavailableException {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
- if (service == null) {
- Slog.e(TAG, "Failed to find statsd when getting data");
- return null;
- }
return service.getData(configKey);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to connect to statsd when getting data");
- return null;
+ throw new StatsUnavailableException("could not connect", e);
}
}
}
/**
+ * TODO: Temporary for backwards compatibility. Remove.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public @Nullable byte[] getData(long configKey) {
+ try {
+ return getReports(configKey);
+ } catch (StatsUnavailableException e) {
+ return null;
+ }
+ }
+
+ /**
* Clients can request metadata for statsd. Will contain stats across all configurations but not
- * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+ * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
* This getter is not destructive and will not reset any metrics/counters.
*
- * @return Serialized StatsdStatsReport proto. Returns null on failure (eg, if statsd crashed).
+ * @return Serialized StatsdStatsReport proto.
+ * @throws StatsUnavailableException if unsuccessful due to failing to connect to stats service
*/
@RequiresPermission(Manifest.permission.DUMP)
- public @Nullable byte[] getMetadata() {
+ public byte[] getStatsMetadata() throws StatsUnavailableException {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
- if (service == null) {
- Slog.e(TAG, "Failed to find statsd when getting metadata");
- return null;
- }
return service.getMetadata();
} catch (RemoteException e) {
Slog.e(TAG, "Failed to connect to statsd when getting metadata");
- return null;
+ throw new StatsUnavailableException("could not connect", e);
}
}
}
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getReports(long)}.
+ * This getter is not destructive and will not reset any metrics/counters.
+ *
+ * @return Serialized StatsdStatsReport proto. Returns null on failure (eg, if statsd crashed).
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public @Nullable byte[] getMetadata() {
+ try {
+ return getStatsMetadata();
+ } catch (StatsUnavailableException e) {
+ return null;
+ }
+ }
+
private class StatsdDeathRecipient implements IBinder.DeathRecipient {
@Override
public void binderDied() {
@@ -279,14 +341,33 @@ public final class StatsManager {
}
}
- private IStatsManager getIStatsManagerLocked() throws RemoteException {
+ private IStatsManager getIStatsManagerLocked() throws StatsUnavailableException {
if (mService != null) {
return mService;
}
mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats"));
- if (mService != null) {
+ if (mService == null) {
+ throw new StatsUnavailableException("could not be found");
+ }
+ try {
mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0);
+ } catch (RemoteException e) {
+ throw new StatsUnavailableException("could not connect when linkToDeath", e);
}
return mService;
}
+
+ /**
+ * Exception thrown when communication with the stats service fails (eg if it is not available).
+ * This might be thrown early during boot before the stats service has started or if it crashed.
+ */
+ public static class StatsUnavailableException extends AndroidException {
+ public StatsUnavailableException(String reason) {
+ super("Failed to connect to statsd: " + reason);
+ }
+
+ public StatsUnavailableException(String reason, Throwable e) {
+ super("Failed to connect to statsd: " + reason, e);
+ }
+ }
}
diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java
index 1776eace..246d4a37 100644
--- a/android/app/SystemServiceRegistry.java
+++ b/android/app/SystemServiceRegistry.java
@@ -18,6 +18,7 @@ package android.app;
import android.accounts.AccountManager;
import android.accounts.IAccountManager;
+import android.app.ContextImpl.ServiceInitializationState;
import android.app.admin.DevicePolicyManager;
import android.app.admin.IDevicePolicyManager;
import android.app.job.IJobScheduler;
@@ -104,10 +105,12 @@ import android.nfc.NfcManager;
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.Build;
+import android.os.DeviceIdleManager;
import android.os.DropBoxManager;
import android.os.HardwarePropertiesManager;
import android.os.IBatteryPropertiesRegistrar;
import android.os.IBinder;
+import android.os.IDeviceIdleController;
import android.os.IHardwarePropertiesManager;
import android.os.IPowerManager;
import android.os.IRecoverySystem;
@@ -160,7 +163,6 @@ import com.android.internal.os.IDropBoxManagerService;
import com.android.internal.policy.PhoneLayoutInflater;
import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicInteger;
/**
* Manages all of the system services that can be returned by {@link Context#getSystemService}.
@@ -279,12 +281,12 @@ final class SystemServiceRegistry {
}});
registerService(Context.IPSEC_SERVICE, IpSecManager.class,
- new StaticServiceFetcher<IpSecManager>() {
+ new CachedServiceFetcher<IpSecManager>() {
@Override
- public IpSecManager createService() {
+ public IpSecManager createService(ContextImpl ctx) throws ServiceNotFoundException {
IBinder b = ServiceManager.getService(Context.IPSEC_SERVICE);
IIpSecService service = IIpSecService.Stub.asInterface(b);
- return new IpSecManager(service);
+ return new IpSecManager(ctx, service);
}});
registerService(Context.COUNTRY_DETECTOR, CountryDetector.class,
@@ -984,6 +986,17 @@ final class SystemServiceRegistry {
ctx.mMainThread.getHandler());
}
});
+
+ registerService(Context.DEVICE_IDLE_CONTROLLER, DeviceIdleManager.class,
+ new CachedServiceFetcher<DeviceIdleManager>() {
+ @Override
+ public DeviceIdleManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IDeviceIdleController service = IDeviceIdleController.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(
+ Context.DEVICE_IDLE_CONTROLLER));
+ return new DeviceIdleManager(ctx.getOuterContext(), service);
+ }});
}
/**
@@ -993,10 +1006,6 @@ final class SystemServiceRegistry {
return new Object[sServiceCacheSize];
}
- public static AtomicInteger[] createServiceInitializationStateArray() {
- return new AtomicInteger[sServiceCacheSize];
- }
-
/**
* Gets a system service from a given context.
*/
@@ -1037,7 +1046,10 @@ final class SystemServiceRegistry {
static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
private final int mCacheIndex;
- public CachedServiceFetcher() {
+ CachedServiceFetcher() {
+ // Note this class must be instantiated only by the static initializer of the
+ // outer class (SystemServiceRegistry), which already does the synchronization,
+ // so bare access to sServiceCacheSize is okay here.
mCacheIndex = sServiceCacheSize++;
}
@@ -1045,95 +1057,73 @@ final class SystemServiceRegistry {
@SuppressWarnings("unchecked")
public final T getService(ContextImpl ctx) {
final Object[] cache = ctx.mServiceCache;
+ final int[] gates = ctx.mServiceInitializationStateArray;
+
+ for (;;) {
+ boolean doInitialize = false;
+ synchronized (cache) {
+ // Return it if we already have a cached instance.
+ T service = (T) cache[mCacheIndex];
+ if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {
+ return service;
+ }
- // Fast path. If it's already cached, just return it.
- Object service = cache[mCacheIndex];
- if (service != null) {
- return (T) service;
- }
+ // If we get here, there's no cached instance.
- // Slow path.
- final AtomicInteger[] gates = ctx.mServiceInitializationStateArray;
- final AtomicInteger gate;
+ // Grr... if gate is STATE_READY, then this means we initialized the service
+ // once but someone cleared it.
+ // We start over from STATE_UNINITIALIZED.
+ if (gates[mCacheIndex] == ContextImpl.STATE_READY) {
+ gates[mCacheIndex] = ContextImpl.STATE_UNINITIALIZED;
+ }
- synchronized (cache) {
- // See if it's cached or not again, with the lock held this time.
- service = cache[mCacheIndex];
- if (service != null) {
- return (T) service;
- }
+ // It's possible for multiple threads to get here at the same time, so
+ // use the "gate" to make sure only the first thread will call createService().
- // Not initialized yet. Create an atomic boolean to control which thread should
- // instantiate the service.
- if (gates[mCacheIndex] != null) {
- gate = gates[mCacheIndex];
- } else {
- gate = new AtomicInteger(ContextImpl.STATE_UNINITIALIZED);
- gates[mCacheIndex] = gate;
+ // At this point, the gate must be either UNINITIALIZED or INITIALIZING.
+ if (gates[mCacheIndex] == ContextImpl.STATE_UNINITIALIZED) {
+ doInitialize = true;
+ gates[mCacheIndex] = ContextImpl.STATE_INITIALIZING;
+ }
}
- }
- // Not cached yet.
- //
- // Note multiple threads can reach here for the same service on the same context
- // concurrently.
- //
- // Now we're going to instantiate the service, but do so without the cache held;
- // otherwise it could deadlock. (b/71882178)
- //
- // However we still don't want to instantiate the same service multiple times, so
- // use the atomic integer to ensure only one thread will call createService().
-
- if (gate.compareAndSet(
- ContextImpl.STATE_UNINITIALIZED, ContextImpl.STATE_INITIALIZING)) {
- try {
- // This thread is the first one to get here. Instantiate the service
- // *without* the cache lock held.
+ if (doInitialize) {
+ // Only the first thread gets here.
+
+ T service = null;
+ @ServiceInitializationState int newState = ContextImpl.STATE_NOT_FOUND;
try {
+ // This thread is the first one to get here. Instantiate the service
+ // *without* the cache lock held.
service = createService(ctx);
+ newState = ContextImpl.STATE_READY;
+
+ } catch (ServiceNotFoundException e) {
+ onServiceNotFound(e);
+ } finally {
synchronized (cache) {
cache[mCacheIndex] = service;
+ gates[mCacheIndex] = newState;
+ cache.notifyAll();
}
- } catch (ServiceNotFoundException e) {
- onServiceNotFound(e);
- }
- } finally {
- // Tell the all other threads that the cache is ready now.
- // (But it's still be null in case of ServiceNotFoundException.)
- synchronized (gate) {
- gate.set(ContextImpl.STATE_READY);
- gate.notifyAll();
}
+ return service;
}
- return (T) service;
- }
- // Other threads will wait on the gate lock.
- synchronized (gate) {
- boolean interrupted = false;
-
- // Note: We check whether "state == STATE_READY", not
- // "cache[mCacheIndex] != null", because "cache[mCacheIndex] == null"
- // is still a valid outcome in the ServiceNotFoundException case.
- while (gate.get() != ContextImpl.STATE_READY) {
- try {
- gate.wait();
- } catch (InterruptedException e) {
- Log.w(TAG, "getService() interrupted");
- interrupted = true;
+ // The other threads will wait for the first thread to call notifyAll(),
+ // and go back to the top and retry.
+ synchronized (cache) {
+ while (gates[mCacheIndex] < ContextImpl.STATE_READY) {
+ try {
+ cache.wait();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "getService() interrupted");
+ Thread.currentThread().interrupt();
+ return null;
+ }
}
}
- if (interrupted) {
- Thread.currentThread().interrupt();
- }
- }
- // Now the first thread has initialized it.
- // It may still be null if ServiceNotFoundException was thrown, but that shouldn't
- // happen, so we'll just return null here in that case.
- synchronized (cache) {
- service = cache[mCacheIndex];
}
- return (T) service;
}
public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java
index bd4933a2..c03340e8 100644
--- a/android/app/UiAutomation.java
+++ b/android/app/UiAutomation.java
@@ -580,6 +580,8 @@ public final class UiAutomation {
// Execute the command *without* the lock being held.
command.run();
+ List<AccessibilityEvent> receivedEvents = new ArrayList<>();
+
// Acquire the lock and wait for the event.
try {
// Wait for the event.
@@ -600,14 +602,14 @@ public final class UiAutomation {
if (filter.accept(event)) {
return event;
}
- event.recycle();
+ receivedEvents.add(event);
}
// Check if timed out and if not wait.
final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
if (remainingTimeMillis <= 0) {
throw new TimeoutException("Expected event not received within: "
- + timeoutMillis + " ms.");
+ + timeoutMillis + " ms among: " + receivedEvents);
}
synchronized (mLock) {
if (mEventQueue.isEmpty()) {
@@ -620,6 +622,11 @@ public final class UiAutomation {
}
}
} finally {
+ int size = receivedEvents.size();
+ for (int i = 0; i < size; i++) {
+ receivedEvents.get(i).recycle();
+ }
+
synchronized (mLock) {
mWaitingForEventDelivery = false;
mEventQueue.clear();
diff --git a/android/app/WallpaperManager.java b/android/app/WallpaperManager.java
index 465340f6..6c2fb2df 100644
--- a/android/app/WallpaperManager.java
+++ b/android/app/WallpaperManager.java
@@ -401,7 +401,8 @@ public class WallpaperManager {
}
}
synchronized (this) {
- if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) {
+ if (mCachedWallpaper != null && mCachedWallpaperUserId == userId
+ && !mCachedWallpaper.isRecycled()) {
return mCachedWallpaper;
}
mCachedWallpaper = null;
@@ -412,7 +413,7 @@ public class WallpaperManager {
} catch (OutOfMemoryError e) {
Log.w(TAG, "Out of memory loading the current wallpaper: " + e);
} catch (SecurityException e) {
- if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {
+ if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) {
Log.w(TAG, "No permission to access wallpaper, suppressing"
+ " exception to avoid crashing legacy app.");
} else {
@@ -976,7 +977,7 @@ public class WallpaperManager {
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} catch (SecurityException e) {
- if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {
+ if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) {
Log.w(TAG, "No permission to access wallpaper, suppressing"
+ " exception to avoid crashing legacy app.");
return null;
diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java
index b64aae52..c491dccb 100644
--- a/android/app/admin/DevicePolicyManager.java
+++ b/android/app/admin/DevicePolicyManager.java
@@ -1169,10 +1169,18 @@ public class DevicePolicyManager {
* Constant to indicate the feature of mandatory backups. Used as argument to
* {@link #createAdminSupportIntent(String)}.
* @see #setMandatoryBackupTransport(ComponentName, ComponentName)
+ * @hide
*/
public static final String POLICY_MANDATORY_BACKUPS = "policy_mandatory_backups";
/**
+ * Constant to indicate the feature of suspending app. Use it as the value of
+ * {@link #EXTRA_RESTRICTION}.
+ * @hide
+ */
+ public static final String POLICY_SUSPEND_PACKAGES = "policy_suspend_packages";
+
+ /**
* A String indicating a specific restricted feature. Can be a user restriction from the
* {@link UserManager}, e.g. {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the values
* {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
@@ -4211,6 +4219,15 @@ public class DevicePolicyManager {
return null;
}
+ /**
+ * Returns {@code true} if the device supports attestation of device identifiers in addition
+ * to key attestation.
+ * @return {@code true} if Device ID attestation is supported.
+ */
+ public boolean isDeviceIdAttestationSupported() {
+ PackageManager pm = mContext.getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ID_ATTESTATION);
+ }
/**
* Called by a device or profile owner, or delegated certificate installer, to associate
@@ -6182,6 +6199,7 @@ public class DevicePolicyManager {
* @hide
*/
@SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
public @Nullable List<String> getPermittedAccessibilityServices(int userId) {
throwIfParentInstance("getPermittedAccessibilityServices");
if (mService != null) {
@@ -6826,8 +6844,7 @@ public class DevicePolicyManager {
* @param restriction Indicates for which feature the dialog should be displayed. Can be a
* user restriction from {@link UserManager}, e.g.
* {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the constants
- * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
- * {@link #POLICY_MANDATORY_BACKUPS}.
+ * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
* @return Intent An intent to be used to start the dialog-activity if the restriction is
* set by an admin, or null if the restriction does not exist or no admin set it.
*/
@@ -8774,13 +8791,6 @@ public class DevicePolicyManager {
*
* <p> Backup service is off by default when device owner is present.
*
- * <p> If backups are made mandatory by specifying a non-null mandatory backup transport using
- * the {@link DevicePolicyManager#setMandatoryBackupTransport} method, the backup service is
- * automatically enabled.
- *
- * <p> If the backup service is disabled using this method after the mandatory backup transport
- * has been set, the mandatory backup transport is cleared.
- *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param enabled {@code true} to enable the backup service, {@code false} to disable it.
* @throws SecurityException if {@code admin} is not a device owner.
@@ -8818,6 +8828,8 @@ public class DevicePolicyManager {
* <p>Only device owner can call this method.
* <p>If backups were disabled and a non-null backup transport {@link ComponentName} is
* specified, backups will be enabled.
+ * <p> If the backup service is disabled after the mandatory backup transport has been set, the
+ * mandatory backup transport is cleared.
*
* <p>NOTE: The method shouldn't be called on the main thread.
*
@@ -8825,6 +8837,7 @@ public class DevicePolicyManager {
* @param backupTransportComponent The backup transport layer to be used for mandatory backups.
* @return {@code true} if the backup transport was successfully set; {@code false} otherwise.
* @throws SecurityException if {@code admin} is not a device owner.
+ * @hide
*/
@WorkerThread
public boolean setMandatoryBackupTransport(
@@ -8844,6 +8857,7 @@ public class DevicePolicyManager {
*
* @return a {@link ComponentName} of the backup transport layer to be used if backups are
* mandatory or {@code null} if backups are not mandatory.
+ * @hide
*/
public ComponentName getMandatoryBackupTransport() {
throwIfParentInstance("getMandatoryBackupTransport");
diff --git a/android/app/admin/FreezeInterval.java b/android/app/admin/FreezePeriod.java
index de5e21ac..657f0177 100644
--- a/android/app/admin/FreezeInterval.java
+++ b/android/app/admin/FreezePeriod.java
@@ -20,49 +20,88 @@ import android.util.Log;
import android.util.Pair;
import java.time.LocalDate;
+import java.time.MonthDay;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
- * An interval representing one freeze period which repeats annually. We use the number of days
- * since the start of (non-leap) year to define the start and end dates of an interval, both
- * inclusive. If the end date is smaller than the start date, the interval is considered wrapped
- * around the year-end. As far as an interval is concerned, February 29th should be treated as
- * if it were February 28th: so an interval starting or ending on February 28th are not
- * distinguishable from an interval on February 29th. When calulating interval length or
- * distance between two dates, February 29th is also disregarded.
+ * A class that represents one freeze period which repeats <em>annually</em>. A freeze period has
+ * two {@link java.time#MonthDay} values that define the start and end dates of the period, both
+ * inclusive. If the end date is earlier than the start date, the period is considered wrapped
+ * around the year-end. As far as freeze period is concerned, leap year is disregarded and February
+ * 29th should be treated as if it were February 28th: so a freeze starting or ending on February
+ * 28th is identical to a freeze starting or ending on February 29th. When calulating the length of
+ * a freeze or the distance bewteen two freee periods, February 29th is also ignored.
*
* @see SystemUpdatePolicy#setFreezePeriods
- * @hide
*/
-public class FreezeInterval {
- private static final String TAG = "FreezeInterval";
+public class FreezePeriod {
+ private static final String TAG = "FreezePeriod";
private static final int DUMMY_YEAR = 2001;
static final int DAYS_IN_YEAR = 365; // 365 since DUMMY_YEAR is not a leap year
- final int mStartDay; // [1,365]
- final int mEndDay; // [1,365]
+ private final MonthDay mStart;
+ private final MonthDay mEnd;
- FreezeInterval(int startDay, int endDay) {
- if (startDay < 1 || startDay > 365 || endDay < 1 || endDay > 365) {
- throw new RuntimeException("Bad dates for Interval: " + startDay + "," + endDay);
- }
+ /*
+ * Start and end dates represented by number of days since the beginning of the year.
+ * They are internal representations of mStart and mEnd with normalized Leap year days
+ * (Feb 29 == Feb 28 == 59th day of year). All internal calclations are based on
+ * these two values so that leap year days are disregarded.
+ */
+ private final int mStartDay; // [1, 365]
+ private final int mEndDay; // [1, 365]
+
+ /**
+ * Creates a freeze period by its start and end dates. If the end date is earlier than the start
+ * date, the freeze period is considered wrapping year-end.
+ */
+ public FreezePeriod(MonthDay start, MonthDay end) {
+ mStart = start;
+ mStartDay = mStart.atYear(DUMMY_YEAR).getDayOfYear();
+ mEnd = end;
+ mEndDay = mEnd.atYear(DUMMY_YEAR).getDayOfYear();
+ }
+
+ /**
+ * Returns the start date (inclusive) of this freeze period.
+ */
+ public MonthDay getStart() {
+ return mStart;
+ }
+
+ /**
+ * Returns the end date (inclusive) of this freeze period.
+ */
+ public MonthDay getEnd() {
+ return mEnd;
+ }
+
+ /**
+ * @hide
+ */
+ private FreezePeriod(int startDay, int endDay) {
mStartDay = startDay;
+ mStart = dayOfYearToMonthDay(startDay);
mEndDay = endDay;
+ mEnd = dayOfYearToMonthDay(endDay);
}
+ /** @hide */
int getLength() {
return getEffectiveEndDay() - mStartDay + 1;
}
+ /** @hide */
boolean isWrapped() {
return mEndDay < mStartDay;
}
/**
* Returns the effective end day, taking wrapping around year-end into consideration
+ * @hide
*/
int getEffectiveEndDay() {
if (!isWrapped()) {
@@ -72,6 +111,7 @@ public class FreezeInterval {
}
}
+ /** @hide */
boolean contains(LocalDate localDate) {
final int daysOfYear = dayOfYearDisregardLeapYear(localDate);
if (!isWrapped()) {
@@ -84,6 +124,7 @@ public class FreezeInterval {
}
}
+ /** @hide */
boolean after(LocalDate localDate) {
return mStartDay > dayOfYearDisregardLeapYear(localDate);
}
@@ -95,6 +136,7 @@ public class FreezeInterval {
* include now, the returned dates represents the next future interval.
* The result will always have the same month and dayOfMonth value as the non-instantiated
* interval itself.
+ * @hide
*/
Pair<LocalDate, LocalDate> toCurrentOrFutureRealDates(LocalDate now) {
final int nowDays = dayOfYearDisregardLeapYear(now);
@@ -138,14 +180,24 @@ public class FreezeInterval {
+ LocalDate.ofYearDay(DUMMY_YEAR, mEndDay).format(formatter);
}
- // Treat the supplied date as in a non-leap year and return its day of year.
- static int dayOfYearDisregardLeapYear(LocalDate date) {
+ /** @hide */
+ private static MonthDay dayOfYearToMonthDay(int dayOfYear) {
+ LocalDate date = LocalDate.ofYearDay(DUMMY_YEAR, dayOfYear);
+ return MonthDay.of(date.getMonth(), date.getDayOfMonth());
+ }
+
+ /**
+ * Treat the supplied date as in a non-leap year and return its day of year.
+ * @hide
+ */
+ private static int dayOfYearDisregardLeapYear(LocalDate date) {
return date.withYear(DUMMY_YEAR).getDayOfYear();
}
/**
* Compute the number of days between first (inclusive) and second (exclusive),
* treating all years in between as non-leap.
+ * @hide
*/
public static int distanceWithoutLeapYear(LocalDate first, LocalDate second) {
return dayOfYearDisregardLeapYear(first) - dayOfYearDisregardLeapYear(second)
@@ -165,16 +217,16 @@ public class FreezeInterval {
* 3. At most one wrapped Interval remains, and it will be at the end of the list
* @hide
*/
- protected static List<FreezeInterval> canonicalizeIntervals(List<FreezeInterval> intervals) {
+ static List<FreezePeriod> canonicalizePeriods(List<FreezePeriod> intervals) {
boolean[] taken = new boolean[DAYS_IN_YEAR];
// First convert the intervals into flat array
- for (FreezeInterval interval : intervals) {
+ for (FreezePeriod interval : intervals) {
for (int i = interval.mStartDay; i <= interval.getEffectiveEndDay(); i++) {
taken[(i - 1) % DAYS_IN_YEAR] = true;
}
}
// Then reconstruct intervals from the array
- List<FreezeInterval> result = new ArrayList<>();
+ List<FreezePeriod> result = new ArrayList<>();
int i = 0;
while (i < DAYS_IN_YEAR) {
if (!taken[i]) {
@@ -183,14 +235,14 @@ public class FreezeInterval {
}
final int intervalStart = i + 1;
while (i < DAYS_IN_YEAR && taken[i]) i++;
- result.add(new FreezeInterval(intervalStart, i));
+ result.add(new FreezePeriod(intervalStart, i));
}
// Check if the last entry can be merged to the first entry to become one single
// wrapped interval
final int lastIndex = result.size() - 1;
if (lastIndex > 0 && result.get(lastIndex).mEndDay == DAYS_IN_YEAR
&& result.get(0).mStartDay == 1) {
- FreezeInterval wrappedInterval = new FreezeInterval(result.get(lastIndex).mStartDay,
+ FreezePeriod wrappedInterval = new FreezePeriod(result.get(lastIndex).mStartDay,
result.get(0).mEndDay);
result.set(lastIndex, wrappedInterval);
result.remove(0);
@@ -207,18 +259,18 @@ public class FreezeInterval {
*
* @hide
*/
- protected static void validatePeriods(List<FreezeInterval> periods) {
- List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods);
+ static void validatePeriods(List<FreezePeriod> periods) {
+ List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods);
if (allPeriods.size() != periods.size()) {
throw SystemUpdatePolicy.ValidationFailedException.duplicateOrOverlapPeriods();
}
for (int i = 0; i < allPeriods.size(); i++) {
- FreezeInterval current = allPeriods.get(i);
+ FreezePeriod current = allPeriods.get(i);
if (current.getLength() > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
throw SystemUpdatePolicy.ValidationFailedException.freezePeriodTooLong("Freeze "
+ "period " + current + " is too long: " + current.getLength() + " days");
}
- FreezeInterval previous = i > 0 ? allPeriods.get(i - 1)
+ FreezePeriod previous = i > 0 ? allPeriods.get(i - 1)
: allPeriods.get(allPeriods.size() - 1);
if (previous != current) {
final int separation;
@@ -247,7 +299,7 @@ public class FreezeInterval {
*
* @hide
*/
- protected static void validateAgainstPreviousFreezePeriod(List<FreezeInterval> periods,
+ static void validateAgainstPreviousFreezePeriod(List<FreezePeriod> periods,
LocalDate prevPeriodStart, LocalDate prevPeriodEnd, LocalDate now) {
if (periods.size() == 0 || prevPeriodStart == null || prevPeriodEnd == null) {
return;
@@ -258,14 +310,14 @@ public class FreezeInterval {
// Clock was adjusted backwards. We can continue execution though, the separation
// and length validation below still works under this condition.
}
- List<FreezeInterval> allPeriods = FreezeInterval.canonicalizeIntervals(periods);
+ List<FreezePeriod> allPeriods = FreezePeriod.canonicalizePeriods(periods);
// Given current time now, find the freeze period that's either current, or the one
// that's immediately afterwards. For the later case, it might be after the year-end,
// but this can only happen if there is only one freeze period.
- FreezeInterval curOrNextFreezePeriod = allPeriods.get(0);
- for (FreezeInterval interval : allPeriods) {
+ FreezePeriod curOrNextFreezePeriod = allPeriods.get(0);
+ for (FreezePeriod interval : allPeriods) {
if (interval.contains(now)
- || interval.mStartDay > FreezeInterval.dayOfYearDisregardLeapYear(now)) {
+ || interval.mStartDay > FreezePeriod.dayOfYearDisregardLeapYear(now)) {
curOrNextFreezePeriod = interval;
break;
}
@@ -282,7 +334,7 @@ public class FreezeInterval {
// Now validate [prevPeriodStart, prevPeriodEnd] against curOrNextFreezeDates
final String periodsDescription = "Prev: " + prevPeriodStart + "," + prevPeriodEnd
+ "; cur: " + curOrNextFreezeDates.first + "," + curOrNextFreezeDates.second;
- long separation = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.first,
+ long separation = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.first,
prevPeriodEnd) - 1;
if (separation > 0) {
// Two intervals do not overlap, check separation
@@ -292,7 +344,7 @@ public class FreezeInterval {
}
} else {
// Two intervals overlap, check combined length
- long length = FreezeInterval.distanceWithoutLeapYear(curOrNextFreezeDates.second,
+ long length = FreezePeriod.distanceWithoutLeapYear(curOrNextFreezeDates.second,
prevPeriodStart) + 1;
if (length > SystemUpdatePolicy.FREEZE_PERIOD_MAX_LENGTH) {
throw ValidationFailedException.combinedPeriodTooLong("Combined freeze period "
diff --git a/android/app/admin/SystemUpdatePolicy.java b/android/app/admin/SystemUpdatePolicy.java
index 47b3a81d..20eef6cc 100644
--- a/android/app/admin/SystemUpdatePolicy.java
+++ b/android/app/admin/SystemUpdatePolicy.java
@@ -38,9 +38,11 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
+import java.time.MonthDay;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -51,7 +53,7 @@ import java.util.stream.Collectors;
* @see DevicePolicyManager#setSystemUpdatePolicy
* @see DevicePolicyManager#getSystemUpdatePolicy
*/
-public class SystemUpdatePolicy implements Parcelable {
+public final class SystemUpdatePolicy implements Parcelable {
private static final String TAG = "SystemUpdatePolicy";
/** @hide */
@@ -163,6 +165,7 @@ public class SystemUpdatePolicy implements Parcelable {
ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE,
ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG,
ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE,
+ ERROR_UNKNOWN,
})
@Retention(RetentionPolicy.SOURCE)
@interface ValidationFailureType {}
@@ -171,33 +174,38 @@ public class SystemUpdatePolicy implements Parcelable {
public static final int ERROR_NONE = 0;
/**
+ * Validation failed with unknown error.
+ */
+ public static final int ERROR_UNKNOWN = 1;
+
+ /**
* The freeze periods contains duplicates, periods that overlap with each
* other or periods whose start and end joins.
*/
- public static final int ERROR_DUPLICATE_OR_OVERLAP = 1;
+ public static final int ERROR_DUPLICATE_OR_OVERLAP = 2;
/**
* There exists at least one freeze period whose length exceeds 90 days.
*/
- public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 2;
+ public static final int ERROR_NEW_FREEZE_PERIOD_TOO_LONG = 3;
/**
* There exists some freeze period which starts within 60 days of the preceding period's
* end time.
*/
- public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 3;
+ public static final int ERROR_NEW_FREEZE_PERIOD_TOO_CLOSE = 4;
/**
* The device has been in a freeze period and when combining with the new freeze period
* to be set, it will result in the total freeze period being longer than 90 days.
*/
- public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 4;
+ public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_LONG = 5;
/**
* The device has been in a freeze period and some new freeze period to be set is less
* than 60 days from the end of the last freeze period the device went through.
*/
- public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 5;
+ public static final int ERROR_COMBINED_FREEZE_PERIOD_TOO_CLOSE = 6;
@ValidationFailureType
private final int mErrorCode;
@@ -272,7 +280,7 @@ public class SystemUpdatePolicy implements Parcelable {
private int mMaintenanceWindowStart;
private int mMaintenanceWindowEnd;
- private final ArrayList<FreezeInterval> mFreezePeriods;
+ private final ArrayList<FreezePeriod> mFreezePeriods;
private SystemUpdatePolicy() {
mPolicyType = TYPE_UNKNOWN;
@@ -444,12 +452,10 @@ public class SystemUpdatePolicy implements Parcelable {
* requirement set above
* @return this instance
*/
- public SystemUpdatePolicy setFreezePeriods(List<Pair<Integer, Integer>> freezePeriods) {
- List<FreezeInterval> newPeriods = freezePeriods.stream().map(
- p -> new FreezeInterval(p.first, p.second)).collect(Collectors.toList());
- FreezeInterval.validatePeriods(newPeriods);
+ public SystemUpdatePolicy setFreezePeriods(List<FreezePeriod> freezePeriods) {
+ FreezePeriod.validatePeriods(freezePeriods);
mFreezePeriods.clear();
- mFreezePeriods.addAll(newPeriods);
+ mFreezePeriods.addAll(freezePeriods);
return this;
}
@@ -458,12 +464,8 @@ public class SystemUpdatePolicy implements Parcelable {
*
* @return the list of freeze periods, or an empty list if none was set.
*/
- public List<Pair<Integer, Integer>> getFreezePeriods() {
- List<Pair<Integer, Integer>> result = new ArrayList<>(mFreezePeriods.size());
- for (FreezeInterval interval : mFreezePeriods) {
- result.add(new Pair<>(interval.mStartDay, interval.mEndDay));
- }
- return result;
+ public List<FreezePeriod> getFreezePeriods() {
+ return Collections.unmodifiableList(mFreezePeriods);
}
/**
@@ -472,7 +474,7 @@ public class SystemUpdatePolicy implements Parcelable {
* @hide
*/
public Pair<LocalDate, LocalDate> getCurrentFreezePeriod(LocalDate now) {
- for (FreezeInterval interval : mFreezePeriods) {
+ for (FreezePeriod interval : mFreezePeriods) {
if (interval.contains(now)) {
return interval.toCurrentOrFutureRealDates(now);
}
@@ -485,10 +487,10 @@ public class SystemUpdatePolicy implements Parcelable {
* is not within a freeze period.
*/
private long timeUntilNextFreezePeriod(long now) {
- List<FreezeInterval> sortedPeriods = FreezeInterval.canonicalizeIntervals(mFreezePeriods);
+ List<FreezePeriod> sortedPeriods = FreezePeriod.canonicalizePeriods(mFreezePeriods);
LocalDate nowDate = millisToDate(now);
LocalDate nextFreezeStart = null;
- for (FreezeInterval interval : sortedPeriods) {
+ for (FreezePeriod interval : sortedPeriods) {
if (interval.after(nowDate)) {
nextFreezeStart = interval.toCurrentOrFutureRealDates(nowDate).first;
break;
@@ -506,13 +508,13 @@ public class SystemUpdatePolicy implements Parcelable {
/** @hide */
public void validateFreezePeriods() {
- FreezeInterval.validatePeriods(mFreezePeriods);
+ FreezePeriod.validatePeriods(mFreezePeriods);
}
/** @hide */
public void validateAgainstPreviousFreezePeriod(LocalDate prevPeriodStart,
LocalDate prevPeriodEnd, LocalDate now) {
- FreezeInterval.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart,
+ FreezePeriod.validateAgainstPreviousFreezePeriod(mFreezePeriods, prevPeriodStart,
prevPeriodEnd, now);
}
@@ -521,10 +523,10 @@ public class SystemUpdatePolicy implements Parcelable {
* updates and how long this action is valid for, given the current system update policy. Its
* action could be one of the following
* <ul>
- * <li> {@code TYPE_INSTALL_AUTOMATIC} system updates should be installed immedately and without
- * user intervention as soon as they become available.
- * <li> {@code TYPE_POSTPONE} system updates should be postponed for a maximum of 30 days
- * <li> {@code TYPE_PAUSE} system updates should be postponed indefinitely until further notice
+ * <li> {@link #TYPE_INSTALL_AUTOMATIC} system updates should be installed immedately and
+ * without user intervention as soon as they become available.
+ * <li> {@link #TYPE_POSTPONE} system updates should be postponed for a maximum of 30 days
+ * <li> {@link #TYPE_PAUSE} system updates should be postponed indefinitely until further notice
* </ul>
*
* The effective time measures how long this installation option is valid for from the queried
@@ -535,18 +537,38 @@ public class SystemUpdatePolicy implements Parcelable {
*/
@SystemApi
public static class InstallationOption {
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_INSTALL_AUTOMATIC,
+ TYPE_PAUSE,
+ TYPE_POSTPONE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface InstallationOptionType {}
+
+ @InstallationOptionType
private final int mType;
private long mEffectiveTime;
- InstallationOption(int type, long effectiveTime) {
+ InstallationOption(@InstallationOptionType int type, long effectiveTime) {
this.mType = type;
this.mEffectiveTime = effectiveTime;
}
- public int getType() {
+ /**
+ * Returns the type of the current installation option, could be one of
+ * {@link #TYPE_INSTALL_AUTOMATIC}, {@link #TYPE_POSTPONE} and {@link #TYPE_PAUSE}.
+ * @return type of installation option.
+ */
+ public @InstallationOptionType int getType() {
return mType;
}
+ /**
+ * Returns how long the current installation option in effective for, starting from the time
+ * of query.
+ * @return the effective time in milliseconds.
+ */
public long getEffectiveTime() {
return mEffectiveTime;
}
@@ -667,9 +689,11 @@ public class SystemUpdatePolicy implements Parcelable {
int freezeCount = mFreezePeriods.size();
dest.writeInt(freezeCount);
for (int i = 0; i < freezeCount; i++) {
- FreezeInterval interval = mFreezePeriods.get(i);
- dest.writeInt(interval.mStartDay);
- dest.writeInt(interval.mEndDay);
+ FreezePeriod interval = mFreezePeriods.get(i);
+ dest.writeInt(interval.getStart().getMonthValue());
+ dest.writeInt(interval.getStart().getDayOfMonth());
+ dest.writeInt(interval.getEnd().getMonthValue());
+ dest.writeInt(interval.getEnd().getDayOfMonth());
}
}
@@ -686,8 +710,9 @@ public class SystemUpdatePolicy implements Parcelable {
int freezeCount = source.readInt();
policy.mFreezePeriods.ensureCapacity(freezeCount);
for (int i = 0; i < freezeCount; i++) {
- policy.mFreezePeriods.add(
- new FreezeInterval(source.readInt(), source.readInt()));
+ MonthDay start = MonthDay.of(source.readInt(), source.readInt());
+ MonthDay end = MonthDay.of(source.readInt(), source.readInt());
+ policy.mFreezePeriods.add(new FreezePeriod(start, end));
}
return policy;
}
@@ -730,9 +755,9 @@ public class SystemUpdatePolicy implements Parcelable {
if (!parser.getName().equals(KEY_FREEZE_TAG)) {
continue;
}
- policy.mFreezePeriods.add(new FreezeInterval(
- Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_START)),
- Integer.parseInt(parser.getAttributeValue(null, KEY_FREEZE_END))));
+ policy.mFreezePeriods.add(new FreezePeriod(
+ MonthDay.parse(parser.getAttributeValue(null, KEY_FREEZE_START)),
+ MonthDay.parse(parser.getAttributeValue(null, KEY_FREEZE_END))));
}
return policy;
}
@@ -751,10 +776,10 @@ public class SystemUpdatePolicy implements Parcelable {
out.attribute(null, KEY_INSTALL_WINDOW_START, Integer.toString(mMaintenanceWindowStart));
out.attribute(null, KEY_INSTALL_WINDOW_END, Integer.toString(mMaintenanceWindowEnd));
for (int i = 0; i < mFreezePeriods.size(); i++) {
- FreezeInterval interval = mFreezePeriods.get(i);
+ FreezePeriod interval = mFreezePeriods.get(i);
out.startTag(null, KEY_FREEZE_TAG);
- out.attribute(null, KEY_FREEZE_START, Integer.toString(interval.mStartDay));
- out.attribute(null, KEY_FREEZE_END, Integer.toString(interval.mEndDay));
+ out.attribute(null, KEY_FREEZE_START, interval.getStart().toString());
+ out.attribute(null, KEY_FREEZE_END, interval.getEnd().toString());
out.endTag(null, KEY_FREEZE_TAG);
}
}
diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java
index bf3398ad..4336f184 100644
--- a/android/app/slice/Slice.java
+++ b/android/app/slice/Slice.java
@@ -21,19 +21,13 @@ import android.annotation.Nullable;
import android.annotation.StringDef;
import android.app.PendingIntent;
import android.app.RemoteInput;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.IContentProvider;
-import android.content.Intent;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.RemoteException;
import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -575,45 +569,4 @@ public final class Slice implements Parcelable {
}
return sb.toString();
}
-
- /**
- * @deprecated TO BE REMOVED.
- */
- @Deprecated
- public static @Nullable Slice bindSlice(ContentResolver resolver,
- @NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
- Preconditions.checkNotNull(uri, "uri");
- IContentProvider provider = resolver.acquireProvider(uri);
- if (provider == null) {
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
- try {
- Bundle extras = new Bundle();
- extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
- extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
- new ArrayList<>(supportedSpecs));
- final Bundle res = provider.call(resolver.getPackageName(), SliceProvider.METHOD_SLICE,
- null, extras);
- Bundle.setDefusable(res, true);
- if (res == null) {
- return null;
- }
- return res.getParcelable(SliceProvider.EXTRA_SLICE);
- } catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- return null;
- } finally {
- resolver.releaseProvider(provider);
- }
- }
-
- /**
- * @deprecated TO BE REMOVED.
- */
- @Deprecated
- public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent,
- @NonNull List<SliceSpec> supportedSpecs) {
- return context.getSystemService(SliceManager.class).bindSlice(intent, supportedSpecs);
- }
}
diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java
index 0285e9f9..ad49437f 100644
--- a/android/app/slice/SliceManager.java
+++ b/android/app/slice/SliceManager.java
@@ -16,6 +16,8 @@
package android.app.slice;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
@@ -38,6 +40,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.os.UserHandle;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -47,6 +50,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/**
* Class to handle interactions with {@link Slice}s.
@@ -101,22 +105,6 @@ public class SliceManager {
private final IBinder mToken = new Binder();
/**
- * Permission denied.
- * @hide
- */
- public static final int PERMISSION_DENIED = -1;
- /**
- * Permission granted.
- * @hide
- */
- public static final int PERMISSION_GRANTED = 0;
- /**
- * Permission just granted by the user, and should be granted uri permission as well.
- * @hide
- */
- public static final int PERMISSION_USER_GRANTED = 1;
-
- /**
* @hide
*/
public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
@@ -140,7 +128,7 @@ public class SliceManager {
* @see Intent#ACTION_ASSIST
* @see Intent#CATEGORY_HOME
*/
- public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
+ public void pinSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> specs) {
try {
mService.pinSlice(mContext.getPackageName(), uri,
specs.toArray(new SliceSpec[specs.size()]), mToken);
@@ -150,6 +138,14 @@ public class SliceManager {
}
/**
+ * @deprecated TO BE REMOVED
+ */
+ @Deprecated
+ public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
+ pinSlice(uri, new ArraySet<>(specs));
+ }
+
+ /**
* Remove a pin for a slice.
* <p>
* If the slice has no other pins/callbacks then the slice will be unpinned.
@@ -189,9 +185,10 @@ public class SliceManager {
* into account all clients and returns only specs supported by all.
* @see SliceSpec
*/
- public @NonNull List<SliceSpec> getPinnedSpecs(Uri uri) {
+ public @NonNull Set<SliceSpec> getPinnedSpecs(Uri uri) {
try {
- return Arrays.asList(mService.getPinnedSpecs(uri, mContext.getPackageName()));
+ return new ArraySet<>(Arrays.asList(mService.getPinnedSpecs(uri,
+ mContext.getPackageName())));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -240,7 +237,7 @@ public class SliceManager {
* @return The Slice provided by the app or null if none is given.
* @see Slice
*/
- public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
+ public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull Set<SliceSpec> supportedSpecs) {
Preconditions.checkNotNull(uri, "uri");
ContentResolver resolver = mContext.getContentResolver();
try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
@@ -265,6 +262,14 @@ public class SliceManager {
}
/**
+ * @deprecated TO BE REMOVED
+ */
+ @Deprecated
+ public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
+ return bindSlice(uri, new ArraySet<>(supportedSpecs));
+ }
+
+ /**
* Turns a slice intent into a slice uri. Expects an explicit intent.
* <p>
* This goes through a several stage resolution process to determine if any slice
@@ -272,12 +277,12 @@ public class SliceManager {
* <ol>
* <li> If the intent contains data that {@link ContentResolver#getType} is
* {@link SliceProvider#SLICE_TYPE} then the data will be returned.</li>
- * <li>If the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
- * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
- * will be returned.</li>
- * <li>Lastly, if the intent explicitly points at an activity, and that activity has
+ * <li>If the intent explicitly points at an activity, and that activity has
* meta-data for key {@link #SLICE_METADATA_KEY}, then the Uri specified there will be
* returned.</li>
+ * <li>Lastly, if the intent with {@link #CATEGORY_SLICE} added resolves to a provider, then
+ * the provider will be asked to {@link SliceProvider#onMapIntentToUri} and that result
+ * will be returned.</li>
* <li>If no slice is found, then {@code null} is returned.</li>
* </ol>
* @param intent The intent associated with a slice.
@@ -287,37 +292,12 @@ public class SliceManager {
* @see Intent
*/
public @Nullable Uri mapIntentToUri(@NonNull Intent intent) {
- Preconditions.checkNotNull(intent, "intent");
- Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
- || intent.getData() != null,
- "Slice intent must be explicit %s", intent);
ContentResolver resolver = mContext.getContentResolver();
-
- // Check if the intent has data for the slice uri on it and use that
- final Uri intentData = intent.getData();
- if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
- return intentData;
- }
+ final Uri staticUri = resolveStatic(intent, resolver);
+ if (staticUri != null) return staticUri;
// Otherwise ask the app
- Intent queryIntent = new Intent(intent);
- if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
- queryIntent.addCategory(CATEGORY_SLICE);
- }
- List<ResolveInfo> providers =
- mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
- if (providers == null || providers.isEmpty()) {
- // There are no providers, see if this activity has a direct link.
- ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
- PackageManager.GET_META_DATA);
- if (resolve != null && resolve.activityInfo != null
- && resolve.activityInfo.metaData != null
- && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
- return Uri.parse(
- resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
- }
- return null;
- }
- String authority = providers.get(0).providerInfo.authority;
+ String authority = getAuthority(intent);
+ if (authority == null) return null;
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).build();
try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
@@ -338,10 +318,43 @@ public class SliceManager {
}
}
+ private String getAuthority(Intent intent) {
+ Intent queryIntent = new Intent(intent);
+ if (!queryIntent.hasCategory(CATEGORY_SLICE)) {
+ queryIntent.addCategory(CATEGORY_SLICE);
+ }
+ List<ResolveInfo> providers =
+ mContext.getPackageManager().queryIntentContentProviders(queryIntent, 0);
+ return providers != null && !providers.isEmpty() ? providers.get(0).providerInfo.authority
+ : null;
+ }
+
+ private Uri resolveStatic(@NonNull Intent intent, ContentResolver resolver) {
+ Preconditions.checkNotNull(intent, "intent");
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
+ || intent.getData() != null,
+ "Slice intent must be explicit %s", intent);
+
+ // Check if the intent has data for the slice uri on it and use that
+ final Uri intentData = intent.getData();
+ if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
+ return intentData;
+ }
+ // There are no providers, see if this activity has a direct link.
+ ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
+ PackageManager.GET_META_DATA);
+ if (resolve != null && resolve.activityInfo != null
+ && resolve.activityInfo.metaData != null
+ && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
+ return Uri.parse(
+ resolve.activityInfo.metaData.getString(SLICE_METADATA_KEY));
+ }
+ return null;
+ }
+
/**
- * Turns a slice intent into slice content. Expects an explicit intent. If there is no
- * {@link android.content.ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
+ * Turns a slice intent into slice content. Is a shortcut to perform the action
+ * of both {@link #mapIntentToUri(Intent)} and {@link #bindSlice(Uri, List)} at once.
*
* @param intent The intent associated with a slice.
* @param supportedSpecs List of supported specs.
@@ -351,34 +364,17 @@ public class SliceManager {
* @see Intent
*/
public @Nullable Slice bindSlice(@NonNull Intent intent,
- @NonNull List<SliceSpec> supportedSpecs) {
+ @NonNull Set<SliceSpec> supportedSpecs) {
Preconditions.checkNotNull(intent, "intent");
Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null
|| intent.getData() != null,
"Slice intent must be explicit %s", intent);
ContentResolver resolver = mContext.getContentResolver();
-
- // Check if the intent has data for the slice uri on it and use that
- final Uri intentData = intent.getData();
- if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
- return bindSlice(intentData, supportedSpecs);
- }
+ final Uri staticUri = resolveStatic(intent, resolver);
+ if (staticUri != null) return bindSlice(staticUri, supportedSpecs);
// Otherwise ask the app
- List<ResolveInfo> providers =
- mContext.getPackageManager().queryIntentContentProviders(intent, 0);
- if (providers == null || providers.isEmpty()) {
- // There are no providers, see if this activity has a direct link.
- ResolveInfo resolve = mContext.getPackageManager().resolveActivity(intent,
- PackageManager.GET_META_DATA);
- if (resolve != null && resolve.activityInfo != null
- && resolve.activityInfo.metaData != null
- && resolve.activityInfo.metaData.containsKey(SLICE_METADATA_KEY)) {
- return bindSlice(Uri.parse(resolve.activityInfo.metaData
- .getString(SLICE_METADATA_KEY)), supportedSpecs);
- }
- return null;
- }
- String authority = providers.get(0).providerInfo.authority;
+ String authority = getAuthority(intent);
+ if (authority == null) return null;
Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority).build();
try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
@@ -387,8 +383,6 @@ public class SliceManager {
}
Bundle extras = new Bundle();
extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
- extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
- new ArrayList<>(supportedSpecs));
final Bundle res = provider.call(SliceProvider.METHOD_MAP_INTENT, null, extras);
if (res == null) {
return null;
@@ -402,6 +396,16 @@ public class SliceManager {
}
/**
+ * @deprecated TO BE REMOVED.
+ */
+ @Deprecated
+ @Nullable
+ public Slice bindSlice(@NonNull Intent intent,
+ @NonNull List<SliceSpec> supportedSpecs) {
+ return bindSlice(intent, new ArraySet<>(supportedSpecs));
+ }
+
+ /**
* Determine whether a particular process and user ID has been granted
* permission to access a specific slice URI.
*
@@ -417,9 +421,11 @@ public class SliceManager {
* @see #grantSlicePermission(String, Uri)
*/
public @PermissionResult int checkSlicePermission(@NonNull Uri uri, int pid, int uid) {
- // TODO: Switch off Uri permissions.
- return mContext.checkUriPermission(uri, pid, uid,
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ try {
+ return mService.checkSlicePermission(uri, null, pid, uid, null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -431,11 +437,11 @@ public class SliceManager {
* @see #revokeSlicePermission
*/
public void grantSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- mContext.grantUriPermission(toPackage, uri,
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ try {
+ mService.grantSlicePermission(mContext.getPackageName(), toPackage, uri);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -453,11 +459,11 @@ public class SliceManager {
* @see #grantSlicePermission
*/
public void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri) {
- // TODO: Switch off Uri permissions.
- mContext.revokeUriPermission(toPackage, uri,
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ try {
+ mService.revokeSlicePermission(mContext.getPackageName(), toPackage, uri);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -478,16 +484,6 @@ public class SliceManager {
throw new SecurityException("User " + uid + " does not have slice permission for "
+ uri + ".");
}
- if (result == PERMISSION_USER_GRANTED) {
- // We just had a user grant of this permission and need to grant this to the app
- // permanently.
- mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
- Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
- // Notify a change has happened because we just granted a permission.
- mContext.getContentResolver().notifyChange(uri, null);
- }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java
index fe5742d6..d369272d 100644
--- a/android/app/slice/SliceProvider.java
+++ b/android/app/slice/SliceProvider.java
@@ -44,6 +44,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/**
* A SliceProvider allows an app to provide content to be displayed in system spaces. This content
@@ -197,6 +198,14 @@ public abstract class SliceProvider extends ContentProvider {
* @see {@link Slice}.
* @see {@link Slice#HINT_PARTIAL}
*/
+ public Slice onBindSlice(Uri sliceUri, Set<SliceSpec> supportedSpecs) {
+ return onBindSlice(sliceUri, new ArrayList<>(supportedSpecs));
+ }
+
+ /**
+ * @deprecated TO BE REMOVED
+ */
+ @Deprecated
public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
return null;
}
diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java
index 7252f028..216a4a09 100644
--- a/android/app/usage/NetworkStats.java
+++ b/android/app/usage/NetworkStats.java
@@ -237,20 +237,26 @@ public final class NetworkStats implements AutoCloseable {
DEFAULT_NETWORK_YES
})
@Retention(RetentionPolicy.SOURCE)
- public @interface DefaultNetwork {}
+ public @interface DefaultNetworkStatus {}
/**
- * Combined usage for this network regardless of whether it was the active default network.
+ * Combined usage for this network regardless of default network status.
*/
public static final int DEFAULT_NETWORK_ALL = -1;
/**
- * Usage that occurs while this network is not the active default network.
+ * Usage that occurs while this network is not a default network.
+ *
+ * <p>This implies that the app responsible for this usage requested that it occur on a
+ * specific network different from the one(s) the system would have selected for it.
*/
public static final int DEFAULT_NETWORK_NO = 0x1;
/**
- * Usage that occurs while this network is the active default network.
+ * Usage that occurs while this network is a default network.
+ *
+ * <p>This implies that the app either did not select a specific network for this usage,
+ * or it selected a network that the system could have selected for app traffic.
*/
public static final int DEFAULT_NETWORK_YES = 0x2;
@@ -262,7 +268,7 @@ public final class NetworkStats implements AutoCloseable {
private int mUid;
private int mTag;
private int mState;
- private int mDefaultNetwork;
+ private int mDefaultNetworkStatus;
private int mMetered;
private int mRoaming;
private long mBeginTimeStamp;
@@ -323,8 +329,9 @@ public final class NetworkStats implements AutoCloseable {
return 0;
}
- private static @DefaultNetwork int convertDefaultNetwork(int defaultNetwork) {
- switch (defaultNetwork) {
+ private static @DefaultNetworkStatus int convertDefaultNetworkStatus(
+ int defaultNetworkStatus) {
+ switch (defaultNetworkStatus) {
case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL;
case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO;
case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES;
@@ -397,18 +404,15 @@ public final class NetworkStats implements AutoCloseable {
}
/**
- * Default network state. One of the following values:<p/>
+ * Default network status. One of the following values:<p/>
* <ul>
* <li>{@link #DEFAULT_NETWORK_ALL}</li>
* <li>{@link #DEFAULT_NETWORK_NO}</li>
* <li>{@link #DEFAULT_NETWORK_YES}</li>
* </ul>
- * <p>Indicates whether the network usage occurred on the system default network for this
- * type of traffic, or whether the application chose to send this traffic on a network that
- * was not the one selected by the system.
*/
- public @DefaultNetwork int getDefaultNetwork() {
- return mDefaultNetwork;
+ public @DefaultNetworkStatus int getDefaultNetworkStatus() {
+ return mDefaultNetworkStatus;
}
/**
@@ -605,7 +609,7 @@ public final class NetworkStats implements AutoCloseable {
bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
- bucketOut.mDefaultNetwork = Bucket.convertDefaultNetwork(
+ bucketOut.mDefaultNetworkStatus = Bucket.convertDefaultNetworkStatus(
mRecycledSummaryEntry.defaultNetwork);
bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
@@ -657,7 +661,7 @@ public final class NetworkStats implements AutoCloseable {
bucketOut.mUid = Bucket.convertUid(getUid());
bucketOut.mTag = Bucket.convertTag(mTag);
bucketOut.mState = mState;
- bucketOut.mDefaultNetwork = Bucket.DEFAULT_NETWORK_ALL;
+ bucketOut.mDefaultNetworkStatus = Bucket.DEFAULT_NETWORK_ALL;
bucketOut.mMetered = Bucket.METERED_ALL;
bucketOut.mRoaming = Bucket.ROAMING_ALL;
bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
diff --git a/android/app/usage/NetworkStatsManager.java b/android/app/usage/NetworkStatsManager.java
index b2fe9586..0b21196f 100644
--- a/android/app/usage/NetworkStatsManager.java
+++ b/android/app/usage/NetworkStatsManager.java
@@ -35,6 +35,7 @@ import android.os.Messenger;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.DataUnit;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -95,6 +96,15 @@ public class NetworkStatsManager {
/** @hide */
public static final int CALLBACK_RELEASED = 1;
+ /**
+ * Minimum data usage threshold for registering usage callbacks.
+ *
+ * Requests registered with a threshold lower than this will only be triggered once this minimum
+ * is reached.
+ * @hide
+ */
+ public static final long MIN_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(2);
+
private final Context mContext;
private final INetworkStatsService mService;
@@ -305,6 +315,8 @@ public class NetworkStatsManager {
* {@link java.lang.System#currentTimeMillis}.
* @param uid UID of app
* @param tag TAG of interest. Use {@link NetworkStats.Bucket#TAG_NONE} for no tags.
+ * @param state state of interest. Use {@link NetworkStats.Bucket#STATE_ALL} to aggregate
+ * traffic from all states.
* @return Statistics object or null if an error happened during statistics collection.
* @throws SecurityException if permissions are insufficient to read network statistics.
*/
diff --git a/android/app/usage/TimeSparseArray.java b/android/app/usage/TimeSparseArray.java
index 9ef88e41..4ec0e9e4 100644
--- a/android/app/usage/TimeSparseArray.java
+++ b/android/app/usage/TimeSparseArray.java
@@ -88,7 +88,7 @@ public class TimeSparseArray<E> extends LongSparseArray<E> {
key++;
keyIndex++;
}
- if (key >= origKey + 10) {
+ if (key >= origKey + 100) {
Slog.w(TAG, "Value " + value + " supposed to be inserted at " + origKey
+ " displaced to " + key);
}
diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java
index 84f57a30..503ca6c3 100644
--- a/android/app/usage/UsageEvents.java
+++ b/android/app/usage/UsageEvents.java
@@ -111,7 +111,7 @@ public final class UsageEvents implements Parcelable {
/**
* An event type denoting a change in App Standby Bucket. The new bucket can be
- * retrieved by calling {@link #getStandbyBucket()}.
+ * retrieved by calling {@link #getAppStandbyBucket()}.
*
* @see UsageStatsManager#getAppStandbyBucket()
*/
@@ -326,13 +326,23 @@ public final class UsageEvents implements Parcelable {
* Returns the standby bucket of the app, if the event is of type
* {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0.
* @return the standby bucket associated with the event.
- *
+ * @hide
*/
public int getStandbyBucket() {
return (mBucketAndReason & 0xFFFF0000) >>> 16;
}
/**
+ * Returns the standby bucket of the app, if the event is of type
+ * {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0.
+ * @return the standby bucket associated with the event.
+ *
+ */
+ public int getAppStandbyBucket() {
+ return (mBucketAndReason & 0xFFFF0000) >>> 16;
+ }
+
+ /**
* Returns the reason for the bucketing, if the event is of type
* {@link #STANDBY_BUCKET_CHANGED}, otherwise returns 0. Reason values include
* the main reason which is one of REASON_MAIN_*, OR'ed with REASON_SUB_*, if there
diff --git a/android/appwidget/AppWidgetHost.java b/android/appwidget/AppWidgetHost.java
index 37360bad..49cc498c 100644
--- a/android/appwidget/AppWidgetHost.java
+++ b/android/appwidget/AppWidgetHost.java
@@ -37,6 +37,7 @@ import android.util.SparseArray;
import android.widget.RemoteViews;
import android.widget.RemoteViews.OnClickHandler;
+import com.android.internal.R;
import com.android.internal.appwidget.IAppWidgetHost;
import com.android.internal.appwidget.IAppWidgetService;
@@ -171,8 +172,9 @@ public class AppWidgetHost {
return;
}
sServiceInitialized = true;
- if (!context.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_APP_WIDGETS)) {
+ PackageManager packageManager = context.getPackageManager();
+ if (!packageManager.hasSystemFeature(PackageManager.FEATURE_APP_WIDGETS)
+ && !context.getResources().getBoolean(R.bool.config_enableAppWidgetService)) {
return;
}
IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE);
diff --git a/android/bluetooth/BluetoothHearingAid.java b/android/bluetooth/BluetoothHearingAid.java
index 8f8083ed..159e165d 100644
--- a/android/bluetooth/BluetoothHearingAid.java
+++ b/android/bluetooth/BluetoothHearingAid.java
@@ -421,29 +421,29 @@ public final class BluetoothHearingAid implements BluetoothProfile {
}
/**
- * Check whether the device is active.
+ * Get the connected physical Hearing Aid devices that are active
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH}
* permission.
*
- * @return the connected device that is active or null if no device
- * is active
+ * @return the list of active devices. The first element is the left active
+ * device; the second element is the right active device. If either or both side
+ * is not active, it will be null on that position. Returns empty list on error.
* @hide
*/
@RequiresPermission(Manifest.permission.BLUETOOTH)
- public boolean isActiveDevice(@Nullable BluetoothDevice device) {
- if (VDBG) log("isActiveDevice()");
+ public List<BluetoothDevice> getActiveDevices() {
+ if (VDBG) log("getActiveDevices()");
try {
mServiceLock.readLock().lock();
- if (mService != null && isEnabled()
- && ((device == null) || isValidDevice(device))) {
- return mService.isActiveDevice(device);
+ if (mService != null && isEnabled()) {
+ return mService.getActiveDevices();
}
if (mService == null) Log.w(TAG, "Proxy not attached to service");
- return false;
+ return new ArrayList<>();
} catch (RemoteException e) {
Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
- return false;
+ return new ArrayList<>();
} finally {
mServiceLock.readLock().unlock();
}
diff --git a/android/bluetooth/BluetoothHidDevice.java b/android/bluetooth/BluetoothHidDevice.java
index af99bf7d..3bc8544e 100644
--- a/android/bluetooth/BluetoothHidDevice.java
+++ b/android/bluetooth/BluetoothHidDevice.java
@@ -701,6 +701,28 @@ public final class BluetoothHidDevice implements BluetoothProfile {
}
/**
+ * Gets the application name of the current HidDeviceService user.
+ *
+ * @return the current user name, or empty string if cannot get the name
+ * {@hide}
+ */
+ public String getUserAppName() {
+ final IBluetoothHidDevice service = mService;
+
+ if (service != null) {
+ try {
+ return service.getUserAppName();
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ } else {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+
+ return "";
+ }
+
+ /**
* Initiates connection to host which is currently paired with this device. If the application
* is not registered, #connect(BluetoothDevice) will fail. The connection state should be
* tracked by the application by handling callback from Callback#onConnectionStateChanged. The
diff --git a/android/content/ContentResolver.java b/android/content/ContentResolver.java
index 9f3df377..f7908b69 100644
--- a/android/content/ContentResolver.java
+++ b/android/content/ContentResolver.java
@@ -51,7 +51,6 @@ import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.util.MimeIconUtils;
import com.android.internal.util.Preconditions;
@@ -602,6 +601,8 @@ public abstract class ContentResolver {
try {
return provider.getType(url);
} catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
return null;
} catch (java.lang.Exception e) {
Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")");
@@ -620,9 +621,7 @@ public abstract class ContentResolver {
ContentProvider.getUriWithoutUserId(url), resolveUserId(url));
return type;
} catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- return null;
+ throw e.rethrowFromSystemServer();
} catch (java.lang.Exception e) {
Log.w(TAG, "Failed to get type for: " + url + " (" + e.getMessage() + ")");
return null;
@@ -1964,6 +1963,7 @@ public abstract class ContentResolver {
getContentService().registerContentObserver(uri, notifyForDescendents,
observer.getContentObserver(), userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -1982,6 +1982,7 @@ public abstract class ContentResolver {
contentObserver);
}
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2089,6 +2090,7 @@ public abstract class ContentResolver {
syncToNetwork ? NOTIFY_SYNC_TO_NETWORK : 0,
userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2105,6 +2107,7 @@ public abstract class ContentResolver {
observer != null && observer.deliverSelfNotifications(), flags,
userHandle, mTargetSdkVersion);
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2126,6 +2129,7 @@ public abstract class ContentResolver {
ContentProvider.getUriWithoutUserId(uri), modeFlags, /* toPackage= */ null,
resolveUserId(uri));
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2141,6 +2145,7 @@ public abstract class ContentResolver {
ContentProvider.getUriWithoutUserId(uri), modeFlags, toPackage,
resolveUserId(uri));
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2160,6 +2165,7 @@ public abstract class ContentResolver {
ContentProvider.getUriWithoutUserId(uri), modeFlags, /* toPackage= */ null,
resolveUserId(uri));
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2178,7 +2184,7 @@ public abstract class ContentResolver {
return ActivityManager.getService()
.getPersistedUriPermissions(mPackageName, true).getList();
} catch (RemoteException e) {
- throw new RuntimeException("Activity manager has died", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2194,7 +2200,7 @@ public abstract class ContentResolver {
return ActivityManager.getService()
.getPersistedUriPermissions(mPackageName, false).getList();
} catch (RemoteException e) {
- throw new RuntimeException("Activity manager has died", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2273,7 +2279,7 @@ public abstract class ContentResolver {
try {
getContentService().syncAsUser(request, userId);
} catch(RemoteException e) {
- // Shouldn't happen.
+ throw e.rethrowFromSystemServer();
}
}
@@ -2285,7 +2291,7 @@ public abstract class ContentResolver {
try {
getContentService().sync(request);
} catch(RemoteException e) {
- // Shouldn't happen.
+ throw e.rethrowFromSystemServer();
}
}
@@ -2349,6 +2355,7 @@ public abstract class ContentResolver {
try {
getContentService().cancelSync(account, authority, null);
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2360,6 +2367,7 @@ public abstract class ContentResolver {
try {
getContentService().cancelSyncAsUser(account, authority, null, userId);
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
}
@@ -2371,7 +2379,7 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncAdapterTypes();
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2383,7 +2391,7 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncAdapterTypesAsUser(userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2397,8 +2405,8 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncAdapterPackagesForAuthorityAsUser(authority, userId);
} catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
- return ArrayUtils.emptyArray(String.class);
}
/**
@@ -2414,7 +2422,7 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncAutomatically(account, authority);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2427,7 +2435,7 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncAutomaticallyAsUser(account, authority, userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2453,8 +2461,7 @@ public abstract class ContentResolver {
try {
getContentService().setSyncAutomaticallyAsUser(account, authority, sync, userId);
} catch (RemoteException e) {
- // exception ignored; if this is thrown then it means the runtime is in the midst of
- // being restarted
+ throw e.rethrowFromSystemServer();
}
}
@@ -2500,8 +2507,7 @@ public abstract class ContentResolver {
try {
getContentService().addPeriodicSync(account, authority, extras, pollFrequency);
} catch (RemoteException e) {
- // exception ignored; if this is thrown then it means the runtime is in the midst of
- // being restarted
+ throw e.rethrowFromSystemServer();
}
}
@@ -2540,7 +2546,7 @@ public abstract class ContentResolver {
try {
getContentService().removePeriodicSync(account, authority, extras);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2564,8 +2570,7 @@ public abstract class ContentResolver {
try {
getContentService().cancelRequest(request);
} catch (RemoteException e) {
- // exception ignored; if this is thrown then it means the runtime is in the midst of
- // being restarted
+ throw e.rethrowFromSystemServer();
}
}
@@ -2582,7 +2587,7 @@ public abstract class ContentResolver {
try {
return getContentService().getPeriodicSyncs(account, authority, null);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2596,7 +2601,7 @@ public abstract class ContentResolver {
try {
return getContentService().getIsSyncable(account, authority);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2609,7 +2614,7 @@ public abstract class ContentResolver {
try {
return getContentService().getIsSyncableAsUser(account, authority, userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2623,8 +2628,7 @@ public abstract class ContentResolver {
try {
getContentService().setIsSyncable(account, authority, syncable);
} catch (RemoteException e) {
- // exception ignored; if this is thrown then it means the runtime is in the midst of
- // being restarted
+ throw e.rethrowFromSystemServer();
}
}
@@ -2640,7 +2644,7 @@ public abstract class ContentResolver {
try {
return getContentService().getMasterSyncAutomatically();
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2652,7 +2656,7 @@ public abstract class ContentResolver {
try {
return getContentService().getMasterSyncAutomaticallyAsUser(userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2676,8 +2680,7 @@ public abstract class ContentResolver {
try {
getContentService().setMasterSyncAutomaticallyAsUser(sync, userId);
} catch (RemoteException e) {
- // exception ignored; if this is thrown then it means the runtime is in the midst of
- // being restarted
+ throw e.rethrowFromSystemServer();
}
}
@@ -2701,7 +2704,7 @@ public abstract class ContentResolver {
try {
return getContentService().isSyncActive(account, authority, null);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2727,7 +2730,7 @@ public abstract class ContentResolver {
}
return syncs.get(0);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2744,7 +2747,7 @@ public abstract class ContentResolver {
try {
return getContentService().getCurrentSyncs();
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2756,7 +2759,7 @@ public abstract class ContentResolver {
try {
return getContentService().getCurrentSyncsAsUser(userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2771,7 +2774,7 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncStatus(account, authority, null);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2784,7 +2787,7 @@ public abstract class ContentResolver {
try {
return getContentService().getSyncStatusAsUser(account, authority, null, userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2809,7 +2812,7 @@ public abstract class ContentResolver {
try {
return getContentService().isSyncPendingAsUser(account, authority, null, userId);
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2841,7 +2844,7 @@ public abstract class ContentResolver {
getContentService().addStatusChangeListener(mask, observer);
return observer;
} catch (RemoteException e) {
- throw new RuntimeException("the ContentService should always be reachable", e);
+ throw e.rethrowFromSystemServer();
}
}
@@ -2856,8 +2859,7 @@ public abstract class ContentResolver {
try {
getContentService().removeStatusChangeListener((ISyncStatusObserver.Stub) handle);
} catch (RemoteException e) {
- // exception ignored; if this is thrown then it means the runtime is in the midst of
- // being restarted
+ throw e.rethrowFromSystemServer();
}
}
@@ -3027,9 +3029,7 @@ public abstract class ContentResolver {
return sContentService;
}
IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME);
- if (false) Log.v("ContentService", "default service binder = " + b);
sContentService = IContentService.Stub.asInterface(b);
- if (false) Log.v("ContentService", "default service = " + sContentService);
return sContentService;
}
@@ -3038,7 +3038,7 @@ public abstract class ContentResolver {
return mPackageName;
}
- private static IContentService sContentService;
+ private static volatile IContentService sContentService;
private final Context mContext;
final String mPackageName;
diff --git a/android/content/Context.java b/android/content/Context.java
index 920056a8..ede7ee4b 100644
--- a/android/content/Context.java
+++ b/android/content/Context.java
@@ -3780,7 +3780,7 @@ public abstract class Context {
public static final String DROPBOX_SERVICE = "dropbox";
/**
- * System service name for the DeviceIdleController. There is no Java API for this.
+ * System service name for the DeviceIdleManager.
* @see #getSystemService(String)
* @hide
*/
diff --git a/android/content/Intent.java b/android/content/Intent.java
index 000912cd..f608fcb1 100644
--- a/android/content/Intent.java
+++ b/android/content/Intent.java
@@ -40,6 +40,7 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.PersistableBundle;
import android.os.Process;
import android.os.ResultReceiver;
import android.os.ShellCommand;
@@ -1814,8 +1815,12 @@ public class Intent implements Parcelable, Cloneable {
public static final String EXTRA_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME";
/**
- * Intent extra: A {@link Bundle} of extras for a package being suspended. Will be sent with
- * {@link #ACTION_MY_PACKAGE_SUSPENDED}.
+ * Intent extra: A {@link Bundle} of extras for a package being suspended. Will be sent as an
+ * extra with {@link #ACTION_MY_PACKAGE_SUSPENDED}.
+ *
+ * <p>The contents of this {@link Bundle} are a contract between the suspended app and the
+ * suspending app, i.e. any app with the permission {@code android.permission.SUSPEND_APPS}.
+ * This is meant to enable the suspended app to better handle the state of being suspended.
*
* @see #ACTION_MY_PACKAGE_SUSPENDED
* @see #ACTION_MY_PACKAGE_UNSUSPENDED
@@ -2282,6 +2287,34 @@ public class Intent implements Parcelable, Cloneable {
public static final String ACTION_MY_PACKAGE_SUSPENDED = "android.intent.action.MY_PACKAGE_SUSPENDED";
/**
+ * Activity Action: Started to show more details about why an application was suspended.
+ *
+ * <p>Whenever the system detects an activity launch for a suspended app, it shows a dialog to
+ * the user to inform them of the state and present them an affordance to start this activity
+ * action to show more details about the reason for suspension.
+ *
+ * <p>Apps holding {@link android.Manifest.permission#SUSPEND_APPS} must declare an activity
+ * handling this intent and protect it with
+ * {@link android.Manifest.permission#SEND_SHOW_SUSPENDED_APP_DETAILS}.
+ *
+ * <p>Includes an extra {@link #EXTRA_PACKAGE_NAME} which is the name of the suspended package.
+ *
+ * <p class="note">This is a protected intent that can only be sent
+ * by the system.
+ *
+ * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
+ * PersistableBundle, String)
+ * @see PackageManager#isPackageSuspended()
+ * @see #ACTION_PACKAGES_SUSPENDED
+ *
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_SHOW_SUSPENDED_APP_DETAILS =
+ "android.intent.action.SHOW_SUSPENDED_APP_DETAILS";
+
+ /**
* Broadcast Action: Sent to a package that has been unsuspended.
*
* <p class="note">This is a protected intent that can only be sent
@@ -6788,6 +6821,9 @@ public class Intent implements Parcelable, Cloneable {
case "--activity-task-on-home":
intent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME);
break;
+ case "--activity-match-external":
+ intent.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL);
+ break;
case "--receiver-registered-only":
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
break;
@@ -6924,7 +6960,7 @@ public class Intent implements Parcelable, Cloneable {
" [--activity-no-user-action] [--activity-previous-is-top]",
" [--activity-reorder-to-front] [--activity-reset-task-if-needed]",
" [--activity-single-top] [--activity-clear-task]",
- " [--activity-task-on-home]",
+ " [--activity-task-on-home] [--activity-match-external]",
" [--receiver-registered-only] [--receiver-replace-pending]",
" [--receiver-foreground] [--receiver-no-abort]",
" [--receiver-include-background]",
diff --git a/android/content/QuickViewConstants.java b/android/content/QuickViewConstants.java
index a25513de..132d43f2 100644
--- a/android/content/QuickViewConstants.java
+++ b/android/content/QuickViewConstants.java
@@ -45,8 +45,8 @@ public class QuickViewConstants {
* Feature to delete an individual document. Quick viewer implementations must use
* Storage Access Framework to both verify delete permission and to delete content.
*
- * @see DocumentsContract#Document#FLAG_SUPPORTS_DELETE
- * @see DocumentsContract#deleteDocument(ContentResolver resolver, Uri documentUri)
+ * @see DocumentsContract.Document#FLAG_SUPPORTS_DELETE
+ * @see DocumentsContract#deleteDocument(ContentResolver, Uri)
*/
public static final String FEATURE_DELETE = "android:delete";
diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java
index e85058df..d65e051b 100644
--- a/android/content/pm/ApplicationInfo.java
+++ b/android/content/pm/ApplicationInfo.java
@@ -26,6 +26,7 @@ import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
@@ -590,26 +591,33 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
public static final int PRIVATE_FLAG_VIRTUAL_PRELOAD = 1 << 16;
/**
- * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+ * Value for {@link #privateFlags}: whether this app is pre-installed on the
* OEM partition of the system image.
* @hide
*/
public static final int PRIVATE_FLAG_OEM = 1 << 17;
/**
- * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+ * Value for {@link #privateFlags}: whether this app is pre-installed on the
* vendor partition of the system image.
* @hide
*/
public static final int PRIVATE_FLAG_VENDOR = 1 << 18;
/**
- * Value for {@linl #privateFlags}: whether this app is pre-installed on the
+ * Value for {@link #privateFlags}: whether this app is pre-installed on the
* product partition of the system image.
* @hide
*/
public static final int PRIVATE_FLAG_PRODUCT = 1 << 19;
+ /**
+ * Value for {@link #privateFlags}: whether this app is signed with the
+ * platform key.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY = 1 << 20;
+
/** @hide */
@IntDef(flag = true, prefix = { "PRIVATE_FLAG_" }, value = {
PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE,
@@ -629,6 +637,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
PRIVATE_FLAG_PRIVILEGED,
PRIVATE_FLAG_PRODUCT,
PRIVATE_FLAG_REQUIRED_FOR_SYSTEM_USER,
+ PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY,
PRIVATE_FLAG_STATIC_SHARED_LIBRARY,
PRIVATE_FLAG_VENDOR,
PRIVATE_FLAG_VIRTUAL_PRELOAD,
@@ -904,7 +913,17 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
* The app's declared version code.
* @hide
*/
- public long versionCode;
+ public long longVersionCode;
+
+ /**
+ * An integer representation of the app's declared version code. This is being left in place as
+ * some apps were using reflection to access it before the move to long in
+ * {@link android.os.Build.VERSION_CODES#P}
+ * @deprecated Use {@link #longVersionCode} instead.
+ * @hide
+ */
+ @Deprecated
+ public int versionCode;
/**
* The user-visible SDK version (ex. 26) of the framework against which the application claims
@@ -1114,11 +1133,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
*/
public static final int HIDDEN_API_ENFORCEMENT_NONE = 0;
/**
- * Light grey list enforcement, the strictest option. Enforces the light grey, dark grey and
- * black lists.
+ * No API enforcement, but enable the detection logic and warnings. Observed behaviour is the
+ * same as {@link #HIDDEN_API_ENFORCEMENT_NONE} but you may see warnings in the log when APIs
+ * are accessed.
* @hide
* */
- public static final int HIDDEN_API_ENFORCEMENT_ALL_LISTS = 1;
+ public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1;
/**
* Dark grey list enforcement. Enforces the dark grey and black lists
* @hide
@@ -1140,14 +1160,15 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
@IntDef(prefix = { "HIDDEN_API_ENFORCEMENT_" }, value = {
HIDDEN_API_ENFORCEMENT_DEFAULT,
HIDDEN_API_ENFORCEMENT_NONE,
- HIDDEN_API_ENFORCEMENT_ALL_LISTS,
+ HIDDEN_API_ENFORCEMENT_JUST_WARN,
HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK,
HIDDEN_API_ENFORCEMENT_BLACK,
})
@Retention(RetentionPolicy.SOURCE)
public @interface HiddenApiEnforcementPolicy {}
- private boolean isValidHiddenApiEnforcementPolicy(int policy) {
+ /** @hide */
+ public static boolean isValidHiddenApiEnforcementPolicy(int policy) {
return policy >= HIDDEN_API_ENFORCEMENT_DEFAULT && policy <= HIDDEN_API_ENFORCEMENT_MAX;
}
@@ -1214,7 +1235,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
pw.println(prefix + "enabled=" + enabled
+ " minSdkVersion=" + minSdkVersion
+ " targetSdkVersion=" + targetSdkVersion
- + " versionCode=" + versionCode
+ + " versionCode=" + longVersionCode
+ " targetSandboxVersion=" + targetSandboxVersion);
if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
if (manageSpaceActivityName != null) {
@@ -1287,7 +1308,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
proto.write(ApplicationInfoProto.Version.ENABLED, enabled);
proto.write(ApplicationInfoProto.Version.MIN_SDK_VERSION, minSdkVersion);
proto.write(ApplicationInfoProto.Version.TARGET_SDK_VERSION, targetSdkVersion);
- proto.write(ApplicationInfoProto.Version.VERSION_CODE, versionCode);
+ proto.write(ApplicationInfoProto.Version.VERSION_CODE, longVersionCode);
proto.write(ApplicationInfoProto.Version.TARGET_SANDBOX_VERSION, targetSandboxVersion);
proto.end(versionToken);
@@ -1421,7 +1442,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
uid = orig.uid;
minSdkVersion = orig.minSdkVersion;
targetSdkVersion = orig.targetSdkVersion;
- versionCode = orig.versionCode;
+ setVersionCode(orig.longVersionCode);
enabled = orig.enabled;
enabledSetting = orig.enabledSetting;
installLocation = orig.installLocation;
@@ -1495,7 +1516,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
dest.writeInt(uid);
dest.writeInt(minSdkVersion);
dest.writeInt(targetSdkVersion);
- dest.writeLong(versionCode);
+ dest.writeLong(longVersionCode);
dest.writeInt(enabled ? 1 : 0);
dest.writeInt(enabledSetting);
dest.writeInt(installLocation);
@@ -1566,7 +1587,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
uid = source.readInt();
minSdkVersion = source.readInt();
targetSdkVersion = source.readInt();
- versionCode = source.readLong();
+ setVersionCode(source.readLong());
enabled = source.readInt() != 0;
enabledSetting = source.readInt();
installLocation = source.readInt();
@@ -1658,17 +1679,26 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
return SystemConfig.getInstance().getHiddenApiWhitelistedApps().contains(packageName);
}
+ private boolean isAllowedToUseHiddenApis() {
+ return isSignedWithPlatformKey()
+ || (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp()));
+ }
+
/**
* @hide
*/
public @HiddenApiEnforcementPolicy int getHiddenApiEnforcementPolicy() {
+ if (isAllowedToUseHiddenApis()) {
+ return HIDDEN_API_ENFORCEMENT_NONE;
+ }
if (mHiddenApiPolicy != HIDDEN_API_ENFORCEMENT_DEFAULT) {
return mHiddenApiPolicy;
}
- if (isPackageWhitelistedForHiddenApis() && (isSystemApp() || isUpdatedSystemApp())) {
- return HIDDEN_API_ENFORCEMENT_NONE;
+ if (targetSdkVersion < Build.VERSION_CODES.P) {
+ return HIDDEN_API_ENFORCEMENT_BLACK;
+ } else {
+ return HIDDEN_API_ENFORCEMENT_DARK_GREY_AND_BLACK;
}
- return HIDDEN_API_ENFORCEMENT_BLACK;
}
/**
@@ -1682,6 +1712,39 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
}
/**
+ * Updates the hidden API enforcement policy for this app from the given values, if appropriate.
+ *
+ * This will have no effect if this app is not subject to hidden API enforcement, i.e. if it
+ * is on the package whitelist.
+ *
+ * @param policyPreP configured policy for pre-P apps, or {@link
+ * #HIDDEN_API_ENFORCEMENT_DEFAULT} if nothing configured.
+ * @param policyP configured policy for apps targeting P or later, or {@link
+ * #HIDDEN_API_ENFORCEMENT_DEFAULT} if nothing configured.
+ * @hide
+ */
+ public void maybeUpdateHiddenApiEnforcementPolicy(
+ @HiddenApiEnforcementPolicy int policyPreP, @HiddenApiEnforcementPolicy int policyP) {
+ if (isPackageWhitelistedForHiddenApis()) {
+ return;
+ }
+ if (targetSdkVersion < Build.VERSION_CODES.P) {
+ setHiddenApiEnforcementPolicy(policyPreP);
+ } else if (targetSdkVersion >= Build.VERSION_CODES.P) {
+ setHiddenApiEnforcementPolicy(policyP);
+ }
+
+ }
+
+ /**
+ * @hide
+ */
+ public void setVersionCode(long newVersionCode) {
+ longVersionCode = newVersionCode;
+ versionCode = (int) newVersionCode;
+ }
+
+ /**
* @hide
*/
@Override
@@ -1758,6 +1821,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
}
/** @hide */
+ public boolean isSignedWithPlatformKey() {
+ return (privateFlags & ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY) != 0;
+ }
+
+ /** @hide */
@TestApi
public boolean isPrivilegedApp() {
return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
diff --git a/android/content/pm/LauncherApps.java b/android/content/pm/LauncherApps.java
index 9aace2e7..8223363a 100644
--- a/android/content/pm/LauncherApps.java
+++ b/android/content/pm/LauncherApps.java
@@ -212,7 +212,7 @@ public class LauncherApps {
* an applicaton.
*
* <p>Note: On devices running {@link android.os.Build.VERSION_CODES#P Android P} or higher,
- * any apps that override {@link #onPackagesSuspended(String[], Bundle, UserHandle)} will
+ * any apps that override {@link #onPackagesSuspended(String[], UserHandle, Bundle)} will
* not receive this callback.
*
* @param packageNames The names of the packages that have just been
@@ -226,15 +226,20 @@ public class LauncherApps {
* Indicates that one or more packages have been suspended. A device administrator or an app
* with {@code android.permission.SUSPEND_APPS} can do this.
*
+ * <p>A suspending app with the permission {@code android.permission.SUSPEND_APPS} can
+ * optionally provide a {@link Bundle} of extra information that it deems helpful for the
+ * launcher to handle the suspended state of these packages. The contents of this
+ * {@link Bundle} supposed to be a contract between the suspending app and the launcher.
+ *
* @param packageNames The names of the packages that have just been suspended.
- * @param launcherExtras A {@link Bundle} of extras for the launcher.
* @param user the user for which the given packages were suspended.
- *
+ * @param launcherExtras A {@link Bundle} of extras for the launcher, if provided to the
+ * system, {@code null} otherwise.
* @see PackageManager#isPackageSuspended()
* @see #getSuspendedPackageLauncherExtras(String, UserHandle)
*/
- public void onPackagesSuspended(String[] packageNames, @Nullable Bundle launcherExtras,
- UserHandle user) {
+ public void onPackagesSuspended(String[] packageNames, UserHandle user,
+ @Nullable Bundle launcherExtras) {
onPackagesSuspended(packageNames, user);
}
@@ -662,6 +667,9 @@ public class LauncherApps {
* {@code PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
* PersistableBundle, String)}.
*
+ * <p>The contents of this {@link Bundle} are supposed to be a contract between the suspending
+ * app and the launcher.
+ *
* <p>Note: This just returns whatever extras were provided to the system, <em>which might
* even be {@code null}.</em>
*
@@ -670,7 +678,7 @@ public class LauncherApps {
* @return A {@link Bundle} of launcher extras. Or {@code null} if the package is not currently
* suspended.
*
- * @see Callback#onPackagesSuspended(String[], Bundle, UserHandle)
+ * @see Callback#onPackagesSuspended(String[], UserHandle, Bundle)
* @see PackageManager#isPackageSuspended()
*/
public @Nullable Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) {
@@ -1298,8 +1306,8 @@ public class LauncherApps {
mCallback.onPackagesUnavailable(info.packageNames, info.user, info.replacing);
break;
case MSG_SUSPENDED:
- mCallback.onPackagesSuspended(info.packageNames, info.launcherExtras,
- info.user);
+ mCallback.onPackagesSuspended(info.packageNames, info.user, info.launcherExtras
+ );
break;
case MSG_UNSUSPENDED:
mCallback.onPackagesUnsuspended(info.packageNames, info.user);
diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java
index 627ceb78..5f9f8f1f 100644
--- a/android/content/pm/PackageInfo.java
+++ b/android/content/pm/PackageInfo.java
@@ -244,7 +244,7 @@ public class PackageInfo implements Parcelable {
* the first position to be the same across updates.
*
* <strong>Deprecated</strong> This has been replaced by the
- * {@link PackageInfo#signingCertificateHistory} field, which takes into
+ * {@link PackageInfo#signingInfo} field, which takes into
* account signing certificate rotation. For backwards compatibility in
* the event of signing certificate rotation, this will return the oldest
* reported signing certificate, so that an application will appear to
@@ -256,29 +256,15 @@ public class PackageInfo implements Parcelable {
public Signature[] signatures;
/**
- * Array of all signatures arrays read from the package file, potentially
+ * Signing information read from the package file, potentially
* including past signing certificates no longer used after signing
- * certificate rotation. Though signing certificate rotation is only
- * available for apps with a single signing certificate, this provides an
- * array of arrays so that packages signed with multiple signing
- * certificates can still return all signers. This is only filled in if
+ * certificate rotation. This is only filled in if
* the flag {@link PackageManager#GET_SIGNING_CERTIFICATES} was set.
*
- * A package must be singed with at least one certificate, which is at
- * position zero in the array. An application may be signed by multiple
- * certificates, which would be in the array at position zero in an
- * indeterminate order. A package may also have a history of certificates
- * due to signing certificate rotation. In this case, the array will be
- * populated by a series of single-entry arrays corresponding to a signing
- * certificate of the package.
- *
- * <strong>Note:</strong> Signature ordering is not guaranteed to be
- * stable which means that a package signed with certificates A and B is
- * equivalent to being signed with certificates B and A. This means that
- * in case multiple signatures are reported you cannot assume the one at
- * the first position will be the same across updates.
+ * Use this field instead of the deprecated {@code signatures} field.
+ * See {@link SigningInfo} for more information on its contents.
*/
- public Signature[][] signingCertificateHistory;
+ public SigningInfo signingInfo;
/**
* Application specified preferred configuration
@@ -476,17 +462,11 @@ public class PackageInfo implements Parcelable {
dest.writeBoolean(mOverlayIsStatic);
dest.writeInt(compileSdkVersion);
dest.writeString(compileSdkVersionCodename);
- writeSigningCertificateHistoryToParcel(dest, parcelableFlags);
- }
-
- private void writeSigningCertificateHistoryToParcel(Parcel dest, int parcelableFlags) {
- if (signingCertificateHistory != null) {
- dest.writeInt(signingCertificateHistory.length);
- for (int i = 0; i < signingCertificateHistory.length; i++) {
- dest.writeTypedArray(signingCertificateHistory[i], parcelableFlags);
- }
+ if (signingInfo != null) {
+ dest.writeInt(1);
+ signingInfo.writeToParcel(dest, parcelableFlags);
} else {
- dest.writeInt(-1);
+ dest.writeInt(0);
}
}
@@ -544,7 +524,10 @@ public class PackageInfo implements Parcelable {
mOverlayIsStatic = source.readBoolean();
compileSdkVersion = source.readInt();
compileSdkVersionCodename = source.readString();
- readSigningCertificateHistoryFromParcel(source);
+ int hasSigningInfo = source.readInt();
+ if (hasSigningInfo != 0) {
+ signingInfo = SigningInfo.CREATOR.createFromParcel(source);
+ }
// The component lists were flattened with the redundant ApplicationInfo
// instances omitted. Distribute the canonical one here as appropriate.
@@ -556,16 +539,6 @@ public class PackageInfo implements Parcelable {
}
}
- private void readSigningCertificateHistoryFromParcel(Parcel source) {
- int len = source.readInt();
- if (len != -1) {
- signingCertificateHistory = new Signature[len][];
- for (int i = 0; i < len; i++) {
- signingCertificateHistory[i] = source.createTypedArray(Signature.CREATOR);
- }
- }
- }
-
private void propagateApplicationInfo(ApplicationInfo appInfo, ComponentInfo[] components) {
if (components != null) {
for (ComponentInfo ci : components) {
diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java
index 491f0af2..9d3b53f2 100644
--- a/android/content/pm/PackageManager.java
+++ b/android/content/pm/PackageManager.java
@@ -2629,6 +2629,17 @@ public abstract class PackageManager {
"android.hardware.strongbox_keystore";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+ * The device has a Keymaster implementation that supports Device ID attestation.
+ *
+ * @see DevicePolicyManager#isDeviceIdAttestationSupported
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_DEVICE_ID_ATTESTATION =
+ "android.software.device_id_attestation";
+
+ /**
* Action to external storage service to clean out removed apps.
* @hide
*/
@@ -3941,6 +3952,7 @@ public abstract class PackageManager {
*
* @hide
*/
+ @TestApi
public abstract @NonNull String getServicesSystemSharedLibraryPackageName();
/**
@@ -3950,6 +3962,7 @@ public abstract class PackageManager {
*
* @hide
*/
+ @TestApi
public abstract @NonNull String getSharedSystemSharedLibraryPackageName();
/**
@@ -5558,7 +5571,8 @@ public abstract class PackageManager {
* @param packageName The name of the package to get the suspended status of.
* @param userId The user id.
* @return {@code true} if the package is suspended or {@code false} if the package is not
- * suspended or could not be found.
+ * suspended.
+ * @throws IllegalArgumentException if the package was not found.
* @hide
*/
public abstract boolean isPackageSuspendedForUser(String packageName, int userId);
@@ -5567,12 +5581,13 @@ public abstract class PackageManager {
* Query if an app is currently suspended.
*
* @return {@code true} if the given package is suspended, {@code false} otherwise
+ * @throws NameNotFoundException if the package could not be found.
*
* @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String)
* @hide
*/
@SystemApi
- public boolean isPackageSuspended(String packageName) {
+ public boolean isPackageSuspended(String packageName) throws NameNotFoundException {
throw new UnsupportedOperationException("isPackageSuspended not implemented");
}
@@ -5603,51 +5618,16 @@ public abstract class PackageManager {
}
/**
- * Retrieve the {@link PersistableBundle} that was passed as {@code appExtras} when the given
- * package was suspended.
- *
- * <p> The caller must hold permission {@link Manifest.permission#SUSPEND_APPS} to use this
- * api.</p>
- *
- * @param packageName The package to retrieve extras for.
- * @return The {@code appExtras} for the suspended package.
- *
- * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String)
- * @hide
- */
- @SystemApi
- @RequiresPermission(Manifest.permission.SUSPEND_APPS)
- public @Nullable PersistableBundle getSuspendedPackageAppExtras(String packageName) {
- throw new UnsupportedOperationException("getSuspendedPackageAppExtras not implemented");
- }
-
- /**
- * Set the app extras for a suspended package. This method can be used to update the appExtras
- * for a package that was earlier suspended using
- * {@link #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle,
- * String)}
- * Does nothing if the given package is not already in a suspended state.
+ * Returns a {@link Bundle} of extras that was meant to be sent to the calling app when it was
+ * suspended. An app with the permission {@code android.permission.SUSPEND_APPS} can supply this
+ * to the system at the time of suspending an app.
*
- * @param packageName The package for which the appExtras need to be updated
- * @param appExtras The new appExtras for the given package
+ * <p>This is the same {@link Bundle} that is sent along with the broadcast
+ * {@link Intent#ACTION_MY_PACKAGE_SUSPENDED}, whenever the app is suspended. The contents of
+ * this {@link Bundle} are a contract between the suspended app and the suspending app.
*
- * @see #setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, String)
- *
- * @hide
- */
- @SystemApi
- @RequiresPermission(Manifest.permission.SUSPEND_APPS)
- public void setSuspendedPackageAppExtras(String packageName,
- @Nullable PersistableBundle appExtras) {
- throw new UnsupportedOperationException("setSuspendedPackageAppExtras not implemented");
- }
-
- /**
- * Returns any extra information supplied as {@code appExtras} to the system when the calling
- * app was suspended.
- *
- * <p>Note: If no extras were supplied to the system, this method will return {@code null}, even
- * when the calling app has been suspended.</p>
+ * <p>Note: These extras are optional, so if no extras were supplied to the system, this method
+ * will return {@code null}, even when the calling app has been suspended.
*
* @return A {@link Bundle} containing the extras for the app, or {@code null} if the
* package is not currently suspended.
@@ -5655,6 +5635,7 @@ public abstract class PackageManager {
* @see #isPackageSuspended()
* @see Intent#ACTION_MY_PACKAGE_UNSUSPENDED
* @see Intent#ACTION_MY_PACKAGE_SUSPENDED
+ * @see Intent#EXTRA_SUSPENDED_PACKAGE_EXTRAS
*/
public @Nullable Bundle getSuspendedPackageAppExtras() {
throw new UnsupportedOperationException("getSuspendedPackageAppExtras not implemented");
@@ -6118,7 +6099,9 @@ public abstract class PackageManager {
* signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
* since it takes into account the possibility of signing certificate rotation, except in the
* case of packages that are signed by multiple certificates, for which signing certificate
- * rotation is not supported.
+ * rotation is not supported. This method is analogous to using {@code getPackageInfo} with
+ * {@code GET_SIGNING_CERTIFICATES} and then searching through the resulting {@code
+ * signingCertificateHistory} field to see if the desired certificate is present.
*
* @param packageName package whose signing certificates to check
* @param certificate signing certificate for which to search
@@ -6132,13 +6115,19 @@ public abstract class PackageManager {
}
/**
- * Searches the set of signing certificates by which the given uid has proven to have been
- * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
+ * Searches the set of signing certificates by which the package(s) for the given uid has proven
+ * to have been signed. For multiple packages sharing the same uid, this will return the
+ * signing certificates found in the signing history of the "newest" package, where "newest"
+ * indicates the package with the newest signing certificate in the shared uid group. This
+ * method should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
* since it takes into account the possibility of signing certificate rotation, except in the
* case of packages that are signed by multiple certificates, for which signing certificate
- * rotation is not supported.
+ * rotation is not supported. This method is analogous to using {@code getPackagesForUid}
+ * followed by {@code getPackageInfo} with {@code GET_SIGNING_CERTIFICATES}, selecting the
+ * {@code PackageInfo} of the newest-signed bpackage , and finally searching through the
+ * resulting {@code signingCertificateHistory} field to see if the desired certificate is there.
*
- * @param uid package whose signing certificates to check
+ * @param uid uid whose signing certificates to check
* @param certificate signing certificate for which to search
* @param type representation of the {@code certificate}
* @return true if this package was or is signed by exactly the certificate {@code certificate}
diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java
index c9b78c08..a9d09110 100644
--- a/android/content/pm/PackageManagerInternal.java
+++ b/android/content/pm/PackageManagerInternal.java
@@ -191,10 +191,10 @@ public abstract class PackageManagerInternal {
/**
* Retrieve launcher extras for a suspended package provided to the system in
* {@link PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
- * PersistableBundle, String)}
+ * PersistableBundle, String)}.
*
* @param packageName The package for which to return launcher extras.
- * @param userId The user for which to check,
+ * @param userId The user for which to check.
* @return The launcher extras.
*
* @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle,
@@ -205,6 +205,38 @@ public abstract class PackageManagerInternal {
int userId);
/**
+ * Internal api to query the suspended state of a package.
+ * @param packageName The package to check.
+ * @param userId The user id to check for.
+ * @return {@code true} if the package is suspended, {@code false} otherwise.
+ * @see PackageManager#isPackageSuspended(String)
+ */
+ public abstract boolean isPackageSuspended(String packageName, int userId);
+
+ /**
+ * Get the name of the package that suspended the given package. Packages can be suspended by
+ * device administrators or apps holding {@link android.Manifest.permission#MANAGE_USERS} or
+ * {@link android.Manifest.permission#SUSPEND_APPS}.
+ *
+ * @param suspendedPackage The package that has been suspended.
+ * @param userId The user for which to check.
+ * @return Name of the package that suspended the given package. Returns {@code null} if the
+ * given package is not currently suspended and the platform package name - i.e.
+ * {@code "android"} - if the package was suspended by a device admin.
+ */
+ public abstract String getSuspendingPackage(String suspendedPackage, int userId);
+
+ /**
+ * Get the dialog message to be shown to the user when they try to launch a suspended
+ * application.
+ *
+ * @param suspendedPackage The package that has been suspended.
+ * @param userId The user for which to check.
+ * @return The dialog message to be shown to the user.
+ */
+ public abstract String getSuspendedDialogMessage(String suspendedPackage, int userId);
+
+ /**
* Do a straight uid lookup for the given package/application in the given user.
* @see PackageManager#getPackageUidAsUser(String, int, int)
* @return The app's uid, or < 0 if the package was not found in that user
@@ -429,7 +461,7 @@ public abstract class PackageManagerInternal {
* Resolves an activity intent, allowing instant apps to be resolved.
*/
public abstract ResolveInfo resolveIntent(Intent intent, String resolvedType,
- int flags, int userId, boolean resolveForStart);
+ int flags, int userId, boolean resolveForStart, int filterCallingUid);
/**
* Resolves a service intent, allowing instant apps to be resolved.
diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java
index 2f0faf25..453a74aa 100644
--- a/android/content/pm/PackageParser.java
+++ b/android/content/pm/PackageParser.java
@@ -810,21 +810,11 @@ public class PackageParser {
// replacement for GET_SIGNATURES
if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
- if (p.mSigningDetails.hasPastSigningCertificates()) {
- // Package has included signing certificate rotation information. Convert each
- // entry to an array
- int numberOfSigs = p.mSigningDetails.pastSigningCertificates.length;
- pi.signingCertificateHistory = new Signature[numberOfSigs][];
- for (int i = 0; i < numberOfSigs; i++) {
- pi.signingCertificateHistory[i] =
- new Signature[] { p.mSigningDetails.pastSigningCertificates[i] };
- }
- } else if (p.mSigningDetails.hasSignatures()) {
- // otherwise keep old behavior
- int numberOfSigs = p.mSigningDetails.signatures.length;
- pi.signingCertificateHistory = new Signature[1][numberOfSigs];
- System.arraycopy(p.mSigningDetails.signatures, 0,
- pi.signingCertificateHistory[0], 0, numberOfSigs);
+ if (p.mSigningDetails != SigningDetails.UNKNOWN) {
+ // only return a valid SigningInfo if there is signing information to report
+ pi.signingInfo = new SigningInfo(p.mSigningDetails);
+ } else {
+ pi.signingInfo = null;
}
}
return pi;
@@ -1918,7 +1908,7 @@ public class PackageParser {
com.android.internal.R.styleable.AndroidManifest_versionCode, 0);
pkg.mVersionCodeMajor = sa.getInteger(
com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0);
- pkg.applicationInfo.versionCode = pkg.getLongVersionCode();
+ pkg.applicationInfo.setVersionCode(pkg.getLongVersionCode());
pkg.baseRevisionCode = sa.getInteger(
com.android.internal.R.styleable.AndroidManifest_revisionCode, 0);
pkg.mVersionName = sa.getNonConfigurationString(
@@ -2726,7 +2716,7 @@ public class PackageParser {
// Fot apps targeting O-MR1 we require explicit enumeration of all certs.
String[] additionalCertSha256Digests = EmptyArray.STRING;
- if (pkg.applicationInfo.targetSdkVersion > Build.VERSION_CODES.O) {
+ if (pkg.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O_MR1) {
additionalCertSha256Digests = parseAdditionalCertificates(res, parser, outError);
if (additionalCertSha256Digests == null) {
return false;
diff --git a/android/content/pm/PackageSharedLibraryUpdater.java b/android/content/pm/PackageSharedLibraryUpdater.java
index fa894320..b14b321a 100644
--- a/android/content/pm/PackageSharedLibraryUpdater.java
+++ b/android/content/pm/PackageSharedLibraryUpdater.java
@@ -62,7 +62,7 @@ public abstract class PackageSharedLibraryUpdater {
static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(PackageParser.Package pkg) {
int targetSdkVersion = pkg.applicationInfo.targetSdkVersion;
- return targetSdkVersion <= Build.VERSION_CODES.O_MR1;
+ return targetSdkVersion < Build.VERSION_CODES.P;
}
/**
diff --git a/android/content/pm/PackageUserState.java b/android/content/pm/PackageUserState.java
index f7b6e091..f471a1d9 100644
--- a/android/content/pm/PackageUserState.java
+++ b/android/content/pm/PackageUserState.java
@@ -34,6 +34,7 @@ import android.util.ArraySet;
import com.android.internal.util.ArrayUtils;
import java.util.Arrays;
+import java.util.Objects;
/**
* Per-user state information about a package.
@@ -47,6 +48,7 @@ public class PackageUserState {
public boolean hidden; // Is the app restricted by owner / admin
public boolean suspended;
public String suspendingPackage;
+ public String dialogMessage; // Message to show when a suspended package launch attempt is made
public PersistableBundle suspendedAppExtras;
public PersistableBundle suspendedLauncherExtras;
public boolean instantApp;
@@ -82,6 +84,7 @@ public class PackageUserState {
hidden = o.hidden;
suspended = o.suspended;
suspendingPackage = o.suspendingPackage;
+ dialogMessage = o.dialogMessage;
suspendedAppExtras = o.suspendedAppExtras;
suspendedLauncherExtras = o.suspendedLauncherExtras;
instantApp = o.instantApp;
@@ -208,6 +211,9 @@ public class PackageUserState {
|| !suspendingPackage.equals(oldState.suspendingPackage)) {
return false;
}
+ if (!Objects.equals(dialogMessage, oldState.dialogMessage)) {
+ return false;
+ }
if (!BaseBundle.kindofEquals(suspendedAppExtras,
oldState.suspendedAppExtras)) {
return false;
diff --git a/android/content/pm/SigningInfo.java b/android/content/pm/SigningInfo.java
new file mode 100644
index 00000000..ef874035
--- /dev/null
+++ b/android/content/pm/SigningInfo.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information pertaining to the signing certificates used to sign a package.
+ */
+public final class SigningInfo implements Parcelable {
+
+ @NonNull
+ private final PackageParser.SigningDetails mSigningDetails;
+
+ public SigningInfo() {
+ mSigningDetails = PackageParser.SigningDetails.UNKNOWN;
+ }
+
+ /**
+ * @hide only packagemanager should be populating this
+ */
+ public SigningInfo(PackageParser.SigningDetails signingDetails) {
+ mSigningDetails = new PackageParser.SigningDetails(signingDetails);
+ }
+
+ public SigningInfo(SigningInfo orig) {
+ mSigningDetails = new PackageParser.SigningDetails(orig.mSigningDetails);
+ }
+
+ private SigningInfo(Parcel source) {
+ mSigningDetails = PackageParser.SigningDetails.CREATOR.createFromParcel(source);
+ }
+
+ /**
+ * Although relatively uncommon, packages may be signed by more than one signer, in which case
+ * their identity is viewed as being the set of all signers, not just any one.
+ */
+ public boolean hasMultipleSigners() {
+ return mSigningDetails.signatures != null && mSigningDetails.signatures.length > 1;
+ }
+
+ /**
+ * APK Signature Scheme v3 enables packages to provide a proof-of-rotation record that the
+ * platform verifies, and uses, to allow the use of new signing certificates. This is only
+ * available to packages that are not signed by multiple signers. In the event of a change to a
+ * new signing certificate, the package's past signing certificates are presented as well. Any
+ * check of a package's signing certificate should also include a search through its entire
+ * signing history, since it could change to a new signing certificate at any time.
+ */
+ public boolean hasPastSigningCertificates() {
+ return mSigningDetails.signatures != null
+ && mSigningDetails.pastSigningCertificates != null;
+ }
+
+ /**
+ * Returns the signing certificates this package has proven it is authorized to use. This
+ * includes both the signing certificate associated with the signer of the package and the past
+ * signing certificates it included as its proof of signing certificate rotation. This method
+ * is the preferred replacement for the {@code GET_SIGNATURES} flag used with {@link
+ * PackageManager#getPackageInfo(String, int)}. When determining if a package is signed by a
+ * desired certificate, the returned array should be checked to determine if it is one of the
+ * entries.
+ *
+ * <note>
+ * This method returns null if the package is signed by multiple signing certificates, as
+ * opposed to being signed by one current signer and also providing the history of past
+ * signing certificates. {@link #hasMultipleSigners()} may be used to determine if this
+ * package is signed by multiple signers. Packages which are signed by multiple signers
+ * cannot change their signing certificates and their {@code Signature} array should be
+ * checked to make sure that every entry matches the looked-for signing certificates.
+ * </note>
+ */
+ public Signature[] getSigningCertificateHistory() {
+ if (hasMultipleSigners()) {
+ return null;
+ } else if (!hasPastSigningCertificates()) {
+
+ // this package is only signed by one signer with no history, return it
+ return mSigningDetails.signatures;
+ } else {
+
+ // this package has provided proof of past signing certificates, include them
+ return mSigningDetails.pastSigningCertificates;
+ }
+ }
+
+ /**
+ * Returns the signing certificates used to sign the APK contents of this application. Not
+ * including any past signing certificates the package proved it is authorized to use.
+ * <note>
+ * This method should not be used unless {@link #hasMultipleSigners()} returns true,
+ * indicating that {@link #getSigningCertificateHistory()} cannot be used, otherwise {@link
+ * #getSigningCertificateHistory()} should be preferred.
+ * </note>
+ */
+ public Signature[] getApkContentsSigners() {
+ return mSigningDetails.signatures;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int parcelableFlags) {
+ mSigningDetails.writeToParcel(dest, parcelableFlags);
+ }
+
+ public static final Parcelable.Creator<SigningInfo> CREATOR =
+ new Parcelable.Creator<SigningInfo>() {
+ @Override
+ public SigningInfo createFromParcel(Parcel source) {
+ return new SigningInfo(source);
+ }
+
+ @Override
+ public SigningInfo[] newArray(int size) {
+ return new SigningInfo[size];
+ }
+ };
+}
diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java
index 506eab5c..098f1000 100644
--- a/android/graphics/ImageDecoder.java
+++ b/android/graphics/ImageDecoder.java
@@ -16,36 +16,177 @@
package android.graphics;
+import static android.system.OsConstants.SEEK_CUR;
+import static android.system.OsConstants.SEEK_SET;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.AnyThread;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.Px;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
import android.content.res.AssetManager.AssetInputStream;
import android.content.res.Resources;
import android.graphics.drawable.AnimatedImageDrawable;
-import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
import android.net.Uri;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
import android.util.DisplayMetrics;
import android.util.Size;
import android.util.TypedValue;
-import java.nio.ByteBuffer;
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.lang.ArrayIndexOutOfBoundsException;
-import java.lang.AutoCloseable;
-import java.lang.NullPointerException;
import java.lang.annotation.Retention;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
- * Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
+ * <p>A class for converting encoded images (like {@code PNG}, {@code JPEG},
+ * {@code WEBP}, {@code GIF}, or {@code HEIF}) into {@link Drawable} or
+ * {@link Bitmap} objects.
+ *
+ * <p>To use it, first create a {@link Source Source} using one of the
+ * {@code createSource} overloads. For example, to decode from a {@link File}, call
+ * {@link #createSource(File)} and pass the result to {@link #decodeDrawable(Source)}
+ * or {@link #decodeBitmap(Source)}:
+ *
+ * <pre class="prettyprint">
+ * File file = new File(...);
+ * ImageDecoder.Source source = ImageDecoder.createSource(file);
+ * Drawable drawable = ImageDecoder.decodeDrawable(source);
+ * </pre>
+ *
+ * <p>To change the default settings, pass the {@link Source Source} and an
+ * {@link OnHeaderDecodedListener OnHeaderDecodedListener} to
+ * {@link #decodeDrawable(Source, OnHeaderDecodedListener)} or
+ * {@link #decodeBitmap(Source, OnHeaderDecodedListener)}. For example, to
+ * create a sampled image with half the width and height of the original image,
+ * call {@link #setTargetSampleSize setTargetSampleSize(2)} inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}:
+ *
+ * <pre class="prettyprint">
+ * OnHeaderDecodedListener listener = new OnHeaderDecodedListener() {
+ * public void onHeaderDecoded(ImageDecoder decoder, ImageInfo info, Source source) {
+ * decoder.setTargetSampleSize(2);
+ * }
+ * };
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, listener);
+ * </pre>
+ *
+ * <p>The {@link ImageInfo ImageInfo} contains information about the encoded image, like
+ * its width and height, and the {@link Source Source} can be used to match to a particular
+ * {@link Source Source} if a single {@link OnHeaderDecodedListener OnHeaderDecodedListener}
+ * is used with multiple {@link Source Source} objects.
+ *
+ * <p>The {@link OnHeaderDecodedListener OnHeaderDecodedListener} can also be implemented
+ * as a lambda:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ * decoder.setTargetSampleSize(2);
+ * });
+ * </pre>
+ *
+ * <p>If the encoded image is an animated {@code GIF} or {@code WEBP},
+ * {@link #decodeDrawable decodeDrawable} will return an {@link AnimatedImageDrawable}. To
+ * start its animation, call {@link AnimatedImageDrawable#start AnimatedImageDrawable.start()}:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source);
+ * if (drawable instanceof AnimatedImageDrawable) {
+ * ((AnimatedImageDrawable) drawable).start();
+ * }
+ * </pre>
+ *
+ * <p>By default, a {@link Bitmap} created by {@link ImageDecoder} (including
+ * one that is inside a {@link Drawable}) will be immutable (i.e.
+ * {@link Bitmap#isMutable Bitmap.isMutable()} returns {@code false}), and it
+ * will typically have {@code Config} {@link Bitmap.Config#HARDWARE}. Although
+ * these properties can be changed with {@link #setMutableRequired setMutableRequired(true)}
+ * (which is only compatible with {@link #decodeBitmap(Source)} and
+ * {@link #decodeBitmap(Source, OnHeaderDecodedListener)}) and {@link #setAllocator},
+ * it is also possible to apply custom effects regardless of the mutability of
+ * the final returned object by passing a {@link PostProcessor} to
+ * {@link #setPostProcessor setPostProcessor}. A {@link PostProcessor} can also be a lambda:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ * decoder.setPostProcessor((canvas) -&gt; {
+ * // This will create rounded corners.
+ * Path path = new Path();
+ * path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ * int width = canvas.getWidth();
+ * int height = canvas.getHeight();
+ * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
+ * Paint paint = new Paint();
+ * paint.setAntiAlias(true);
+ * paint.setColor(Color.TRANSPARENT);
+ * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ * canvas.drawPath(path, paint);
+ * return PixelFormat.TRANSLUCENT;
+ * });
+ * });
+ * </pre>
+ *
+ * <p>If the encoded image is incomplete or contains an error, or if an
+ * {@link Exception} occurs during decoding, a {@link DecodeException DecodeException}
+ * will be thrown. In some cases, the {@link ImageDecoder} may have decoded part of
+ * the image. In order to display the partial image, an
+ * {@link OnPartialImageListener OnPartialImageListener} must be passed to
+ * {@link #setOnPartialImageListener setOnPartialImageListener}. For example:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ * decoder.setOnPartialImageListener((DecodeException e) -&gt; {
+ * // Returning true indicates to create a Drawable or Bitmap even
+ * // if the whole image could not be decoded. Any remaining lines
+ * // will be blank.
+ * return true;
+ * });
+ * });
+ * </pre>
*/
public final class ImageDecoder implements AutoCloseable {
+ /** @hide **/
+ public static int sApiLevel;
/**
- * Source of the encoded image data.
+ * Source of encoded image data.
+ *
+ * <p>References the data that will be used to decode a {@link Drawable}
+ * or {@link Bitmap} in {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}. Constructing a {@code Source} (with
+ * one of the overloads of {@code createSource}) can be done on any thread
+ * because the construction simply captures values. The real work is done
+ * in {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}.
+ *
+ * <p>A {@code Source} object can be reused to create multiple versions of the
+ * same image. For example, to decode a full size image and its thumbnail,
+ * the same {@code Source} can be used once with no
+ * {@link OnHeaderDecodedListener OnHeaderDecodedListener} and once with an
+ * implementation of {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}
+ * that calls {@link #setTargetSize} with smaller dimensions. One {@code Source}
+ * even used simultaneously in multiple threads.</p>
*/
public static abstract class Source {
private Source() {}
@@ -58,7 +199,7 @@ public final class ImageDecoder implements AutoCloseable {
int getDensity() { return Bitmap.DENSITY_NONE; }
/* @hide */
- int computeDstDensity() {
+ final int computeDstDensity() {
Resources res = getResources();
if (res == null) {
return Bitmap.getDefaultDensity();
@@ -84,7 +225,7 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ return nCreate(mData, mOffset, mLength, this);
}
}
@@ -96,27 +237,126 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ if (!mBuffer.isDirect() && mBuffer.hasArray()) {
+ int offset = mBuffer.arrayOffset() + mBuffer.position();
+ int length = mBuffer.limit() - mBuffer.position();
+ return nCreate(mBuffer.array(), offset, length, this);
+ }
+ ByteBuffer buffer = mBuffer.slice();
+ return nCreate(buffer, buffer.position(), buffer.limit(), this);
}
}
private static class ContentResolverSource extends Source {
- ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri) {
+ ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri,
+ @Nullable Resources res) {
mResolver = resolver;
mUri = uri;
+ mResources = res;
}
private final ContentResolver mResolver;
private final Uri mUri;
+ private final Resources mResources;
+
+ @Nullable
+ Resources getResources() { return mResources; }
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ AssetFileDescriptor assetFd = null;
+ try {
+ if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) {
+ assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
+ "image/*", null);
+ } else {
+ assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
+ }
+ } catch (FileNotFoundException e) {
+ // Some images cannot be opened as AssetFileDescriptors (e.g.
+ // bmp, ico). Open them as InputStreams.
+ InputStream is = mResolver.openInputStream(mUri);
+ if (is == null) {
+ throw new FileNotFoundException(mUri.toString());
+ }
+
+ return createFromStream(is, true, this);
+ }
+
+ final FileDescriptor fd = assetFd.getFileDescriptor();
+ final long offset = assetFd.getStartOffset();
+
+ ImageDecoder decoder = null;
+ try {
+ try {
+ Os.lseek(fd, offset, SEEK_SET);
+ decoder = nCreate(fd, this);
+ } catch (ErrnoException e) {
+ decoder = createFromStream(new FileInputStream(fd), true, this);
+ }
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(assetFd);
+ } else {
+ decoder.mAssetFd = assetFd;
+ }
+ }
+ return decoder;
+ }
+ }
+
+ @NonNull
+ private static ImageDecoder createFromFile(@NonNull File file,
+ @NonNull Source source) throws IOException {
+ FileInputStream stream = new FileInputStream(file);
+ FileDescriptor fd = stream.getFD();
+ try {
+ Os.lseek(fd, 0, SEEK_CUR);
+ } catch (ErrnoException e) {
+ return createFromStream(stream, true, source);
}
+
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(fd, source);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(stream);
+ } else {
+ decoder.mInputStream = stream;
+ decoder.mOwnsInputStream = true;
+ }
+ }
+ return decoder;
+ }
+
+ @NonNull
+ private static ImageDecoder createFromStream(@NonNull InputStream is,
+ boolean closeInputStream, Source source) throws IOException {
+ // Arbitrary size matches BitmapFactory.
+ byte[] storage = new byte[16 * 1024];
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(is, storage, source);
+ } finally {
+ if (decoder == null) {
+ if (closeInputStream) {
+ IoUtils.closeQuietly(is);
+ }
+ } else {
+ decoder.mInputStream = is;
+ decoder.mOwnsInputStream = closeInputStream;
+ decoder.mTempStorage = storage;
+ }
+ }
+
+ return decoder;
}
/**
* For backwards compatibility, this does *not* close the InputStream.
+ *
+ * Further, unlike other Sources, this one is not reusable.
*/
private static class InputStreamSource extends Source {
InputStreamSource(Resources res, InputStream is, int inputDensity) {
@@ -140,7 +380,15 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+
+ synchronized (this) {
+ if (mInputStream == null) {
+ throw new IOException("Cannot reuse InputStreamSource");
+ }
+ InputStream is = mInputStream;
+ mInputStream = null;
+ return createFromStream(is, false, this);
+ }
}
}
@@ -178,7 +426,14 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ synchronized (this) {
+ if (mAssetInputStream == null) {
+ throw new IOException("Cannot reuse AssetInputStreamSource");
+ }
+ AssetInputStream ais = mAssetInputStream;
+ mAssetInputStream = null;
+ return createFromAsset(ais, this);
+ }
}
}
@@ -192,16 +447,70 @@ public final class ImageDecoder implements AutoCloseable {
final Resources mResources;
final int mResId;
int mResDensity;
+ private Object mLock = new Object();
@Override
public Resources getResources() { return mResources; }
@Override
- public int getDensity() { return mResDensity; }
+ public int getDensity() {
+ synchronized (mLock) {
+ return mResDensity;
+ }
+ }
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ TypedValue value = new TypedValue();
+ // This is just used in order to access the underlying Asset and
+ // keep it alive.
+ InputStream is = mResources.openRawResource(mResId, value);
+
+ synchronized (mLock) {
+ if (value.density == TypedValue.DENSITY_DEFAULT) {
+ mResDensity = DisplayMetrics.DENSITY_DEFAULT;
+ } else if (value.density != TypedValue.DENSITY_NONE) {
+ mResDensity = value.density;
+ }
+ }
+
+ return createFromAsset((AssetInputStream) is, this);
+ }
+ }
+
+ /**
+ * ImageDecoder will own the AssetInputStream.
+ */
+ private static ImageDecoder createFromAsset(AssetInputStream ais,
+ Source source) throws IOException {
+ ImageDecoder decoder = null;
+ try {
+ long asset = ais.getNativeAsset();
+ decoder = nCreate(asset, source);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(ais);
+ } else {
+ decoder.mInputStream = ais;
+ decoder.mOwnsInputStream = true;
+ }
+ }
+ return decoder;
+ }
+
+ private static class AssetSource extends Source {
+ AssetSource(@NonNull AssetManager assets, @NonNull String fileName) {
+ mAssets = assets;
+ mFileName = fileName;
+ }
+
+ private final AssetManager mAssets;
+ private final String mFileName;
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ InputStream is = mAssets.open(mFileName);
+ return createFromAsset((AssetInputStream) is, this);
}
}
@@ -214,17 +523,19 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ return createFromFile(mFile, this);
}
}
/**
- * Contains information about the encoded image.
+ * Information about an encoded image.
*/
public static class ImageInfo {
+ private final Size mSize;
private ImageDecoder mDecoder;
private ImageInfo(@NonNull ImageDecoder decoder) {
+ mSize = new Size(decoder.mWidth, decoder.mHeight);
mDecoder = decoder;
}
@@ -233,7 +544,7 @@ public final class ImageDecoder implements AutoCloseable {
*/
@NonNull
public Size getSize() {
- return new Size(0, 0);
+ return mSize;
}
/**
@@ -241,100 +552,271 @@ public final class ImageDecoder implements AutoCloseable {
*/
@NonNull
public String getMimeType() {
- return "";
+ return mDecoder.getMimeType();
}
/**
* Whether the image is animated.
*
- * <p>Calling {@link #decodeDrawable} will return an
- * {@link AnimatedImageDrawable}.</p>
+ * <p>If {@code true}, {@link #decodeDrawable decodeDrawable} will
+ * return an {@link AnimatedImageDrawable}.</p>
*/
public boolean isAnimated() {
return mDecoder.mAnimated;
}
+
+ /**
+ * If known, the color space the decoded bitmap will have. Note that the
+ * output color space is not guaranteed to be the color space the bitmap
+ * is encoded with. If not known (when the config is
+ * {@link Bitmap.Config#ALPHA_8} for instance), or there is an error,
+ * it is set to null.
+ */
+ @Nullable
+ public ColorSpace getColorSpace() {
+ return mDecoder.getColorSpace();
+ }
};
- /**
- * Thrown if the provided data is incomplete.
+ /** @removed
+ * @deprecated Subsumed by {@link #DecodeException}.
*/
+ @Deprecated
public static class IncompleteException extends IOException {};
/**
- * Optional listener supplied to {@link #decodeDrawable} or
- * {@link #decodeBitmap}.
+ * Interface for changing the default settings of a decode.
+ *
+ * <p>Supply an instance to
+ * {@link #decodeDrawable(Source, OnHeaderDecodedListener) decodeDrawable}
+ * or {@link #decodeBitmap(Source, OnHeaderDecodedListener) decodeBitmap},
+ * which will call {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}
+ * (in the same thread) once the size is known. The implementation of
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} can then
+ * change the decode settings as desired.
*/
- public interface OnHeaderDecodedListener {
+ public static interface OnHeaderDecodedListener {
/**
- * Called when the header is decoded and the size is known.
+ * Called by {@link ImageDecoder} when the header has been decoded and
+ * the image size is known.
*
- * @param decoder allows changing the default settings of the decode.
- * @param info Information about the encoded image.
- * @param source that created the decoder.
+ * @param decoder the object performing the decode, for changing
+ * its default settings.
+ * @param info information about the encoded image.
+ * @param source object that created {@code decoder}.
*/
- void onHeaderDecoded(@NonNull ImageDecoder decoder,
+ public void onHeaderDecoded(@NonNull ImageDecoder decoder,
@NonNull ImageInfo info, @NonNull Source source);
};
- /**
- * An Exception was thrown reading the {@link Source}.
+ /** @removed
+ * @deprecated Replaced by {@link #DecodeException#SOURCE_EXCEPTION}.
*/
+ @Deprecated
public static final int ERROR_SOURCE_EXCEPTION = 1;
- /**
- * The encoded data was incomplete.
+ /** @removed
+ * @deprecated Replaced by {@link #DecodeException#SOURCE_INCOMPLETE}.
*/
+ @Deprecated
public static final int ERROR_SOURCE_INCOMPLETE = 2;
- /**
- * The encoded data contained an error.
+ /** @removed
+ * @deprecated Replaced by {@link #DecodeException#SOURCE_MALFORMED_DATA}.
*/
+ @Deprecated
public static final int ERROR_SOURCE_ERROR = 3;
- @Retention(SOURCE)
- public @interface Error {}
+ /**
+ * Information about an interrupted decode.
+ */
+ public static final class DecodeException extends IOException {
+ /**
+ * An Exception was thrown reading the {@link Source}.
+ */
+ public static final int SOURCE_EXCEPTION = 1;
+
+ /**
+ * The encoded data was incomplete.
+ */
+ public static final int SOURCE_INCOMPLETE = 2;
+
+ /**
+ * The encoded data contained an error.
+ */
+ public static final int SOURCE_MALFORMED_DATA = 3;
+
+ /** @hide **/
+ @Retention(SOURCE)
+ @IntDef(value = { SOURCE_EXCEPTION, SOURCE_INCOMPLETE, SOURCE_MALFORMED_DATA },
+ prefix = {"SOURCE_"})
+ public @interface Error {};
+
+ @Error final int mError;
+ @NonNull final Source mSource;
+
+ DecodeException(@Error int error, @Nullable Throwable cause, @NonNull Source source) {
+ super(errorMessage(error, cause), cause);
+ mError = error;
+ mSource = source;
+ }
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ DecodeException(@Error int error, @Nullable String msg, @Nullable Throwable cause,
+ @NonNull Source source) {
+ super(msg + errorMessage(error, cause), cause);
+ mError = error;
+ mSource = source;
+ }
+
+ /**
+ * Retrieve the reason that decoding was interrupted.
+ *
+ * <p>If the error is {@link #SOURCE_EXCEPTION}, the underlying
+ * {@link java.lang.Throwable} can be retrieved with
+ * {@link java.lang.Throwable#getCause}.</p>
+ */
+ @Error
+ public int getError() {
+ return mError;
+ }
+
+ /**
+ * Retrieve the {@link Source Source} that was interrupted.
+ *
+ * <p>This can be used for equality checking to find the Source which
+ * failed to completely decode.</p>
+ */
+ @NonNull
+ public Source getSource() {
+ return mSource;
+ }
+
+ private static String errorMessage(@Error int error, @Nullable Throwable cause) {
+ switch (error) {
+ case SOURCE_EXCEPTION:
+ return "Exception in input: " + cause;
+ case SOURCE_INCOMPLETE:
+ return "Input was incomplete.";
+ case SOURCE_MALFORMED_DATA:
+ return "Input contained an error.";
+ default:
+ return "";
+ }
+ }
+ }
/**
- * Optional listener supplied to the ImageDecoder.
+ * Interface for inspecting a {@link DecodeException DecodeException}
+ * and potentially preventing it from being thrown.
*
- * Without this listener, errors will throw {@link java.io.IOException}.
+ * <p>If an instance is passed to
+ * {@link #setOnPartialImageListener setOnPartialImageListener}, a
+ * {@link DecodeException DecodeException} that would otherwise have been
+ * thrown can be inspected inside
+ * {@link OnPartialImageListener#onPartialImage onPartialImage}.
+ * If {@link OnPartialImageListener#onPartialImage onPartialImage} returns
+ * {@code true}, a partial image will be created.
*/
- public interface OnPartialImageListener {
+ public static interface OnPartialImageListener {
/**
- * Called when there is only a partial image to display.
+ * Called by {@link ImageDecoder} when there is only a partial image to
+ * display.
*
- * If decoding is interrupted after having decoded a partial image,
- * this listener lets the client know that and allows them to
- * optionally finish the rest of the decode/creation process to create
- * a partial {@link Drawable}/{@link Bitmap}.
+ * <p>If decoding is interrupted after having decoded a partial image,
+ * this method will be called. The implementation can inspect the
+ * {@link DecodeException DecodeException} and optionally finish the
+ * rest of the decode creation process to create a partial {@link Drawable}
+ * or {@link Bitmap}.
*
- * @param error indicating what interrupted the decode.
- * @param source that had the error.
- * @return True to create and return a {@link Drawable}/{@link Bitmap}
- * with partial data. False (which is the default) to abort the
- * decode and throw {@link java.io.IOException}.
+ * @param exception exception containing information about the
+ * decode interruption.
+ * @return {@code true} to create and return a {@link Drawable} or
+ * {@link Bitmap} with partial data. {@code false} (which is the
+ * default) to abort the decode and throw {@code e}. Any undecoded
+ * lines in the image will be blank.
*/
- boolean onPartialImage(@Error int error, @NonNull Source source);
+ boolean onPartialImage(@NonNull DecodeException exception);
+ };
+
+ // Fields
+ private long mNativePtr;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mAnimated;
+ private final boolean mIsNinePatch;
+
+ private int mDesiredWidth;
+ private int mDesiredHeight;
+ private int mAllocator = ALLOCATOR_DEFAULT;
+ private boolean mUnpremultipliedRequired = false;
+ private boolean mMutable = false;
+ private boolean mConserveMemory = false;
+ private boolean mDecodeAsAlphaMask = false;
+ private ColorSpace mDesiredColorSpace = null;
+ private Rect mCropRect;
+ private Rect mOutPaddingRect;
+ private Source mSource;
+
+ private PostProcessor mPostProcessor;
+ private OnPartialImageListener mOnPartialImageListener;
+
+ // Objects for interacting with the input.
+ private InputStream mInputStream;
+ private boolean mOwnsInputStream;
+ private byte[] mTempStorage;
+ private AssetFileDescriptor mAssetFd;
+ private final AtomicBoolean mClosed = new AtomicBoolean();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ /**
+ * Private constructor called by JNI. {@link #close} must be
+ * called after decoding to delete native resources.
+ */
+ @SuppressWarnings("unused")
+ private ImageDecoder(long nativePtr, int width, int height,
+ boolean animated, boolean isNinePatch) {
+ mNativePtr = nativePtr;
+ mWidth = width;
+ mHeight = height;
+ mDesiredWidth = width;
+ mDesiredHeight = height;
+ mAnimated = animated;
+ mIsNinePatch = isNinePatch;
+ mCloseGuard.open("close");
}
- private boolean mAnimated;
- private Rect mOutPaddingRect;
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ // Avoid closing these in finalizer.
+ mInputStream = null;
+ mAssetFd = null;
- public ImageDecoder() {
- mAnimated = true; // This is too avoid throwing an exception in AnimatedImageDrawable
+ close();
+ } finally {
+ super.finalize();
+ }
}
/**
- * Create a new {@link Source} from an asset.
- * @hide
+ * Create a new {@link Source Source} from a resource.
*
* @param res the {@link Resources} object containing the image data.
* @param resId resource ID of the image data.
- * // FIXME: Can be an @DrawableRes?
* @return a new Source object, which can be passed to
- * {@link #decodeDrawable} or {@link #decodeBitmap}.
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull Resources res, int resId)
{
@@ -342,34 +824,71 @@ public final class ImageDecoder implements AutoCloseable {
}
/**
- * Create a new {@link Source} from a {@link android.net.Uri}.
+ * Create a new {@link Source Source} from a {@link android.net.Uri}.
+ *
+ * <h5>Accepts the following URI schemes:</h5>
+ * <ul>
+ * <li>content ({@link ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link ContentResolver#SCHEME_FILE})</li>
+ * </ul>
*
* @param cr to retrieve from.
* @param uri of the image file.
* @return a new Source object, which can be passed to
- * {@link #decodeDrawable} or {@link #decodeBitmap}.
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull ContentResolver cr,
@NonNull Uri uri) {
- return new ContentResolverSource(cr, uri);
+ return new ContentResolverSource(cr, uri, null);
+ }
+
+ /**
+ * Provide Resources for density scaling.
+ *
+ * @hide
+ */
+ @AnyThread
+ @NonNull
+ public static Source createSource(@NonNull ContentResolver cr,
+ @NonNull Uri uri, @Nullable Resources res) {
+ return new ContentResolverSource(cr, uri, res);
+ }
+
+ /**
+ * Create a new {@link Source Source} from a file in the "assets" directory.
+ */
+ @AnyThread
+ @NonNull
+ public static Source createSource(@NonNull AssetManager assets, @NonNull String fileName) {
+ return new AssetSource(assets, fileName);
}
/**
- * Create a new {@link Source} from a byte array.
+ * Create a new {@link Source Source} from a byte array.
*
* @param data byte array of compressed image data.
* @param offset offset into data for where the decoder should begin
* parsing.
* @param length number of bytes, beginning at offset, to parse.
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
* @throws NullPointerException if data is null.
* @throws ArrayIndexOutOfBoundsException if offset and length are
* not within data.
* @hide
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull byte[] data, int offset,
int length) throws ArrayIndexOutOfBoundsException {
+ if (data == null) {
+ throw new NullPointerException("null byte[] in createSource!");
+ }
if (offset < 0 || length < 0 || offset >= data.length ||
offset + length > data.length) {
throw new ArrayIndexOutOfBoundsException(
@@ -382,21 +901,29 @@ public final class ImageDecoder implements AutoCloseable {
* See {@link #createSource(byte[], int, int).
* @hide
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull byte[] data) {
return createSource(data, 0, data.length);
}
/**
- * Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
+ * Create a new {@link Source Source} from a {@link java.nio.ByteBuffer}.
+ *
+ * <p>Decoding will start from {@link java.nio.ByteBuffer#position() buffer.position()}.
+ * The position of {@code buffer} will not be affected.</p>
*
- * <p>The returned {@link Source} effectively takes ownership of the
- * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
- * this call.</p>
+ * <p>Note: If this {@code Source} is passed to {@link #decodeDrawable decodeDrawable},
+ * and the encoded image is animated, the returned {@link AnimatedImageDrawable}
+ * will continue reading from the {@code buffer}, so its contents must not
+ * be modified, even after the {@code AnimatedImageDrawable} is returned.
+ * {@code buffer}'s contents should never be modified during decode.</p>
*
- * Decoding will start from {@link java.nio.ByteBuffer#position()}. The
- * position after decoding is undefined.
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull ByteBuffer buffer) {
return new ByteBufferSource(buffer);
@@ -404,23 +931,39 @@ public final class ImageDecoder implements AutoCloseable {
/**
* Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ *
+ * <p>Unlike other Sources, this one cannot be reused.</p>
+ *
* @hide
*/
+ @AnyThread
+ @NonNull
public static Source createSource(Resources res, InputStream is) {
return new InputStreamSource(res, is, Bitmap.getDefaultDensity());
}
/**
* Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ *
+ * <p>Unlike other Sources, this one cannot be reused.</p>
+ *
* @hide
*/
+ @AnyThread
+ @TestApi
+ @NonNull
public static Source createSource(Resources res, InputStream is, int density) {
return new InputStreamSource(res, is, density);
}
/**
- * Create a new {@link Source} from a {@link java.io.File}.
+ * Create a new {@link Source Source} from a {@link java.io.File}.
+ *
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull File file) {
return new FileSource(file);
@@ -431,39 +974,142 @@ public final class ImageDecoder implements AutoCloseable {
*
* <p>This takes an input that functions like
* {@link BitmapFactory.Options#inSampleSize}. It returns a width and
- * height that can be acheived by sampling the encoded image. Other widths
+ * height that can be achieved by sampling the encoded image. Other widths
* and heights may be supported, but will require an additional (internal)
* scaling step. Such internal scaling is *not* supported with
- * {@link #setRequireUnpremultiplied} set to {@code true}.</p>
+ * {@link #setUnpremultipliedRequired} set to {@code true}.</p>
*
* @param sampleSize Sampling rate of the encoded image.
* @return {@link android.util.Size} of the width and height after
* sampling.
+ *
+ * @hide
*/
@NonNull
public Size getSampledSize(int sampleSize) {
- return new Size(0, 0);
+ if (sampleSize <= 0) {
+ throw new IllegalArgumentException("sampleSize must be positive! "
+ + "provided " + sampleSize);
+ }
+ if (mNativePtr == 0) {
+ throw new IllegalStateException("ImageDecoder is closed!");
+ }
+
+ return nGetSampledSize(mNativePtr, sampleSize);
}
// Modifiers
+ /** @removed
+ * @deprecated Renamed to {@link #setTargetSize}.
+ */
+ @Deprecated
+ public ImageDecoder setResize(int width, int height) {
+ this.setTargetSize(width, height);
+ return this;
+ }
+
/**
- * Resize the output to have the following size.
+ * Specify the size of the output {@link Drawable} or {@link Bitmap}.
+ *
+ * <p>By default, the output size will match the size of the encoded
+ * image, which can be retrieved from the {@link ImageInfo ImageInfo} in
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
+ * <p>This will sample or scale the output to an arbitrary size that may
+ * be smaller or larger than the encoded size.</p>
+ *
+ * <p>Only the last call to this or {@link #setTargetSampleSize} is
+ * respected.</p>
*
- * @param width must be greater than 0.
- * @param height must be greater than 0.
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
+ * @param width width in pixels of the output, must be greater than 0
+ * @param height height in pixels of the output, must be greater than 0
+ */
+ public void setTargetSize(@Px @IntRange(from = 1) int width,
+ @Px @IntRange(from = 1) int height) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Dimensions must be positive! "
+ + "provided (" + width + ", " + height + ")");
+ }
+
+ mDesiredWidth = width;
+ mDesiredHeight = height;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setTargetSampleSize}.
*/
- public void setResize(int width, int height) {
+ @Deprecated
+ public ImageDecoder setResize(int sampleSize) {
+ this.setTargetSampleSize(sampleSize);
+ return this;
+ }
+
+ private int getTargetDimension(int original, int sampleSize, int computed) {
+ // Sampling will never result in a smaller size than 1.
+ if (sampleSize >= original) {
+ return 1;
+ }
+
+ // Use integer divide to find the desired size. If that is what
+ // getSampledSize computed, that is the size to use.
+ int target = original / sampleSize;
+ if (computed == target) {
+ return computed;
+ }
+
+ // If sampleSize does not divide evenly into original, the decoder
+ // may round in either direction. It just needs to get a result that
+ // is close.
+ int reverse = computed * sampleSize;
+ if (Math.abs(reverse - original) < sampleSize) {
+ // This is the size that can be decoded most efficiently.
+ return computed;
+ }
+
+ // The decoder could not get close (e.g. it is a DNG image).
+ return target;
}
/**
- * Resize based on a sample size.
+ * Set the target size with a sampleSize.
*
- * <p>This has the same effect as passing the result of
- * {@link #getSampledSize} to {@link #setResize(int, int)}.</p>
+ * <p>By default, the output size will match the size of the encoded
+ * image, which can be retrieved from the {@link ImageInfo ImageInfo} in
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
*
- * @param sampleSize Sampling rate of the encoded image.
+ * <p>Requests the decoder to subsample the original image, returning a
+ * smaller image to save memory. The {@code sampleSize} is the number of pixels
+ * in either dimension that correspond to a single pixel in the output.
+ * For example, {@code sampleSize == 4} returns an image that is 1/4 the
+ * width/height of the original, and 1/16 the number of pixels.</p>
+ *
+ * <p>Must be greater than or equal to 1.</p>
+ *
+ * <p>This has the same effect as calling {@link #setTargetSize} with
+ * dimensions based on the {@code sampleSize}. Unlike dividing the original
+ * width and height by the {@code sampleSize} manually, calling this method
+ * allows {@code ImageDecoder} to round in the direction that it can do most
+ * efficiently.</p>
+ *
+ * <p>Only the last call to this or {@link #setTargetSize} is respected.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
+ * @param sampleSize sampling rate of the encoded image.
*/
- public void setResize(int sampleSize) {
+ public void setTargetSampleSize(@IntRange(from = 1) int sampleSize) {
+ Size size = this.getSampledSize(sampleSize);
+ int targetWidth = getTargetDimension(mWidth, sampleSize, size.getWidth());
+ int targetHeight = getTargetDimension(mHeight, sampleSize, size.getHeight());
+ this.setTargetSize(targetWidth, targetHeight);
+ }
+
+ private boolean requestedResize() {
+ return mWidth != mDesiredWidth || mHeight != mDesiredHeight;
}
// These need to stay in sync with ImageDecoder.cpp's Allocator enum.
@@ -473,14 +1119,15 @@ public final class ImageDecoder implements AutoCloseable {
* Will typically result in a {@link Bitmap.Config#HARDWARE}
* allocation, but may be software for small images. In addition, this will
* switch to software when HARDWARE is incompatible, e.g.
- * {@link #setMutable}, {@link #setAsAlphaMask}.
+ * {@link #setMutableRequired setMutableRequired(true)} or
+ * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)}.
*/
public static final int ALLOCATOR_DEFAULT = 0;
/**
* Use a software allocation for the pixel memory.
*
- * Useful for drawing to a software {@link Canvas} or for
+ * <p>Useful for drawing to a software {@link Canvas} or for
* accessing the pixels on the final output.
*/
public static final int ALLOCATOR_SOFTWARE = 1;
@@ -488,92 +1135,177 @@ public final class ImageDecoder implements AutoCloseable {
/**
* Use shared memory for the pixel memory.
*
- * Useful for sharing across processes.
+ * <p>Useful for sharing across processes.
*/
public static final int ALLOCATOR_SHARED_MEMORY = 2;
/**
* Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}.
*
- * When this is combined with incompatible options, like
- * {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable}
- * / {@link #decodeBitmap} will throw an
- * {@link java.lang.IllegalStateException}.
+ * <p>When this is combined with incompatible options, like
+ * {@link #setMutableRequired setMutableRequired(true)} or
+ * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)},
+ * {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}
+ * will throw an {@link java.lang.IllegalStateException}.
*/
public static final int ALLOCATOR_HARDWARE = 3;
/** @hide **/
@Retention(SOURCE)
+ @IntDef(value = { ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE,
+ ALLOCATOR_SHARED_MEMORY, ALLOCATOR_HARDWARE },
+ prefix = {"ALLOCATOR_"})
public @interface Allocator {};
/**
* Choose the backing for the pixel memory.
*
- * This is ignored for animated drawables.
+ * <p>This is ignored for animated drawables.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
*
* @param allocator Type of allocator to use.
*/
- public ImageDecoder setAllocator(@Allocator int allocator) {
- return this;
+ public void setAllocator(@Allocator int allocator) {
+ if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) {
+ throw new IllegalArgumentException("invalid allocator " + allocator);
+ }
+ mAllocator = allocator;
+ }
+
+ /**
+ * Return the allocator for the pixel memory.
+ */
+ @Allocator
+ public int getAllocator() {
+ return mAllocator;
}
/**
* Specify whether the {@link Bitmap} should have unpremultiplied pixels.
*
- * By default, ImageDecoder will create a {@link Bitmap} with
+ * <p>By default, ImageDecoder will create a {@link Bitmap} with
* premultiplied pixels, which is required for drawing with the
* {@link android.view.View} system (i.e. to a {@link Canvas}). Calling
* this method with a value of {@code true} will result in
* {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied
- * pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with
- * {@link #decodeDrawable}; attempting to decode an unpremultiplied
- * {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
+ * pixels. See {@link Bitmap#isPremultiplied Bitmap.isPremultiplied()}.
+ * This is incompatible with {@link #decodeDrawable decodeDrawable};
+ * attempting to decode an unpremultiplied {@link Drawable} will throw an
+ * {@link java.lang.IllegalStateException}. </p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setUnpremultipliedRequired(boolean unpremultipliedRequired) {
+ mUnpremultipliedRequired = unpremultipliedRequired;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setUnpremultipliedRequired}.
*/
- public ImageDecoder setRequireUnpremultiplied(boolean requireUnpremultiplied) {
+ @Deprecated
+ public ImageDecoder setRequireUnpremultiplied(boolean unpremultipliedRequired) {
+ this.setUnpremultipliedRequired(unpremultipliedRequired);
return this;
}
/**
+ * Return whether the {@link Bitmap} will have unpremultiplied pixels.
+ */
+ public boolean isUnpremultipliedRequired() {
+ return mUnpremultipliedRequired;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isUnpremultipliedRequired}.
+ */
+ @Deprecated
+ public boolean getRequireUnpremultiplied() {
+ return this.isUnpremultipliedRequired();
+ }
+
+ /**
* Modify the image after decoding and scaling.
*
* <p>This allows adding effects prior to returning a {@link Drawable} or
* {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap},
* this is the only way to process the image after decoding.</p>
*
+ * <p>If combined with {@link #setTargetSize} and/or {@link #setCrop},
+ * {@link PostProcessor#onPostProcess} occurs last.</p>
+ *
* <p>If set on a nine-patch image, the nine-patch data is ignored.</p>
*
* <p>For an animated image, the drawing commands drawn on the
* {@link Canvas} will be recorded immediately and then applied to each
* frame.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
*/
- public ImageDecoder setPostProcessor(@Nullable PostProcessor p) {
- return this;
+ public void setPostProcessor(@Nullable PostProcessor postProcessor) {
+ mPostProcessor = postProcessor;
+ }
+
+ /**
+ * Return the {@link PostProcessor} currently set.
+ */
+ @Nullable
+ public PostProcessor getPostProcessor() {
+ return mPostProcessor;
}
/**
* Set (replace) the {@link OnPartialImageListener} on this object.
*
- * Will be called if there is an error in the input. Without one, a
- * partial {@link Bitmap} will be created.
+ * <p>Will be called if there is an error in the input. Without one, an
+ * error will result in an {@code Exception} being thrown.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
*/
- public ImageDecoder setOnPartialImageListener(@Nullable OnPartialImageListener l) {
- return this;
+ public void setOnPartialImageListener(@Nullable OnPartialImageListener listener) {
+ mOnPartialImageListener = listener;
+ }
+
+ /**
+ * Return the {@link OnPartialImageListener OnPartialImageListener} currently set.
+ */
+ @Nullable
+ public OnPartialImageListener getOnPartialImageListener() {
+ return mOnPartialImageListener;
}
/**
* Crop the output to {@code subset} of the (possibly) scaled image.
*
* <p>{@code subset} must be contained within the size set by
- * {@link #setResize} or the bounds of the image if setResize was not
- * called. Otherwise an {@link IllegalStateException} will be thrown by
- * {@link #decodeDrawable}/{@link #decodeBitmap}.</p>
+ * {@link #setTargetSize} or the bounds of the image if setTargetSize was
+ * not called. Otherwise an {@link IllegalStateException} will be thrown by
+ * {@link #decodeDrawable decodeDrawable}/{@link #decodeBitmap decodeBitmap}.</p>
*
* <p>NOT intended as a replacement for
- * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
- * but merely crops the output.</p>
+ * {@link BitmapRegionDecoder#decodeRegion BitmapRegionDecoder.decodeRegion()}.
+ * This supports all formats, but merely crops the output.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
*/
- public ImageDecoder setCrop(@Nullable Rect subset) {
- return this;
+ public void setCrop(@Nullable Rect subset) {
+ mCropRect = subset;
+ }
+
+ /**
+ * Return the cropping rectangle, if set.
+ */
+ @Nullable
+ public Rect getCrop() {
+ return mCropRect;
}
/**
@@ -582,43 +1314,122 @@ public final class ImageDecoder implements AutoCloseable {
* If the image is a nine patch, this Rect will be set to the padding
* rectangle during decode. Otherwise it will not be modified.
*
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
* @hide
*/
- public ImageDecoder setOutPaddingRect(@NonNull Rect outPadding) {
+ public void setOutPaddingRect(@NonNull Rect outPadding) {
mOutPaddingRect = outPadding;
- return this;
}
/**
* Specify whether the {@link Bitmap} should be mutable.
*
- * <p>By default, a {@link Bitmap} created will be immutable, but that can
- * be changed with this call.</p>
+ * <p>By default, a {@link Bitmap} created by {@link #decodeBitmap decodeBitmap}
+ * will be immutable i.e. {@link Bitmap#isMutable() Bitmap.isMutable()} returns
+ * {@code false}. This can be changed with {@code setMutableRequired(true)}.
*
* <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE},
* because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable.
* Attempting to combine them will throw an
* {@link java.lang.IllegalStateException}.</p>
*
- * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable},
+ * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable decodeDrawable},
* which would require retrieving the Bitmap from the returned Drawable in
* order to modify. Attempting to decode a mutable {@link Drawable} will
* throw an {@link java.lang.IllegalStateException}.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setMutableRequired(boolean mutable) {
+ mMutable = mutable;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setMutableRequired}.
*/
+ @Deprecated
public ImageDecoder setMutable(boolean mutable) {
+ this.setMutableRequired(mutable);
return this;
}
/**
- * Specify whether to potentially save RAM at the expense of quality.
+ * Return whether the decoded {@link Bitmap} will be mutable.
+ */
+ public boolean isMutableRequired() {
+ return mMutable;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isMutableRequired}.
+ */
+ @Deprecated
+ public boolean getMutable() {
+ return this.isMutableRequired();
+ }
+
+ /**
+ * Save memory if possible by using a denser {@link Bitmap.Config} at the
+ * cost of some image quality.
*
- * Setting this to {@code true} may result in a {@link Bitmap} with a
- * denser {@link Bitmap.Config}, depending on the image. For example, for
- * an opaque {@link Bitmap}, this may result in a {@link Bitmap.Config}
- * with no alpha information.
+ * <p>For example an opaque 8-bit image may be compressed into an
+ * {@link Bitmap.Config#RGB_565} configuration, sacrificing image
+ * quality to save memory.
*/
- public ImageDecoder setPreferRamOverQuality(boolean preferRamOverQuality) {
- return this;
+ public static final int MEMORY_POLICY_LOW_RAM = 0;
+
+ /**
+ * Use the most natural {@link Bitmap.Config} for the internal {@link Bitmap}.
+ *
+ * <p>This is the recommended default for most applications and usages. This
+ * will use the closest {@link Bitmap.Config} for the encoded source. If the
+ * encoded source does not exactly match any {@link Bitmap.Config}, the next
+ * highest quality {@link Bitmap.Config} will be used avoiding any loss in
+ * image quality.
+ */
+ public static final int MEMORY_POLICY_DEFAULT = 1;
+
+ /** @hide **/
+ @Retention(SOURCE)
+ @IntDef(value = { MEMORY_POLICY_DEFAULT, MEMORY_POLICY_LOW_RAM },
+ prefix = {"MEMORY_POLICY_"})
+ public @interface MemoryPolicy {};
+
+ /**
+ * Specify the memory policy for the decoded {@link Bitmap}.
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setMemorySizePolicy(@MemoryPolicy int policy) {
+ mConserveMemory = (policy == MEMORY_POLICY_LOW_RAM);
+ }
+
+ /**
+ * Retrieve the memory policy for the decoded {@link Bitmap}.
+ */
+ @MemoryPolicy
+ public int getMemorySizePolicy() {
+ return mConserveMemory ? MEMORY_POLICY_LOW_RAM : MEMORY_POLICY_DEFAULT;
+ }
+
+ /** @removed
+ * @deprecated Replaced by {@link #setMemorySizePolicy}.
+ */
+ @Deprecated
+ public void setConserveMemory(boolean conserveMemory) {
+ mConserveMemory = conserveMemory;
+ }
+
+ /** @removed
+ * @deprecated Replaced by {@link #getMemorySizePolicy}.
+ */
+ @Deprecated
+ public boolean getConserveMemory() {
+ return mConserveMemory;
}
/**
@@ -628,77 +1439,467 @@ public final class ImageDecoder implements AutoCloseable {
* with only one channel, treat that channel as alpha. Otherwise this call has
* no effect.</p>
*
- * <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
- * combine them will result in {@link #decodeDrawable}/
- * {@link #decodeBitmap} throwing an
+ * <p>This is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
+ * combine them will result in {@link #decodeDrawable decodeDrawable}/
+ * {@link #decodeBitmap decodeBitmap} throwing an
* {@link java.lang.IllegalStateException}.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setDecodeAsAlphaMaskEnabled(boolean enabled) {
+ mDecodeAsAlphaMask = enabled;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}.
+ */
+ @Deprecated
+ public ImageDecoder setDecodeAsAlphaMask(boolean enabled) {
+ this.setDecodeAsAlphaMaskEnabled(enabled);
+ return this;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}.
*/
+ @Deprecated
public ImageDecoder setAsAlphaMask(boolean asAlphaMask) {
+ this.setDecodeAsAlphaMask(asAlphaMask);
return this;
}
+ /**
+ * Return whether to treat single channel input as alpha.
+ *
+ * <p>This returns whether {@link #setDecodeAsAlphaMaskEnabled} was set to
+ * {@code true}. It may still return {@code true} even if the image has
+ * more than one channel and therefore will not be treated as an alpha
+ * mask.</p>
+ */
+ public boolean isDecodeAsAlphaMaskEnabled() {
+ return mDecodeAsAlphaMask;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}.
+ */
+ @Deprecated
+ public boolean getDecodeAsAlphaMask() {
+ return mDecodeAsAlphaMask;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}.
+ */
+ @Deprecated
+ public boolean getAsAlphaMask() {
+ return this.getDecodeAsAlphaMask();
+ }
+
+ /**
+ * Specify the desired {@link ColorSpace} for the output.
+ *
+ * <p>If non-null, the decoder will try to decode into {@code colorSpace}.
+ * If it is null, which is the default, or the request cannot be met, the
+ * decoder will pick either the color space embedded in the image or the
+ * {@link ColorSpace} best suited for the requested image configuration
+ * (for instance {@link ColorSpace.Named#SRGB sRGB} for the
+ * {@link Bitmap.Config#ARGB_8888} configuration).</p>
+ *
+ * <p>{@link Bitmap.Config#RGBA_F16} always uses the
+ * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB scRGB} color space.
+ * Bitmaps in other configurations without an embedded color space are
+ * assumed to be in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
+ *
+ * <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are
+ * currently supported. An <code>IllegalArgumentException</code> will
+ * be thrown by {@link #decodeDrawable decodeDrawable}/
+ * {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space
+ * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
+ *
+ * <p class="note">The specified color space's transfer function must be
+ * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
+ * <code>IllegalArgumentException</code> will be thrown by the decode methods
+ * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
+ * specified color space returns null.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setTargetColorSpace(ColorSpace colorSpace) {
+ mDesiredColorSpace = colorSpace;
+ }
+
+ /**
+ * Closes this resource, relinquishing any underlying resources. This method
+ * is invoked automatically on objects managed by the try-with-resources
+ * statement.
+ *
+ * <p>This is an implementation detail of {@link ImageDecoder}, and should
+ * never be called manually.</p>
+ */
@Override
public void close() {
+ mCloseGuard.close();
+ if (!mClosed.compareAndSet(false, true)) {
+ return;
+ }
+ nClose(mNativePtr);
+ mNativePtr = 0;
+
+ if (mOwnsInputStream) {
+ IoUtils.closeQuietly(mInputStream);
+ }
+ IoUtils.closeQuietly(mAssetFd);
+
+ mInputStream = null;
+ mAssetFd = null;
+ mTempStorage = null;
+ }
+
+ private void checkState() {
+ if (mNativePtr == 0) {
+ throw new IllegalStateException("Cannot use closed ImageDecoder!");
+ }
+
+ checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
+
+ if (mAllocator == ALLOCATOR_HARDWARE) {
+ if (mMutable) {
+ throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
+ }
+ if (mDecodeAsAlphaMask) {
+ throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!");
+ }
+ }
+
+ if (mPostProcessor != null && mUnpremultipliedRequired) {
+ throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
+ }
+
+ if (mDesiredColorSpace != null) {
+ if (!(mDesiredColorSpace instanceof ColorSpace.Rgb)) {
+ throw new IllegalArgumentException("The target color space must use the "
+ + "RGB color model - provided: " + mDesiredColorSpace);
+ }
+ if (((ColorSpace.Rgb) mDesiredColorSpace).getTransferParameters() == null) {
+ throw new IllegalArgumentException("The target color space must use an "
+ + "ICC parametric transfer function - provided: " + mDesiredColorSpace);
+ }
+ }
+ }
+
+ private static void checkSubset(int width, int height, Rect r) {
+ if (r == null) {
+ return;
+ }
+ if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) {
+ throw new IllegalStateException("Subset " + r + " not contained by "
+ + "scaled image bounds: (" + width + " x " + height + ")");
+ }
+ }
+
+ @WorkerThread
+ @NonNull
+ private Bitmap decodeBitmapInternal() throws IOException {
+ checkState();
+ return nDecodeBitmap(mNativePtr, this, mPostProcessor != null,
+ mDesiredWidth, mDesiredHeight, mCropRect,
+ mMutable, mAllocator, mUnpremultipliedRequired,
+ mConserveMemory, mDecodeAsAlphaMask, mDesiredColorSpace);
+ }
+
+ private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener,
+ @NonNull Source src) {
+ if (listener != null) {
+ ImageInfo info = new ImageInfo(this);
+ try {
+ listener.onHeaderDecoded(this, info, src);
+ } finally {
+ info.mDecoder = null;
+ }
+ }
}
/**
* Create a {@link Drawable} from a {@code Source}.
*
* @param src representing the encoded image.
- * @param listener for learning the {@link ImageInfo} and changing any
- * default settings on the {@code ImageDecoder}. If not {@code null},
- * this will be called on the same thread as {@code decodeDrawable}
- * before that method returns.
+ * @param listener for learning the {@link ImageInfo ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. This will be called on
+ * the same thread as {@code decodeDrawable} before that method returns.
+ * This is required in order to change any of the default settings.
* @return Drawable for displaying the image.
* @throws IOException if {@code src} is not found, is an unsupported
* format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Drawable decodeDrawable(@NonNull Source src,
+ @NonNull OnHeaderDecodedListener listener) throws IOException {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener cannot be null! "
+ + "Use decodeDrawable(Source) to not have a listener");
+ }
+ return decodeDrawableImpl(src, listener);
+ }
+
+ @WorkerThread
+ @NonNull
+ private static Drawable decodeDrawableImpl(@NonNull Source src,
@Nullable OnHeaderDecodedListener listener) throws IOException {
- Bitmap bitmap = decodeBitmap(src, listener);
- return new BitmapDrawable(src.getResources(), bitmap);
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ if (decoder.mUnpremultipliedRequired) {
+ // Though this could be supported (ignored) for opaque images,
+ // it seems better to always report this error.
+ throw new IllegalStateException("Cannot decode a Drawable " +
+ "with unpremultiplied pixels!");
+ }
+
+ if (decoder.mMutable) {
+ throw new IllegalStateException("Cannot decode a mutable " +
+ "Drawable!");
+ }
+
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap and after decode set the density on the resulting bitmap
+ final int srcDensity = decoder.computeDensity(src);
+ if (decoder.mAnimated) {
+ // AnimatedImageDrawable calls postProcessAndRelease only if
+ // mPostProcessor exists.
+ ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
+ null : decoder;
+ Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+ postProcessPtr, decoder.mDesiredWidth,
+ decoder.mDesiredHeight, srcDensity,
+ src.computeDstDensity(), decoder.mCropRect,
+ decoder.mInputStream, decoder.mAssetFd);
+ // d has taken ownership of these objects.
+ decoder.mInputStream = null;
+ decoder.mAssetFd = null;
+ return d;
+ }
+
+ Bitmap bm = decoder.decodeBitmapInternal();
+ bm.setDensity(srcDensity);
+
+ Resources res = src.getResources();
+ byte[] np = bm.getNinePatchChunk();
+ if (np != null && NinePatch.isNinePatchChunk(np)) {
+ Rect opticalInsets = new Rect();
+ bm.getOpticalInsets(opticalInsets);
+ Rect padding = decoder.mOutPaddingRect;
+ if (padding == null) {
+ padding = new Rect();
+ }
+ nGetPadding(decoder.mNativePtr, padding);
+ return new NinePatchDrawable(res, bm, np, padding,
+ opticalInsets, null);
+ }
+
+ return new BitmapDrawable(res, bm);
+ }
}
/**
- * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}.
+ * Create a {@link Drawable} from a {@code Source}.
+ *
+ * <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener},
+ * the default settings will be used. In order to change any settings, call
+ * {@link #decodeDrawable(Source, OnHeaderDecodedListener)} instead.</p>
+ *
+ * @param src representing the encoded image.
+ * @return Drawable for displaying the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Drawable decodeDrawable(@NonNull Source src)
throws IOException {
- return decodeDrawable(src, null);
+ return decodeDrawableImpl(src, null);
}
/**
* Create a {@link Bitmap} from a {@code Source}.
*
* @param src representing the encoded image.
- * @param listener for learning the {@link ImageInfo} and changing any
- * default settings on the {@code ImageDecoder}. If not {@code null},
- * this will be called on the same thread as {@code decodeBitmap}
- * before that method returns.
+ * @param listener for learning the {@link ImageInfo ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. This will be called on
+ * the same thread as {@code decodeBitmap} before that method returns.
+ * This is required in order to change any of the default settings.
* @return Bitmap containing the image.
* @throws IOException if {@code src} is not found, is an unsupported
* format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Bitmap decodeBitmap(@NonNull Source src,
+ @NonNull OnHeaderDecodedListener listener) throws IOException {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener cannot be null! "
+ + "Use decodeBitmap(Source) to not have a listener");
+ }
+ return decodeBitmapImpl(src, listener);
+ }
+
+ @WorkerThread
+ @NonNull
+ private static Bitmap decodeBitmapImpl(@NonNull Source src,
@Nullable OnHeaderDecodedListener listener) throws IOException {
- TypedValue value = new TypedValue();
- value.density = src.getDensity();
- ImageDecoder decoder = src.createImageDecoder();
- if (listener != null) {
- listener.onHeaderDecoded(decoder, new ImageInfo(decoder), src);
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap
+ final int srcDensity = decoder.computeDensity(src);
+ Bitmap bm = decoder.decodeBitmapInternal();
+ bm.setDensity(srcDensity);
+
+ Rect padding = decoder.mOutPaddingRect;
+ if (padding != null) {
+ byte[] np = bm.getNinePatchChunk();
+ if (np != null && NinePatch.isNinePatchChunk(np)) {
+ nGetPadding(decoder.mNativePtr, padding);
+ }
+ }
+
+ return bm;
+ }
+ }
+
+ // This method may modify the decoder so it must be called prior to performing the decode
+ private int computeDensity(@NonNull Source src) {
+ // if the caller changed the size then we treat the density as unknown
+ if (this.requestedResize()) {
+ return Bitmap.DENSITY_NONE;
+ }
+
+ final int srcDensity = src.getDensity();
+ if (srcDensity == Bitmap.DENSITY_NONE) {
+ return srcDensity;
+ }
+
+ // Scaling up nine-patch divs is imprecise and is better handled
+ // at draw time. An app won't be relying on the internal Bitmap's
+ // size, so it is safe to let NinePatchDrawable handle scaling.
+ // mPostProcessor disables nine-patching, so behave normally if
+ // it is present.
+ if (mIsNinePatch && mPostProcessor == null) {
+ return srcDensity;
+ }
+
+ // Special stuff for compatibility mode: if the target density is not
+ // the same as the display density, but the resource -is- the same as
+ // the display density, then don't scale it down to the target density.
+ // This allows us to load the system's density-correct resources into
+ // an application in compatibility mode, without scaling those down
+ // to the compatibility density only to have them scaled back up when
+ // drawn to the screen.
+ Resources res = src.getResources();
+ if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) {
+ return srcDensity;
+ }
+
+ final int dstDensity = src.computeDstDensity();
+ if (srcDensity == dstDensity) {
+ return srcDensity;
}
- return BitmapFactory.decodeResourceStream(src.getResources(), value,
- ((InputStreamSource) src).mInputStream, decoder.mOutPaddingRect, null);
+
+ // For P and above, only resize if it would be a downscale. Scale up prior
+ // to P in case the app relies on the Bitmap's size without considering density.
+ if (srcDensity < dstDensity && sApiLevel >= Build.VERSION_CODES.P) {
+ return srcDensity;
+ }
+
+ float scale = (float) dstDensity / srcDensity;
+ int scaledWidth = (int) (mWidth * scale + 0.5f);
+ int scaledHeight = (int) (mHeight * scale + 0.5f);
+ this.setTargetSize(scaledWidth, scaledHeight);
+ return dstDensity;
+ }
+
+ @NonNull
+ private String getMimeType() {
+ return nGetMimeType(mNativePtr);
+ }
+
+ @Nullable
+ private ColorSpace getColorSpace() {
+ return nGetColorSpace(mNativePtr);
}
/**
- * See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}.
+ * Create a {@link Bitmap} from a {@code Source}.
+ *
+ * <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener},
+ * the default settings will be used. In order to change any settings, call
+ * {@link #decodeBitmap(Source, OnHeaderDecodedListener)} instead.</p>
+ *
+ * @param src representing the encoded image.
+ * @return Bitmap containing the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
- return decodeBitmap(src, null);
+ return decodeBitmapImpl(src, null);
}
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private int postProcessAndRelease(@NonNull Canvas canvas) {
+ try {
+ return mPostProcessor.onPostProcess(canvas);
+ } finally {
+ canvas.release();
+ }
+ }
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private void onPartialImage(@DecodeException.Error int error, @Nullable Throwable cause)
+ throws DecodeException {
+ DecodeException exception = new DecodeException(error, cause, mSource);
+ if (mOnPartialImageListener == null
+ || !mOnPartialImageListener.onPartialImage(exception)) {
+ throw exception;
+ }
+ }
+
+ private static native ImageDecoder nCreate(long asset, Source src) throws IOException;
+ private static native ImageDecoder nCreate(ByteBuffer buffer, int position,
+ int limit, Source src) throws IOException;
+ private static native ImageDecoder nCreate(byte[] data, int offset, int length,
+ Source src) throws IOException;
+ private static native ImageDecoder nCreate(InputStream is, byte[] storage,
+ Source src) throws IOException;
+ // The fd must be seekable.
+ private static native ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException;
+ @NonNull
+ private static native Bitmap nDecodeBitmap(long nativePtr,
+ @NonNull ImageDecoder decoder,
+ boolean doPostProcess,
+ int width, int height,
+ @Nullable Rect cropRect, boolean mutable,
+ int allocator, boolean unpremulRequired,
+ boolean conserveMemory, boolean decodeAsAlphaMask,
+ @Nullable ColorSpace desiredColorSpace)
+ throws IOException;
+ private static native Size nGetSampledSize(long nativePtr,
+ int sampleSize);
+ private static native void nGetPadding(long nativePtr, @NonNull Rect outRect);
+ private static native void nClose(long nativePtr);
+ private static native String nGetMimeType(long nativePtr);
+ private static native ColorSpace nGetColorSpace(long nativePtr);
}
diff --git a/android/graphics/PostProcessor.java b/android/graphics/PostProcessor.java
index b1712e92..6fed39b9 100644
--- a/android/graphics/PostProcessor.java
+++ b/android/graphics/PostProcessor.java
@@ -16,25 +16,26 @@
package android.graphics;
-import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
/**
* Helper interface for adding custom processing to an image.
*
- * <p>The image being processed may be a {@link Drawable}, {@link Bitmap} or frame
- * of an animated image produced by {@link ImageDecoder}. This is called before
- * the requested object is returned.</p>
+ * <p>The image being processed may be a {@link Drawable}, a {@link Bitmap}, or
+ * a frame of an {@link AnimatedImageDrawable} produced by {@link ImageDecoder}.
+ * This is called before the requested object is returned.</p>
*
- * <p>This custom processing also applies to image types that are otherwise
- * immutable, such as {@link Bitmap.Config#HARDWARE}.</p>
+ * <p>This custom processing can even be applied to images that will be returned
+ * as immutable objects, such as a {@link Bitmap} with {@code Config}
+ * {@link Bitmap.Config#HARDWARE} returned by {@link ImageDecoder}.</p>
*
- * <p>On an animated image, the callback will only be called once, but the drawing
- * commands will be applied to each frame, as if the {@code Canvas} had been
- * returned by {@link Picture#beginRecording}.<p>
+ * <p>On an {@link AnimatedImageDrawable}, the callback will only be called once,
+ * but the drawing commands will be applied to each frame, as if the {@link Canvas}
+ * had been returned by {@link Picture#beginRecording Picture.beginRecording}.<p>
*
- * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor}.</p>
+ * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor setPostProcessor}.</p>
*/
public interface PostProcessor {
/**
@@ -43,43 +44,44 @@ public interface PostProcessor {
* <p>Drawing to the {@link Canvas} will behave as if the initial processing
* (e.g. decoding) already exists in the Canvas. An implementation can draw
* effects on top of this, or it can even draw behind it using
- * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency
- * to the corners to achieve rounded corners. That can be done with the
- * following code:</p>
+ * {@link PorterDuff.Mode#DST_OVER PorterDuff.Mode.DST_OVER}. A common
+ * effect is to add transparency to the corners to achieve rounded corners.
+ * That can be done with the following code:</p>
*
- * <code>
- * Path path = new Path();
- * path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
- * int width = canvas.getWidth();
- * int height = canvas.getHeight();
- * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
- * Paint paint = new Paint();
- * paint.setAntiAlias(true);
- * paint.setColor(Color.TRANSPARENT);
- * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
- * canvas.drawPath(path, paint);
- * return PixelFormat.TRANSLUCENT;
- * </code>
+ * <pre class="prettyprint">
+ * Path path = new Path();
+ * path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ * int width = canvas.getWidth();
+ * int height = canvas.getHeight();
+ * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
+ * Paint paint = new Paint();
+ * paint.setAntiAlias(true);
+ * paint.setColor(Color.TRANSPARENT);
+ * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ * canvas.drawPath(path, paint);
+ * return PixelFormat.TRANSLUCENT;
+ * </pre>
*
*
* @param canvas The {@link Canvas} to draw to.
* @return Opacity of the result after drawing.
- * {@link PixelFormat#UNKNOWN} means that the implementation did not
- * change whether the image has alpha. Return this unless you added
- * transparency (e.g. with the code above, in which case you should
- * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to
- * be opaque (e.g. by drawing everywhere with an opaque color and
- * {@code PorterDuff.Mode.DST_OVER}, in which case you should return
- * {@code PixelFormat.OPAQUE}).
- * {@link PixelFormat#TRANSLUCENT} means that the implementation added
- * transparency. This is safe to return even if the image already had
- * transparency. This is also safe to return if the result is opaque,
- * though it may draw more slowly.
- * {@link PixelFormat#OPAQUE} means that the implementation forced the
- * image to be opaque. This is safe to return even if the image was
- * already opaque.
- * {@link PixelFormat#TRANSPARENT} (or any other integer) is not
- * allowed, and will result in throwing an
+ * {@link PixelFormat#UNKNOWN PixelFormat.UNKNOWN} means that the
+ * implementation did not change whether the image has alpha. Return
+ * this unless you added transparency (e.g. with the code above, in
+ * which case you should return
+ * {@link PixelFormat#TRANSLUCENT PixelFormat.TRANSLUCENT}) or you
+ * forced the image to be opaque (e.g. by drawing everywhere with an
+ * opaque color and {@link PorterDuff.Mode#DST_OVER PorterDuff.Mode.DST_OVER},
+ * in which case you should return {@link PixelFormat#OPAQUE PixelFormat.OPAQUE}).
+ * {@link PixelFormat#TRANSLUCENT PixelFormat.TRANSLUCENT} means that
+ * the implementation added transparency. This is safe to return even
+ * if the image already had transparency. This is also safe to return
+ * if the result is opaque, though it may draw more slowly.
+ * {@link PixelFormat#OPAQUE PixelFormat.OPAQUE} means that the
+ * implementation forced the image to be opaque. This is safe to return
+ * even if the image was already opaque.
+ * {@link PixelFormat#TRANSPARENT PixelFormat.TRANSPARENT} (or any other
+ * integer) is not allowed, and will result in throwing an
* {@link java.lang.IllegalArgumentException}.
*/
@PixelFormat.Opacity
diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java
index 20c22b72..18dd97f8 100644
--- a/android/graphics/Typeface.java
+++ b/android/graphics/Typeface.java
@@ -738,6 +738,23 @@ public class Typeface {
/**
* Creates a typeface object that best matches the specified existing typeface and the specified
* weight and italic style
+ * <p>Below are numerical values and corresponding common weight names.</p>
+ * <table>
+ * <thead>
+ * <tr><th>Value</th><th>Common weight name</th></tr>
+ * </thead>
+ * <tbody>
+ * <tr><td>100</td><td>Thin</td></tr>
+ * <tr><td>200</td><td>Extra Light</td></tr>
+ * <tr><td>300</td><td>Light</td></tr>
+ * <tr><td>400</td><td>Normal</td></tr>
+ * <tr><td>500</td><td>Medium</td></tr>
+ * <tr><td>600</td><td>Semi Bold</td></tr>
+ * <tr><td>700</td><td>Bold</td></tr>
+ * <tr><td>800</td><td>Extra Bold</td></tr>
+ * <tr><td>900</td><td>Black</td></tr>
+ * </tbody>
+ * </table>
*
* <p>
* This method is thread safe.
@@ -749,6 +766,9 @@ public class Typeface {
* @param italic {@code true} if italic style is desired to be drawn. Otherwise, {@code false}
* @return A {@link Typeface} object for drawing specified weight and italic style. Never
* returns {@code null}
+ *
+ * @see #getWeight()
+ * @see #isItalic()
*/
public static @NonNull Typeface create(@Nullable Typeface family,
@IntRange(from = 1, to = 1000) int weight, boolean italic) {
diff --git a/android/hardware/biometrics/BiometricDialog.java b/android/hardware/biometrics/BiometricPrompt.java
index dd848a34..1c9de457 100644
--- a/android/hardware/biometrics/BiometricDialog.java
+++ b/android/hardware/biometrics/BiometricPrompt.java
@@ -38,7 +38,7 @@ import javax.crypto.Mac;
/**
* A class that manages a system-provided biometric dialog.
*/
-public class BiometricDialog implements BiometricAuthenticator, BiometricConstants {
+public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants {
/**
* @hide
@@ -190,11 +190,11 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
}
/**
- * Creates a {@link BiometricDialog}.
- * @return a {@link BiometricDialog}
+ * Creates a {@link BiometricPrompt}.
+ * @return a {@link BiometricPrompt}
* @throws IllegalArgumentException if any of the required fields are not set.
*/
- public BiometricDialog build() {
+ public BiometricPrompt build() {
final CharSequence title = mBundle.getCharSequence(KEY_TITLE);
final CharSequence negative = mBundle.getCharSequence(KEY_NEGATIVE_TEXT);
@@ -203,7 +203,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
} else if (TextUtils.isEmpty(negative)) {
throw new IllegalArgumentException("Negative text must be set and non-empty");
}
- return new BiometricDialog(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
+ return new BiometricPrompt(mContext, mBundle, mPositiveButtonInfo, mNegativeButtonInfo);
}
}
@@ -213,7 +213,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
private ButtonInfo mPositiveButtonInfo;
private ButtonInfo mNegativeButtonInfo;
- IBiometricDialogReceiver mDialogReceiver = new IBiometricDialogReceiver.Stub() {
+ IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
@Override
public void onDialogDismissed(int reason) {
// Check the reason and invoke OnClickListener(s) if necessary
@@ -229,7 +229,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
}
};
- private BiometricDialog(Context context, Bundle bundle,
+ private BiometricPrompt(Context context, Bundle bundle,
ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) {
mBundle = bundle;
mPositiveButtonInfo = positiveButtonInfo;
@@ -239,7 +239,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
}
/**
- * A wrapper class for the crypto objects supported by BiometricDialog. Currently the framework
+ * A wrapper class for the crypto objects supported by BiometricPrompt. Currently the framework
* supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
*/
public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -308,8 +308,8 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
}
/**
- * Callback structure provided to {@link BiometricDialog#authenticate(CancellationSignal,
- * Executor, AuthenticationCallback)} or {@link BiometricDialog#authenticate(CryptoObject,
+ * Callback structure provided to {@link BiometricPrompt#authenticate(CancellationSignal,
+ * Executor, AuthenticationCallback)} or {@link BiometricPrompt#authenticate(CryptoObject,
* CancellationSignal, Executor, AuthenticationCallback)}. Users must provide an implementation
* of this for listening to authentication events.
*/
@@ -378,7 +378,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
@NonNull CancellationSignal cancel,
@NonNull @CallbackExecutor Executor executor,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
- if (!(callback instanceof BiometricDialog.AuthenticationCallback)) {
+ if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
throw new IllegalArgumentException("Callback cannot be casted");
}
authenticate(crypto, cancel, executor, (AuthenticationCallback) callback);
@@ -395,7 +395,7 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
public void authenticate(@NonNull CancellationSignal cancel,
@NonNull @CallbackExecutor Executor executor,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
- if (!(callback instanceof BiometricDialog.AuthenticationCallback)) {
+ if (!(callback instanceof BiometricPrompt.AuthenticationCallback)) {
throw new IllegalArgumentException("Callback cannot be casted");
}
authenticate(cancel, executor, (AuthenticationCallback) callback);
@@ -410,8 +410,8 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
* operation can be canceled by using the provided cancel object. The application will receive
* authentication errors through {@link AuthenticationCallback}, and button events through the
* corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor,
- * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricDialog} object,
- * and calling {@link BiometricDialog#authenticate( CancellationSignal, Executor,
+ * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object,
+ * and calling {@link BiometricPrompt#authenticate( CancellationSignal, Executor,
* AuthenticationCallback)} while an existing authentication attempt is occurring will stop the
* previous client and start a new authentication. The interrupted client will receive a
* cancelled notification through {@link AuthenticationCallback#onAuthenticationError(int,
@@ -445,8 +445,8 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
* provided cancel object. The application will receive authentication errors through {@link
* AuthenticationCallback}, and button events through the corresponding callback set in {@link
* Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}. It is
- * safe to reuse the {@link BiometricDialog} object, and calling {@link
- * BiometricDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
+ * safe to reuse the {@link BiometricPrompt} object, and calling {@link
+ * BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
* an existing authentication attempt is occurring will stop the previous client and start a new
* authentication. The interrupted client will receive a cancelled notification through {@link
* AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
@@ -470,15 +470,15 @@ public class BiometricDialog implements BiometricAuthenticator, BiometricConstan
private boolean handlePreAuthenticationErrors(AuthenticationCallback callback,
Executor executor) {
if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
- sendError(BiometricDialog.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
+ sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
executor);
return true;
} else if (!mFingerprintManager.isHardwareDetected()) {
- sendError(BiometricDialog.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
+ sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
executor);
return true;
} else if (!mFingerprintManager.hasEnrolledFingerprints()) {
- sendError(BiometricDialog.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
+ sendError(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
executor);
return true;
}
diff --git a/android/hardware/camera2/CameraManager.java b/android/hardware/camera2/CameraManager.java
index 4124536d..7ebe0f9a 100644
--- a/android/hardware/camera2/CameraManager.java
+++ b/android/hardware/camera2/CameraManager.java
@@ -43,6 +43,9 @@ import android.util.ArrayMap;
import android.util.Log;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
@@ -924,6 +927,37 @@ public final class CameraManager {
idCount++;
}
}
+
+ // The sort logic must match the logic in
+ // libcameraservice/common/CameraProviderManager.cpp::getAPI1CompatibleCameraDeviceIds
+ Arrays.sort(cameraIds, new Comparator<String>() {
+ @Override
+ public int compare(String s1, String s2) {
+ int s1Int = 0, s2Int = 0;
+ try {
+ s1Int = Integer.parseInt(s1);
+ } catch (NumberFormatException e) {
+ s1Int = -1;
+ }
+
+ try {
+ s2Int = Integer.parseInt(s2);
+ } catch (NumberFormatException e) {
+ s2Int = -1;
+ }
+
+ // Uint device IDs first
+ if (s1Int >= 0 && s2Int >= 0) {
+ return s1Int - s2Int;
+ } else if (s1Int >= 0) {
+ return -1;
+ } else if (s2Int >= 0) {
+ return 1;
+ } else {
+ // Simple string compare if both id are not uint
+ return s1.compareTo(s2);
+ }
+ }});
return cameraIds;
}
diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java
index 22525719..411a97e3 100644
--- a/android/hardware/camera2/CaptureRequest.java
+++ b/android/hardware/camera2/CaptureRequest.java
@@ -2105,8 +2105,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* the thumbnail data will also be rotated.</p>
* <p>Note that this orientation is relative to the orientation of the camera sensor, given
* by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
- * <p>To translate from the device orientation given by the Android sensor APIs, the following
- * sample code may be used:</p>
+ * <p>To translate from the device orientation given by the Android sensor APIs for camera
+ * sensors which are not EXTERNAL, the following sample code may be used:</p>
* <pre><code>private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
* if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0;
* int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);
@@ -2125,6 +2125,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* return jpegOrientation;
* }
* </code></pre>
+ * <p>For EXTERNAL cameras the sensor orientation will always be set to 0 and the facing will
+ * also be set to EXTERNAL. The above code is not relevant in such case.</p>
* <p><b>Units</b>: Degrees in multiples of 90</p>
* <p><b>Range of valid values:</b><br>
* 0, 90, 180, 270</p>
diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java
index 8df54472..c1566161 100644
--- a/android/hardware/camera2/CaptureResult.java
+++ b/android/hardware/camera2/CaptureResult.java
@@ -2422,8 +2422,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* the thumbnail data will also be rotated.</p>
* <p>Note that this orientation is relative to the orientation of the camera sensor, given
* by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
- * <p>To translate from the device orientation given by the Android sensor APIs, the following
- * sample code may be used:</p>
+ * <p>To translate from the device orientation given by the Android sensor APIs for camera
+ * sensors which are not EXTERNAL, the following sample code may be used:</p>
* <pre><code>private int getJpegOrientation(CameraCharacteristics c, int deviceOrientation) {
* if (deviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) return 0;
* int sensorOrientation = c.get(CameraCharacteristics.SENSOR_ORIENTATION);
@@ -2442,6 +2442,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* return jpegOrientation;
* }
* </code></pre>
+ * <p>For EXTERNAL cameras the sensor orientation will always be set to 0 and the facing will
+ * also be set to EXTERNAL. The above code is not relevant in such case.</p>
* <p><b>Units</b>: Degrees in multiples of 90</p>
* <p><b>Range of valid values:</b><br>
* 0, 90, 180, 270</p>
diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java
index 67e97bfd..6d9ba778 100644
--- a/android/hardware/display/BrightnessConfiguration.java
+++ b/android/hardware/display/BrightnessConfiguration.java
@@ -86,7 +86,9 @@ public final class BrightnessConfiguration implements Parcelable {
sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")");
}
sb.append("], '");
- sb.append(mDescription);
+ if (mDescription != null) {
+ sb.append(mDescription);
+ }
sb.append("'}");
return sb.toString();
}
@@ -96,7 +98,9 @@ public final class BrightnessConfiguration implements Parcelable {
int result = 1;
result = result * 31 + Arrays.hashCode(mLux);
result = result * 31 + Arrays.hashCode(mNits);
- result = result * 31 + mDescription.hashCode();
+ if (mDescription != null) {
+ result = result * 31 + mDescription.hashCode();
+ }
return result;
}
diff --git a/android/hardware/display/Curve.java b/android/hardware/display/Curve.java
new file mode 100644
index 00000000..ac28fdd6
--- /dev/null
+++ b/android/hardware/display/Curve.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.display;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public final class Curve implements Parcelable {
+ private final float[] mX;
+ private final float[] mY;
+
+ public Curve(float[] x, float[] y) {
+ mX = x;
+ mY = y;
+ }
+
+ public float[] getX() {
+ return mX;
+ }
+
+ public float[] getY() {
+ return mY;
+ }
+
+ public static final Creator<Curve> CREATOR = new Creator<Curve>() {
+ public Curve createFromParcel(Parcel in) {
+ float[] x = in.createFloatArray();
+ float[] y = in.createFloatArray();
+ return new Curve(x, y);
+ }
+
+ public Curve[] newArray(int size) {
+ return new Curve[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeFloatArray(mX);
+ out.writeFloatArray(mY);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java
index efb9517a..b182fa2e 100644
--- a/android/hardware/display/DisplayManager.java
+++ b/android/hardware/display/DisplayManager.java
@@ -28,6 +28,7 @@ import android.content.Context;
import android.graphics.Point;
import android.media.projection.MediaProjection;
import android.os.Handler;
+import android.util.Pair;
import android.util.SparseArray;
import android.view.Display;
import android.view.Surface;
@@ -748,6 +749,22 @@ public final class DisplayManager {
}
/**
+ * Returns the minimum brightness curve, which guarantess that any brightness curve that dips
+ * below it is rejected by the system.
+ * This prevent auto-brightness from setting the screen so dark as to prevent the user from
+ * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable
+ * in that ambient brightness.
+ *
+ * @return The minimum brightness curve (as lux values and their corresponding nits values).
+ *
+ * @hide
+ */
+ @SystemApi
+ public Pair<float[], float[]> getMinimumBrightnessCurve() {
+ return mGlobal.getMinimumBrightnessCurve();
+ }
+
+ /**
* Listens for changes in available display devices.
*/
public interface DisplayListener {
diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java
index 2d0ef2f2..d968a3e9 100644
--- a/android/hardware/display/DisplayManagerGlobal.java
+++ b/android/hardware/display/DisplayManagerGlobal.java
@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayAdjustments;
@@ -563,6 +564,24 @@ public final class DisplayManagerGlobal {
}
/**
+ * Returns the minimum brightness curve, which guarantess that any brightness curve that dips
+ * below it is rejected by the system.
+ * This prevent auto-brightness from setting the screen so dark as to prevent the user from
+ * resetting or disabling it, and maps lux to the absolute minimum nits that are still readable
+ * in that ambient brightness.
+ *
+ * @return The minimum brightness curve (as lux values and their corresponding nits values).
+ */
+ public Pair<float[], float[]> getMinimumBrightnessCurve() {
+ try {
+ Curve curve = mDm.getMinimumBrightnessCurve();
+ return Pair.create(curve.getX(), curve.getY());
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Retrieves ambient brightness stats.
*/
public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
diff --git a/android/hardware/fingerprint/FingerprintManager.java b/android/hardware/fingerprint/FingerprintManager.java
index a6c8c67d..40d31bfe 100644
--- a/android/hardware/fingerprint/FingerprintManager.java
+++ b/android/hardware/fingerprint/FingerprintManager.java
@@ -31,9 +31,9 @@ import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.biometrics.BiometricAuthenticator;
-import android.hardware.biometrics.BiometricDialog;
import android.hardware.biometrics.BiometricFingerprintConstants;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
@@ -57,7 +57,7 @@ import javax.crypto.Mac;
/**
* A class that coordinates access to the fingerprint hardware.
- * @deprecated See {@link BiometricDialog} which shows a system-provided dialog upon starting
+ * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting
* authentication. In a world where devices may have different types of biometric authentication,
* it's much more realistic to have a system-provided authentication dialog since the method may
* vary by vendor/device.
@@ -111,7 +111,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
/**
* A wrapper class for the crypto objects supported by FingerprintManager. Currently the
* framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects.
- * @deprecated See {@link android.hardware.biometrics.BiometricDialog.CryptoObject}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject}
*/
@Deprecated
public static final class CryptoObject extends android.hardware.biometrics.CryptoObject {
@@ -155,7 +155,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
/**
* Container for callback data from {@link FingerprintManager#authenticate(CryptoObject,
* CancellationSignal, int, AuthenticationCallback, Handler)}.
- * @deprecated See {@link android.hardware.biometrics.BiometricDialog.AuthenticationResult}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult}
*/
@Deprecated
public static class AuthenticationResult {
@@ -204,7 +204,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
* FingerprintManager#authenticate(CryptoObject, CancellationSignal,
* int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to
* fingerprint events.
- * @deprecated See {@link android.hardware.biometrics.BiometricDialog.AuthenticationCallback}
+ * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback}
*/
@Deprecated
public static abstract class AuthenticationCallback
@@ -378,10 +378,10 @@ public class FingerprintManager implements BiometricFingerprintConstants {
* by <a href="{@docRoot}training/articles/keystore.html">Android Keystore
* facility</a>.
* @throws IllegalStateException if the crypto primitive is not initialized.
- * @deprecated See {@link BiometricDialog#authenticate(CancellationSignal, Executor,
- * BiometricDialog.AuthenticationCallback)} and {@link BiometricDialog#authenticate(
- * BiometricDialog.CryptoObject, CancellationSignal, Executor,
- * BiometricDialog.AuthenticationCallback)}
+ * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+ * BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate(
+ * BiometricPrompt.CryptoObject, CancellationSignal, Executor,
+ * BiometricPrompt.AuthenticationCallback)}
*/
@Deprecated
@RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT})
@@ -444,7 +444,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
/**
* Per-user version, see {@link FingerprintManager#authenticate(CryptoObject,
- * CancellationSignal, Bundle, Executor, IBiometricDialogReceiver, AuthenticationCallback)}
+ * CancellationSignal, Bundle, Executor, IBiometricPromptReceiver, AuthenticationCallback)}
* @param userId the user ID that the fingerprint hardware will authenticate for.
*/
private void authenticate(int userId,
@@ -452,7 +452,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
@NonNull CancellationSignal cancel,
@NonNull Bundle bundle,
@NonNull @CallbackExecutor Executor executor,
- @NonNull IBiometricDialogReceiver receiver,
+ @NonNull IBiometricPromptReceiver receiver,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
mCryptoObject = crypto;
if (cancel.isCanceled()) {
@@ -480,8 +480,8 @@ public class FingerprintManager implements BiometricFingerprintConstants {
}
/**
- * Private method, see {@link BiometricDialog#authenticate(CancellationSignal, Executor,
- * BiometricDialog.AuthenticationCallback)}
+ * Private method, see {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
+ * BiometricPrompt.AuthenticationCallback)}
* @param cancel
* @param executor
* @param callback
@@ -491,7 +491,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
@NonNull CancellationSignal cancel,
@NonNull Bundle bundle,
@NonNull @CallbackExecutor Executor executor,
- @NonNull IBiometricDialogReceiver receiver,
+ @NonNull IBiometricPromptReceiver receiver,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
if (cancel == null) {
throw new IllegalArgumentException("Must supply a cancellation signal");
@@ -512,8 +512,8 @@ public class FingerprintManager implements BiometricFingerprintConstants {
}
/**
- * Private method, see {@link BiometricDialog#authenticate(BiometricDialog.CryptoObject,
- * CancellationSignal, Executor, BiometricDialog.AuthenticationCallback)}
+ * Private method, see {@link BiometricPrompt#authenticate(BiometricPrompt.CryptoObject,
+ * CancellationSignal, Executor, BiometricPrompt.AuthenticationCallback)}
* @param crypto
* @param cancel
* @param executor
@@ -524,7 +524,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
@NonNull CancellationSignal cancel,
@NonNull Bundle bundle,
@NonNull @CallbackExecutor Executor executor,
- @NonNull IBiometricDialogReceiver receiver,
+ @NonNull IBiometricPromptReceiver receiver,
@NonNull BiometricAuthenticator.AuthenticationCallback callback) {
if (crypto == null) {
throw new IllegalArgumentException("Must supply a crypto object");
@@ -743,7 +743,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
* Determine if there is at least one fingerprint enrolled.
*
* @return true if at least one fingerprint is enrolled, false otherwise
- * @deprecated See {@link BiometricDialog} and
+ * @deprecated See {@link BiometricPrompt} and
* {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS}
*/
@Deprecated
@@ -777,7 +777,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
* Determine if fingerprint hardware is present and functional.
*
* @return true if hardware is present and functional, false otherwise.
- * @deprecated See {@link BiometricDialog} and
+ * @deprecated See {@link BiometricPrompt} and
* {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE}
*/
@Deprecated
@@ -1158,7 +1158,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
@Override // binder call
public void onError(long deviceId, int error, int vendorCode) {
if (mExecutor != null) {
- // BiometricDialog case
+ // BiometricPrompt case
if (error == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED) {
// User tapped somewhere to cancel, the biometric dialog is already dismissed.
mExecutor.execute(() -> {
@@ -1172,7 +1172,7 @@ public class FingerprintManager implements BiometricFingerprintConstants {
mExecutor.execute(() -> {
sendErrorResult(deviceId, error, vendorCode);
});
- }, BiometricDialog.HIDE_DIALOG_DELAY);
+ }, BiometricPrompt.HIDE_DIALOG_DELAY);
}
} else {
mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java
index a5239580..6eb3d8d1 100644
--- a/android/location/LocationManager.java
+++ b/android/location/LocationManager.java
@@ -29,6 +29,7 @@ import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -392,6 +393,18 @@ public class LocationManager {
}
/**
+ * @hide
+ */
+ @TestApi
+ public String[] getBackgroundThrottlingWhitelist() {
+ try {
+ return mService.getBackgroundThrottlingWhitelist();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* @hide - hide this constructor because it has a parameter
* of type ILocationManager, which is a system private class. The
* right way to create an instance of this class is using the
@@ -1249,40 +1262,11 @@ public class LocationManager {
@SystemApi
@RequiresPermission(WRITE_SECURE_SETTINGS)
public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
- final List<String> allProvidersList = getAllProviders();
- // Update all providers on device plus gps and network provider when disabling location.
- Set<String> allProvidersSet = new ArraySet<>(allProvidersList.size() + 2);
- allProvidersSet.addAll(allProvidersList);
- // When disabling location, disable gps and network provider that could have been enabled by
- // location mode api.
- if (enabled == false) {
- allProvidersSet.add(GPS_PROVIDER);
- allProvidersSet.add(NETWORK_PROVIDER);
- }
- if (allProvidersSet.isEmpty()) {
- return;
- }
- // to ensure thread safety, we write the provider name with a '+' or '-'
- // and let the SettingsProvider handle it rather than reading and modifying
- // the list of enabled providers.
- final String prefix = enabled ? "+" : "-";
- StringBuilder locationProvidersAllowed = new StringBuilder();
- for (String provider : allProvidersSet) {
- checkProvider(provider);
- if (provider.equals(PASSIVE_PROVIDER)) {
- continue;
- }
- locationProvidersAllowed.append(prefix);
- locationProvidersAllowed.append(provider);
- locationProvidersAllowed.append(",");
+ try {
+ mService.setLocationEnabledForUser(enabled, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
- // Remove the trailing comma
- locationProvidersAllowed.setLength(locationProvidersAllowed.length() - 1);
- Settings.Secure.putStringForUser(
- mContext.getContentResolver(),
- Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
- locationProvidersAllowed.toString(),
- userHandle.getIdentifier());
}
/**
@@ -1295,22 +1279,11 @@ public class LocationManager {
*/
@SystemApi
public boolean isLocationEnabledForUser(UserHandle userHandle) {
- final String allowedProviders = Settings.Secure.getStringForUser(
- mContext.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
- userHandle.getIdentifier());
- if (allowedProviders == null) {
- return false;
- }
- final List<String> providerList = Arrays.asList(allowedProviders.split(","));
- for(String provider : getAllProviders()) {
- if (provider.equals(PASSIVE_PROVIDER)) {
- continue;
- }
- if (providerList.contains(provider)) {
- return true;
- }
+ try {
+ return mService.isLocationEnabledForUser(userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
}
- return false;
}
/**
@@ -1362,9 +1335,12 @@ public class LocationManager {
@SystemApi
public boolean isProviderEnabledForUser(String provider, UserHandle userHandle) {
checkProvider(provider);
- String allowedProviders = Settings.Secure.getStringForUser(mContext.getContentResolver(),
- Settings.Secure.LOCATION_PROVIDERS_ALLOWED, userHandle.getIdentifier());
- return TextUtils.delimitedStringContains(allowedProviders, ',', provider);
+
+ try {
+ return mService.isProviderEnabledForUser(provider, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -1383,16 +1359,13 @@ public class LocationManager {
public boolean setProviderEnabledForUser(
String provider, boolean enabled, UserHandle userHandle) {
checkProvider(provider);
- // to ensure thread safety, we write the provider name with a '+' or '-'
- // and let the SettingsProvider handle it rather than reading and modifying
- // the list of enabled providers.
- if (enabled) {
- provider = "+" + provider;
- } else {
- provider = "-" + provider;
- }
- return Settings.Secure.putStringForUser(mContext.getContentResolver(),
- Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider, userHandle.getIdentifier());
+
+ try {
+ return mService.setProviderEnabledForUser(
+ provider, enabled, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index d4326583..9152ff2e 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -180,6 +180,7 @@ public final class AudioAttributes implements Parcelable {
* IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES
* if applicable, as well as audioattributes.proto.
* Also consider adding them to <aaudio/AAudio.h> for the NDK.
+ * Also consider adding them to UsageTypeConverter for service dump and etc.
*/
/**
@@ -249,9 +250,10 @@ public final class AudioAttributes implements Parcelable {
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, SUPPRESSIBLE_MEDIA);
SUPPRESSIBLE_USAGES.put(USAGE_GAME, SUPPRESSIBLE_MEDIA);
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT, SUPPRESSIBLE_MEDIA);
+ /** default volume assignment is STREAM_MUSIC, handle unknown usage as media */
+ SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_MEDIA);
SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING, SUPPRESSIBLE_SYSTEM);
SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION, SUPPRESSIBLE_SYSTEM);
- SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN, SUPPRESSIBLE_SYSTEM);
}
/**
@@ -1056,8 +1058,7 @@ public final class AudioAttributes implements Parcelable {
case USAGE_ASSISTANCE_ACCESSIBILITY:
return AudioSystem.STREAM_ACCESSIBILITY;
case USAGE_UNKNOWN:
- return fromGetVolumeControlStream ?
- AudioManager.USE_DEFAULT_STREAM_TYPE : AudioSystem.STREAM_MUSIC;
+ return AudioSystem.STREAM_MUSIC;
default:
if (fromGetVolumeControlStream) {
throw new IllegalArgumentException("Unknown usage value " + aa.getUsage() +
diff --git a/android/media/AudioFocusRequest.java b/android/media/AudioFocusRequest.java
index 7104dad4..fe89b89d 100644
--- a/android/media/AudioFocusRequest.java
+++ b/android/media/AudioFocusRequest.java
@@ -19,6 +19,7 @@ package android.media;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.os.Bundle;
import android.os.Handler;
@@ -262,6 +263,7 @@ public final class AudioFocusRequest {
* Returns the focus change listener set for this {@code AudioFocusRequest}.
* @return null if no {@link AudioManager.OnAudioFocusChangeListener} was set.
*/
+ @TestApi
public @Nullable OnAudioFocusChangeListener getOnAudioFocusChangeListener() {
return mFocusListener;
}
diff --git a/android/media/AudioFormat.java b/android/media/AudioFormat.java
index f98480b2..d0a2c98f 100644
--- a/android/media/AudioFormat.java
+++ b/android/media/AudioFormat.java
@@ -18,6 +18,7 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -437,6 +438,7 @@ public final class AudioFormat implements Parcelable {
* @param mask a combination of the CHANNEL_IN_* definitions, even CHANNEL_IN_DEFAULT
* @return number of channels for the mask
*/
+ @TestApi
public static int channelCountFromInChannelMask(int mask) {
return Integer.bitCount(mask);
}
@@ -446,6 +448,7 @@ public final class AudioFormat implements Parcelable {
* @param mask a combination of the CHANNEL_OUT_* definitions, but not CHANNEL_OUT_DEFAULT
* @return number of channels for the mask
*/
+ @TestApi
public static int channelCountFromOutChannelMask(int mask) {
return Integer.bitCount(mask);
}
@@ -492,6 +495,7 @@ public final class AudioFormat implements Parcelable {
// CHANNEL_IN_ALL is not yet defined; if added then it should match AUDIO_CHANNEL_IN_ALL
/** @hide */
+ @TestApi
public static int getBytesPerSample(int audioFormat)
{
switch (audioFormat) {
@@ -562,6 +566,7 @@ public final class AudioFormat implements Parcelable {
}
/** @hide */
+ @TestApi
public static boolean isEncodingLinearPcm(int audioFormat)
{
switch (audioFormat) {
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index aeef2158..fdb7499b 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -63,6 +63,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -4786,6 +4787,21 @@ public class AudioManager {
}
/**
+ * Add {@link MicrophoneInfo} by device information while filtering certain types.
+ */
+ private void addMicrophonesFromAudioDeviceInfo(ArrayList<MicrophoneInfo> microphones,
+ HashSet<Integer> filterTypes) {
+ AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS);
+ for (AudioDeviceInfo device : devices) {
+ if (filterTypes.contains(device.getType())) {
+ continue;
+ }
+ MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device);
+ microphones.add(microphone);
+ }
+ }
+
+ /**
* Returns a list of {@link MicrophoneInfo} that corresponds to the characteristics
* of all available microphones. The list is empty when no microphones are available
* on the device. An error during the query will result in an IOException being thrown.
@@ -4796,21 +4812,17 @@ public class AudioManager {
public List<MicrophoneInfo> getMicrophones() throws IOException {
ArrayList<MicrophoneInfo> microphones = new ArrayList<MicrophoneInfo>();
int status = AudioSystem.getMicrophones(microphones);
+ HashSet<Integer> filterTypes = new HashSet<>();
+ filterTypes.add(AudioDeviceInfo.TYPE_TELEPHONY);
if (status != AudioManager.SUCCESS) {
- // fail and bail!
+ // fail and populate microphones with unknown characteristics by device information.
Log.e(TAG, "getMicrophones failed:" + status);
- return new ArrayList<MicrophoneInfo>(); // Always return a list.
+ addMicrophonesFromAudioDeviceInfo(microphones, filterTypes);
+ return microphones;
}
setPortIdForMicrophones(microphones);
- AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS);
- for (AudioDeviceInfo device : devices) {
- if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC ||
- device.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
- continue;
- }
- MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device);
- microphones.add(microphone);
- }
+ filterTypes.add(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ addMicrophonesFromAudioDeviceInfo(microphones, filterTypes);
return microphones;
}
diff --git a/android/media/AudioPlaybackConfiguration.java b/android/media/AudioPlaybackConfiguration.java
index 8a36f91c..7dfdb20a 100644
--- a/android/media/AudioPlaybackConfiguration.java
+++ b/android/media/AudioPlaybackConfiguration.java
@@ -43,6 +43,8 @@ public final class AudioPlaybackConfiguration implements Parcelable {
/** @hide */
public static final int PLAYER_PIID_INVALID = -1;
/** @hide */
+ public static final int PLAYER_PIID_UNASSIGNED = 0;
+ /** @hide */
public static final int PLAYER_UPID_INVALID = -1;
// information about the implementation
diff --git a/android/media/AudioPresentation.java b/android/media/AudioPresentation.java
index e39cb7db..ce71436b 100644
--- a/android/media/AudioPresentation.java
+++ b/android/media/AudioPresentation.java
@@ -18,8 +18,7 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
-
-import com.android.internal.annotations.VisibleForTesting;
+import android.annotation.TestApi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -94,7 +93,7 @@ public final class AudioPresentation {
/**
* @hide
*/
- @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @TestApi
public AudioPresentation(int presentationId,
int programId,
@NonNull Map<String, String> labels,
@@ -119,7 +118,7 @@ public final class AudioPresentation {
* decoder. Presentation id is typically sequential, but does not have to be.
* @hide
*/
- @VisibleForTesting
+ @TestApi
public int getPresentationId() {
return mPresentationId;
}
@@ -129,7 +128,7 @@ public final class AudioPresentation {
* Program id can be used to further uniquely identify the presentation to a decoder.
* @hide
*/
- @VisibleForTesting
+ @TestApi
public int getProgramId() {
return mProgramId;
}
diff --git a/android/media/AudioRecord.java b/android/media/AudioRecord.java
index 4f0dccb8..6b35dd4c 100644
--- a/android/media/AudioRecord.java
+++ b/android/media/AudioRecord.java
@@ -1628,7 +1628,6 @@ public class AudioRecord implements AudioRouting
int status = native_get_active_microphones(activeMicrophones);
if (status != AudioManager.SUCCESS) {
Log.e(TAG, "getActiveMicrophones failed:" + status);
- return new ArrayList<MicrophoneInfo>();
}
AudioManager.setPortIdForMicrophones(activeMicrophones);
diff --git a/android/media/BufferingParams.java b/android/media/BufferingParams.java
index 521e8975..aaae5e7b 100644
--- a/android/media/BufferingParams.java
+++ b/android/media/BufferingParams.java
@@ -17,6 +17,7 @@
package android.media;
import android.annotation.IntDef;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -63,6 +64,7 @@ import java.lang.annotation.RetentionPolicy;
* <p>Users should use {@link Builder} to change {@link BufferingParams}.
* @hide
*/
+@TestApi
public final class BufferingParams implements Parcelable {
private static final int BUFFERING_NO_MARK = -1;
diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java
index bc0e43b5..78884367 100644
--- a/android/media/ExifInterface.java
+++ b/android/media/ExifInterface.java
@@ -66,7 +66,7 @@ import libcore.io.Streams;
/**
* This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
* <p>
- * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW and RAF.
+ * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF.
* <p>
* Attribute mutation is supported for JPEG image files.
*/
@@ -2524,46 +2524,46 @@ public class ExifInterface {
private void getHeifAttributes(ByteOrderedDataInputStream in) throws IOException {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
- if (mSeekableFileDescriptor != null) {
- retriever.setDataSource(mSeekableFileDescriptor);
- } else {
- retriever.setDataSource(new MediaDataSource() {
- long mPosition;
-
- @Override
- public void close() throws IOException {}
+ retriever.setDataSource(new MediaDataSource() {
+ long mPosition;
- @Override
- public int readAt(long position, byte[] buffer, int offset, int size)
- throws IOException {
- if (size == 0) {
- return 0;
- }
- if (position < 0) {
- return -1;
- }
- if (mPosition != position) {
- in.seek(position);
- mPosition = position;
- }
+ @Override
+ public void close() throws IOException {}
- int bytesRead = in.read(buffer, offset, size);
- if (bytesRead < 0) {
- mPosition = -1; // need to seek on next read
- return -1;
- }
-
- mPosition += bytesRead;
- return bytesRead;
+ @Override
+ public int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException {
+ if (size == 0) {
+ return 0;
+ }
+ if (position < 0) {
+ return -1;
+ }
+ if (mPosition != position) {
+ in.seek(position);
+ mPosition = position;
}
- @Override
- public long getSize() throws IOException {
+ int bytesRead = in.read(buffer, offset, size);
+ if (bytesRead < 0) {
+ mPosition = -1; // need to seek on next read
return -1;
}
- });
- }
+ mPosition += bytesRead;
+ return bytesRead;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return -1;
+ }
+ });
+
+ String exifOffsetStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET);
+ String exifLengthStr = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH);
String hasImage = retriever.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
String hasVideo = retriever.extractMetadata(
@@ -2622,6 +2622,30 @@ public class ExifInterface {
ExifAttribute.createUShort(orientation, mExifByteOrder));
}
+ if (exifOffsetStr != null && exifLengthStr != null) {
+ int offset = Integer.parseInt(exifOffsetStr);
+ int length = Integer.parseInt(exifLengthStr);
+ if (length <= 6) {
+ throw new IOException("Invalid exif length");
+ }
+ in.seek(offset);
+ byte[] identifier = new byte[6];
+ if (in.read(identifier) != 6) {
+ throw new IOException("Can't read identifier");
+ }
+ offset += 6;
+ length -= 6;
+ if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
+ throw new IOException("Invalid identifier");
+ }
+
+ byte[] bytes = new byte[length];
+ if (in.read(bytes) != length) {
+ throw new IOException("Can't read exif");
+ }
+ readExifSegment(bytes, IFD_TYPE_PRIMARY);
+ }
+
if (DEBUG) {
Log.d(TAG, "Heif meta: " + width + "x" + height + ", rotation " + rotation);
}
diff --git a/android/media/Image.java b/android/media/Image.java
index 37c57854..9828275e 100644
--- a/android/media/Image.java
+++ b/android/media/Image.java
@@ -193,6 +193,13 @@ public abstract class Image implements AutoCloseable {
public abstract int getTransform();
/**
+ * Get the scaling mode associated with this frame.
+ * @return The scaling mode that needs to be applied for this frame.
+ * @hide
+ */
+ public abstract int getScalingMode();
+
+ /**
* Get the {@link android.hardware.HardwareBuffer HardwareBuffer} handle of the input image
* intended for GPU and/or hardware access.
* <p>
diff --git a/android/media/ImageReader.java b/android/media/ImageReader.java
index 72d52d3d..8ec0e353 100644
--- a/android/media/ImageReader.java
+++ b/android/media/ImageReader.java
@@ -871,6 +871,12 @@ public class ImageReader implements AutoCloseable {
}
@Override
+ public int getScalingMode() {
+ throwISEIfImageIsInvalid();
+ return mScalingMode;
+ }
+
+ @Override
public HardwareBuffer getHardwareBuffer() {
throwISEIfImageIsInvalid();
return nativeGetHardwareBuffer();
@@ -1004,14 +1010,11 @@ public class ImageReader implements AutoCloseable {
private long mNativeBuffer;
/**
- * This field is set by native code during nativeImageSetup().
+ * These fields are set by native code during nativeImageSetup().
*/
private long mTimestamp;
-
- /**
- * This field is set by native code during nativeImageSetup().
- */
private int mTransform;
+ private int mScalingMode;
private SurfacePlane[] mPlanes;
private int mFormat = ImageFormat.UNKNOWN;
diff --git a/android/media/ImageWriter.java b/android/media/ImageWriter.java
index 8ee27ae5..397768af 100644
--- a/android/media/ImageWriter.java
+++ b/android/media/ImageWriter.java
@@ -371,7 +371,7 @@ public class ImageWriter implements AutoCloseable {
Rect crop = image.getCropRect();
nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), crop.left, crop.top,
- crop.right, crop.bottom, image.getTransform());
+ crop.right, crop.bottom, image.getTransform(), image.getScalingMode());
/**
* Only remove and cleanup the Images that are owned by this
@@ -558,7 +558,7 @@ public class ImageWriter implements AutoCloseable {
Rect crop = image.getCropRect();
nativeAttachAndQueueImage(mNativeContext, image.getNativeContext(), image.getFormat(),
image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom,
- image.getTransform());
+ image.getTransform(), image.getScalingMode());
}
/**
@@ -676,6 +676,7 @@ public class ImageWriter implements AutoCloseable {
private long mTimestamp = DEFAULT_TIMESTAMP;
private int mTransform = 0; //Default no transform
+ private int mScalingMode = 0; //Default frozen scaling mode
public WriterSurfaceImage(ImageWriter writer) {
mOwner = writer;
@@ -721,6 +722,13 @@ public class ImageWriter implements AutoCloseable {
}
@Override
+ public int getScalingMode() {
+ throwISEIfImageIsInvalid();
+
+ return mScalingMode;
+ }
+
+ @Override
public long getTimestamp() {
throwISEIfImageIsInvalid();
@@ -866,11 +874,12 @@ public class ImageWriter implements AutoCloseable {
private synchronized native void nativeDequeueInputImage(long nativeCtx, Image wi);
private synchronized native void nativeQueueInputImage(long nativeCtx, Image image,
- long timestampNs, int left, int top, int right, int bottom, int transform);
+ long timestampNs, int left, int top, int right, int bottom, int transform,
+ int scalingMode);
private synchronized native int nativeAttachAndQueueImage(long nativeCtx,
long imageNativeBuffer, int imageFormat, long timestampNs, int left,
- int top, int right, int bottom, int transform);
+ int top, int right, int bottom, int transform, int scalingMode);
private synchronized native void cancelImage(long nativeCtx, Image image);
diff --git a/android/media/MediaCodec.java b/android/media/MediaCodec.java
index e3fba0cd..1f00c782 100644
--- a/android/media/MediaCodec.java
+++ b/android/media/MediaCodec.java
@@ -3574,6 +3574,7 @@ final public class MediaCodec {
private final static int TYPE_YUV = 1;
private final int mTransform = 0; //Default no transform
+ private final int mScalingMode = 0; //Default frozen scaling mode
@Override
public int getFormat() {
@@ -3600,6 +3601,12 @@ final public class MediaCodec {
}
@Override
+ public int getScalingMode() {
+ throwISEIfImageIsInvalid();
+ return mScalingMode;
+ }
+
+ @Override
public long getTimestamp() {
throwISEIfImageIsInvalid();
return mTimestamp;
diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java
index 2a601f9b..c29300d1 100644
--- a/android/media/MediaCodecInfo.java
+++ b/android/media/MediaCodecInfo.java
@@ -2971,6 +2971,8 @@ public final class MediaCodecInfo {
public static final int AACObjectLD = 23;
public static final int AACObjectHE_PS = 29;
public static final int AACObjectELD = 39;
+ /** xHE-AAC (includes USAC) */
+ public static final int AACObjectXHE = 42;
// from OMX_VIDEO_VP8LEVELTYPE
public static final int VP8Level_Version0 = 0x01;
diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java
index 0955dd63..8ab5ec44 100644
--- a/android/media/MediaMetadataRetriever.java
+++ b/android/media/MediaMetadataRetriever.java
@@ -890,5 +890,14 @@ public class MediaMetadataRetriever
*/
public static final int METADATA_KEY_VIDEO_FRAME_COUNT = 32;
+ /**
+ * @hide
+ */
+ public static final int METADATA_KEY_EXIF_OFFSET = 33;
+
+ /**
+ * @hide
+ */
+ public static final int METADATA_KEY_EXIF_LENGTH = 34;
// Add more here...
}
diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java
index aef31b11..392a1eb0 100644
--- a/android/media/MediaPlayer.java
+++ b/android/media/MediaPlayer.java
@@ -19,6 +19,7 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.app.ActivityThread;
import android.content.ContentProvider;
import android.content.ContentResolver;
@@ -1680,6 +1681,7 @@ public class MediaPlayer extends PlayerBase
* @hide
*/
@NonNull
+ @TestApi
public native BufferingParams getBufferingParams();
/**
@@ -1696,6 +1698,7 @@ public class MediaPlayer extends PlayerBase
* @throws IllegalArgumentException if params is invalid or not supported.
* @hide
*/
+ @TestApi
public native void setBufferingParams(@NonNull BufferingParams params);
/**
@@ -2128,7 +2131,13 @@ public class MediaPlayer extends PlayerBase
mTimeProvider.close();
mTimeProvider = null;
}
- mOnSubtitleDataListener = null;
+ synchronized(this) {
+ mSubtitleDataListenerDisabled = false;
+ mExtSubtitleDataListener = null;
+ mExtSubtitleDataHandler = null;
+ mOnMediaTimeDiscontinuityListener = null;
+ mOnMediaTimeDiscontinuityHandler = null;
+ }
// Modular DRM clean up
mOnDrmConfigHelper = null;
@@ -2699,7 +2708,7 @@ public class MediaPlayer extends PlayerBase
private int mSelectedSubtitleTrackIndex = -1;
private Vector<InputStream> mOpenSubtitleSources;
- private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() {
+ private final OnSubtitleDataListener mIntSubtitleDataListener = new OnSubtitleDataListener() {
@Override
public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
int index = data.getTrackIndex();
@@ -2725,7 +2734,9 @@ public class MediaPlayer extends PlayerBase
}
mSelectedSubtitleTrackIndex = -1;
}
- setOnSubtitleDataListener(null);
+ synchronized (this) {
+ mSubtitleDataListenerDisabled = true;
+ }
if (track == null) {
return;
}
@@ -2745,7 +2756,9 @@ public class MediaPlayer extends PlayerBase
selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
} catch (IllegalStateException e) {
}
- setOnSubtitleDataListener(mSubtitleDataListener);
+ synchronized (this) {
+ mSubtitleDataListenerDisabled = false;
+ }
}
// no need to select out-of-band tracks
}
@@ -3304,6 +3317,7 @@ public class MediaPlayer extends PlayerBase
private static final int MEDIA_SUBTITLE_DATA = 201;
private static final int MEDIA_META_DATA = 202;
private static final int MEDIA_DRM_INFO = 210;
+ private static final int MEDIA_TIME_DISCONTINUITY = 211;
private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000;
private TimeProvider mTimeProvider;
@@ -3514,15 +3528,34 @@ public class MediaPlayer extends PlayerBase
return;
case MEDIA_SUBTITLE_DATA:
- OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
- if (onSubtitleDataListener == null) {
- return;
+ final OnSubtitleDataListener extSubtitleListener;
+ final Handler extSubtitleHandler;
+ synchronized(this) {
+ if (mSubtitleDataListenerDisabled) {
+ return;
+ }
+ extSubtitleListener = mExtSubtitleDataListener;
+ extSubtitleHandler = mExtSubtitleDataHandler;
}
if (msg.obj instanceof Parcel) {
Parcel parcel = (Parcel) msg.obj;
- SubtitleData data = new SubtitleData(parcel);
+ final SubtitleData data = new SubtitleData(parcel);
parcel.recycle();
- onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+
+ mIntSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+
+ if (extSubtitleListener != null) {
+ if (extSubtitleHandler == null) {
+ extSubtitleListener.onSubtitleData(mMediaPlayer, data);
+ } else {
+ extSubtitleHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ extSubtitleListener.onSubtitleData(mMediaPlayer, data);
+ }
+ });
+ }
+ }
}
return;
@@ -3553,6 +3586,43 @@ public class MediaPlayer extends PlayerBase
}
return;
+ case MEDIA_TIME_DISCONTINUITY:
+ final OnMediaTimeDiscontinuityListener mediaTimeListener;
+ final Handler mediaTimeHandler;
+ synchronized(this) {
+ mediaTimeListener = mOnMediaTimeDiscontinuityListener;
+ mediaTimeHandler = mOnMediaTimeDiscontinuityHandler;
+ }
+ if (mediaTimeListener == null) {
+ return;
+ }
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel) msg.obj;
+ parcel.setDataPosition(0);
+ long anchorMediaUs = parcel.readLong();
+ long anchorRealUs = parcel.readLong();
+ float playbackRate = parcel.readFloat();
+ parcel.recycle();
+ final MediaTimestamp timestamp;
+ if (anchorMediaUs != -1 && anchorRealUs != -1) {
+ timestamp = new MediaTimestamp(
+ anchorMediaUs /*Us*/, anchorRealUs * 1000 /*Ns*/, playbackRate);
+ } else {
+ timestamp = MediaTimestamp.TIMESTAMP_UNKNOWN;
+ }
+ if (mediaTimeHandler == null) {
+ mediaTimeListener.onMediaTimeDiscontinuity(mMediaPlayer, timestamp);
+ } else {
+ mediaTimeHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mediaTimeListener.onMediaTimeDiscontinuity(mMediaPlayer, timestamp);
+ }
+ });
+ }
+ }
+ return;
+
default:
Log.e(TAG, "Unknown message type " + msg.what);
return;
@@ -3877,13 +3947,15 @@ public class MediaPlayer extends PlayerBase
private void setOnSubtitleDataListenerInt(
@Nullable OnSubtitleDataListener listener, @Nullable Handler handler) {
synchronized (this) {
- mOnSubtitleDataListener = listener;
- mOnSubtitleDataHandler = handler;
+ mExtSubtitleDataListener = listener;
+ mExtSubtitleDataHandler = handler;
}
}
- private OnSubtitleDataListener mOnSubtitleDataListener;
- private Handler mOnSubtitleDataHandler;
+ private boolean mSubtitleDataListenerDisabled;
+ /** External OnSubtitleDataListener, the one set by {@link #setOnSubtitleDataListener}. */
+ private OnSubtitleDataListener mExtSubtitleDataListener;
+ private Handler mExtSubtitleDataHandler;
/**
* Interface definition of a callback to be invoked when discontinuity in the normal progression
diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java
index 90b6bff6..82d64f30 100644
--- a/android/media/MediaRecorder.java
+++ b/android/media/MediaRecorder.java
@@ -1434,7 +1434,6 @@ public class MediaRecorder implements AudioRouting
int status = native_getActiveMicrophones(activeMicrophones);
if (status != AudioManager.SUCCESS) {
Log.e(TAG, "getActiveMicrophones failed:" + status);
- return new ArrayList<MicrophoneInfo>();
}
AudioManager.setPortIdForMicrophones(activeMicrophones);
diff --git a/android/media/MediaTimestamp.java b/android/media/MediaTimestamp.java
index 938dd14a..dd43b4e0 100644
--- a/android/media/MediaTimestamp.java
+++ b/android/media/MediaTimestamp.java
@@ -98,4 +98,13 @@ public final class MediaTimestamp
&& (this.nanoTime == that.nanoTime)
&& (this.clockRate == that.clockRate);
}
+
+ @Override
+ public String toString() {
+ return getClass().getName()
+ + "{AnchorMediaTimeUs=" + mediaTimeUs
+ + " AnchorSystemNanoTime=" + nanoTime
+ + " clockRate=" + clockRate
+ + "}";
+ }
}
diff --git a/android/media/PlaybackParams.java b/android/media/PlaybackParams.java
index 938a953a..b85e4d01 100644
--- a/android/media/PlaybackParams.java
+++ b/android/media/PlaybackParams.java
@@ -17,6 +17,7 @@
package android.media;
import android.annotation.IntDef;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -151,6 +152,7 @@ public final class PlaybackParams implements Parcelable {
* @param audioStretchMode
* @return this <code>PlaybackParams</code> instance.
*/
+ @TestApi
public PlaybackParams setAudioStretchMode(@AudioStretchMode int audioStretchMode) {
mAudioStretchMode = audioStretchMode;
mSet |= SET_AUDIO_STRETCH_MODE;
@@ -163,6 +165,7 @@ public final class PlaybackParams implements Parcelable {
* @return audio stretch mode
* @throws IllegalStateException if the audio stretch mode is not set.
*/
+ @TestApi
public @AudioStretchMode int getAudioStretchMode() {
if ((mSet & SET_AUDIO_STRETCH_MODE) == 0) {
throw new IllegalStateException("audio stretch mode not set");
diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java
index 80049ba5..7c6367e8 100644
--- a/android/media/PlayerBase.java
+++ b/android/media/PlayerBase.java
@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
@@ -58,20 +59,29 @@ public abstract class PlayerBase {
protected float mRightVolume = 1.0f;
protected float mAuxEffectSendLevel = 0.0f;
+ // NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in
+ // the same process as AudioService, which can synchronously call back into this class,
+ // causing deadlocks between the two
+ private final Object mLock = new Object();
+
// for AppOps
- private IAppOpsService mAppOps; // may be null
+ private @Nullable IAppOpsService mAppOps;
private IAppOpsCallback mAppOpsCallback;
- private boolean mHasAppOpsPlayAudio = true; // sync'd on mLock
- private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private boolean mHasAppOpsPlayAudio = true;
private final int mImplType;
// uniquely identifies the Player Interface throughout the system (P I Id)
- private int mPlayerIId;
+ private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_UNASSIGNED;
- private int mState; // sync'd on mLock
- private int mStartDelayMs = 0; // sync'd on mLock
- private float mPanMultiplierL = 1.0f; // sync'd on mLock
- private float mPanMultiplierR = 1.0f; // sync'd on mLock
+ @GuardedBy("mLock")
+ private int mState;
+ @GuardedBy("mLock")
+ private int mStartDelayMs = 0;
+ @GuardedBy("mLock")
+ private float mPanMultiplierL = 1.0f;
+ @GuardedBy("mLock")
+ private float mPanMultiplierR = 1.0f;
/**
* Constructor. Must be given audio attributes, as they are required for AppOps.
@@ -134,16 +144,24 @@ public abstract class PlayerBase {
}
}
- void baseStart() {
- if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); }
+ private void updateState(int state) {
+ final int piid;
+ synchronized (mLock) {
+ mState = state;
+ piid = mPlayerIId;
+ }
try {
- synchronized (mLock) {
- mState = AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
- getService().playerEvent(mPlayerIId, mState);
- }
+ getService().playerEvent(piid, state);
} catch (RemoteException e) {
- Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e);
+ Log.e(TAG, "Error talking to audio service, "
+ + AudioPlaybackConfiguration.toLogFriendlyPlayerState(state)
+ + " state will not be tracked for piid=" + piid, e);
}
+ }
+
+ void baseStart() {
+ if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); }
+ updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
synchronized (mLock) {
if (isRestricted_sync()) {
playerSetVolume(true/*muting*/,0, 0);
@@ -165,26 +183,12 @@ public abstract class PlayerBase {
void basePause() {
if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); }
- try {
- synchronized (mLock) {
- mState = AudioPlaybackConfiguration.PLAYER_STATE_PAUSED;
- getService().playerEvent(mPlayerIId, mState);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error talking to audio service, PAUSED state will not be tracked", e);
- }
+ updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED);
}
void baseStop() {
if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); }
- try {
- synchronized (mLock) {
- mState = AudioPlaybackConfiguration.PLAYER_STATE_STOPPED;
- getService().playerEvent(mPlayerIId, mState);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error talking to audio service, STOPPED state will not be tracked", e);
- }
+ updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED);
}
void baseSetPan(float pan) {
@@ -228,12 +232,16 @@ public abstract class PlayerBase {
*/
void baseRelease() {
if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); }
+ boolean releasePlayer = false;
+ synchronized (mLock) {
+ if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
+ releasePlayer = true;
+ mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
+ }
+ }
try {
- synchronized (mLock) {
- if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
- getService().releasePlayer(mPlayerIId);
- mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
- }
+ if (releasePlayer) {
+ getService().releasePlayer(mPlayerIId);
}
} catch (RemoteException e) {
Log.e(TAG, "Error talking to audio service, the player will still be tracked", e);
diff --git a/android/media/VolumeShaper.java b/android/media/VolumeShaper.java
index 30687065..b654214c 100644
--- a/android/media/VolumeShaper.java
+++ b/android/media/VolumeShaper.java
@@ -18,6 +18,7 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -843,6 +844,7 @@ public final class VolumeShaper implements AutoCloseable {
* @return the same {@code Builder} instance.
* @throws IllegalArgumentException if flag is not recognized.
*/
+ @TestApi
public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
diff --git a/android/media/audiofx/AudioEffect.java b/android/media/audiofx/AudioEffect.java
index 21d68737..24c595f5 100644
--- a/android/media/audiofx/AudioEffect.java
+++ b/android/media/audiofx/AudioEffect.java
@@ -18,6 +18,7 @@ package android.media.audiofx;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.TestApi;
import android.app.ActivityThread;
import android.os.Handler;
import android.os.Looper;
@@ -133,9 +134,10 @@ public class AudioEffect {
.fromString("7261676f-6d75-7369-6364-28e2fd3ac39e");
/**
- * Null effect UUID. Used when the UUID for effect type of
+ * Null effect UUID. See {@link AudioEffect(UUID, UUID, int, int)} for use.
* @hide
*/
+ @TestApi
public static final UUID EFFECT_TYPE_NULL = UUID
.fromString("ec7178ec-e5e1-4432-a3f4-4657e6795210");
@@ -492,6 +494,7 @@ public class AudioEffect {
* @return true if the device implements the specified effect type, false otherwise.
* @hide
*/
+ @TestApi
public static boolean isEffectTypeAvailable(UUID type) {
AudioEffect.Descriptor[] desc = AudioEffect.queryEffects();
if (desc == null) {
@@ -544,6 +547,7 @@ public class AudioEffect {
* @throws IllegalStateException
* @hide
*/
+ @TestApi
public int setParameter(byte[] param, byte[] value)
throws IllegalStateException {
checkState("setParameter()");
@@ -556,6 +560,7 @@ public class AudioEffect {
* @see #setParameter(byte[], byte[])
* @hide
*/
+ @TestApi
public int setParameter(int param, int value) throws IllegalStateException {
byte[] p = intToByteArray(param);
byte[] v = intToByteArray(value);
@@ -569,6 +574,7 @@ public class AudioEffect {
* @see #setParameter(byte[], byte[])
* @hide
*/
+ @TestApi
public int setParameter(int param, short value)
throws IllegalStateException {
byte[] p = intToByteArray(param);
@@ -583,6 +589,7 @@ public class AudioEffect {
* @see #setParameter(byte[], byte[])
* @hide
*/
+ @TestApi
public int setParameter(int param, byte[] value)
throws IllegalStateException {
byte[] p = intToByteArray(param);
@@ -596,6 +603,7 @@ public class AudioEffect {
* @see #setParameter(byte[], byte[])
* @hide
*/
+ @TestApi
public int setParameter(int[] param, int[] value)
throws IllegalStateException {
if (param.length > 2 || value.length > 2) {
@@ -647,6 +655,7 @@ public class AudioEffect {
* @see #setParameter(byte[], byte[])
* @hide
*/
+ @TestApi
public int setParameter(int[] param, byte[] value)
throws IllegalStateException {
if (param.length > 2) {
@@ -675,6 +684,7 @@ public class AudioEffect {
* @throws IllegalStateException
* @hide
*/
+ @TestApi
public int getParameter(byte[] param, byte[] value)
throws IllegalStateException {
checkState("getParameter()");
@@ -688,6 +698,7 @@ public class AudioEffect {
* @see #getParameter(byte[], byte[])
* @hide
*/
+ @TestApi
public int getParameter(int param, byte[] value)
throws IllegalStateException {
byte[] p = intToByteArray(param);
@@ -703,6 +714,7 @@ public class AudioEffect {
* In case of success, returns the number of meaningful integers in value array.
* @hide
*/
+ @TestApi
public int getParameter(int param, int[] value)
throws IllegalStateException {
if (value.length > 2) {
@@ -734,6 +746,7 @@ public class AudioEffect {
* In case of success, returns the number of meaningful short integers in value array.
* @hide
*/
+ @TestApi
public int getParameter(int param, short[] value)
throws IllegalStateException {
if (value.length > 2) {
@@ -799,6 +812,7 @@ public class AudioEffect {
* In case of success, returns the number of meaningful short integers in value array.
* @hide
*/
+ @TestApi
public int getParameter(int[] param, short[] value)
throws IllegalStateException {
if (param.length > 2 || value.length > 2) {
@@ -938,6 +952,7 @@ public class AudioEffect {
* @param listener
* @hide
*/
+ @TestApi
public void setParameterListener(OnParameterChangeListener listener) {
synchronized (mListenerLock) {
mParameterChangeListener = listener;
@@ -999,6 +1014,7 @@ public class AudioEffect {
* when a parameter is changed in the effect engine by the controlling application.
* @hide
*/
+ @TestApi
public interface OnParameterChangeListener {
/**
* Called on the listener to notify it that a parameter value has changed.
@@ -1291,6 +1307,7 @@ public class AudioEffect {
/**
* @hide
*/
+ @TestApi
public static boolean isError(int status) {
return (status < 0);
}
@@ -1298,6 +1315,7 @@ public class AudioEffect {
/**
* @hide
*/
+ @TestApi
public static int byteArrayToInt(byte[] valueBuf) {
return byteArrayToInt(valueBuf, 0);
@@ -1316,6 +1334,7 @@ public class AudioEffect {
/**
* @hide
*/
+ @TestApi
public static byte[] intToByteArray(int value) {
ByteBuffer converter = ByteBuffer.allocate(4);
converter.order(ByteOrder.nativeOrder());
@@ -1326,6 +1345,7 @@ public class AudioEffect {
/**
* @hide
*/
+ @TestApi
public static short byteArrayToShort(byte[] valueBuf) {
return byteArrayToShort(valueBuf, 0);
}
@@ -1343,6 +1363,7 @@ public class AudioEffect {
/**
* @hide
*/
+ @TestApi
public static byte[] shortToByteArray(short value) {
ByteBuffer converter = ByteBuffer.allocate(2);
converter.order(ByteOrder.nativeOrder());
diff --git a/android/media/session/MediaController.java b/android/media/session/MediaController.java
index f16804c9..84f85e78 100644
--- a/android/media/session/MediaController.java
+++ b/android/media/session/MediaController.java
@@ -531,7 +531,7 @@ public final class MediaController {
*
* @param state The new playback state of the session
*/
- public void onPlaybackStateChanged(@NonNull PlaybackState state) {
+ public void onPlaybackStateChanged(@Nullable PlaybackState state) {
}
/**
diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java
index 519af1ba..fbc14384 100644
--- a/android/media/session/MediaSessionManager.java
+++ b/android/media/session/MediaSessionManager.java
@@ -47,6 +47,7 @@ import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -342,12 +343,16 @@ public final class MediaSessionManager {
}
/**
- * Returns whether the app is trusted.
+ * Checks whether the remote user is a trusted app.
* <p>
* An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL
* permission or has an enabled notification listener.
*
- * @param userInfo The remote user info
+ * @param userInfo The remote user info from either
+ * {@link MediaSession#getCurrentControllerInfo()} or
+ * {@link MediaBrowserService#getCurrentBrowserInfo()}.
+ * @return {@code true} if the remote user is trusted and its package name matches with the UID.
+ * {@code false} otherwise.
*/
public boolean isTrustedForMediaControl(RemoteUserInfo userInfo) {
if (userInfo.getPackageName() == null) {
@@ -814,6 +819,11 @@ public final class MediaSessionManager {
&& mPid == otherUserInfo.mPid
&& mUid == otherUserInfo.mUid;
}
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mPid, mUid);
+ }
}
private static final class SessionsChangedWrapper {
diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java
index 15255083..a61ea50d 100644
--- a/android/net/IpSecManager.java
+++ b/android/net/IpSecManager.java
@@ -20,7 +20,6 @@ import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;
@@ -140,6 +139,7 @@ public final class IpSecManager {
}
}
+ private final Context mContext;
private final IIpSecService mService;
/**
@@ -336,6 +336,9 @@ public final class IpSecManager {
*/
public void applyTransportModeTransform(@NonNull Socket socket,
@PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+ // Ensure creation of FD. See b/77548890 for more details.
+ socket.getSoLinger();
+
applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
}
@@ -440,6 +443,9 @@ public final class IpSecManager {
* @throws IOException indicating that the transform could not be removed from the socket
*/
public void removeTransportModeTransforms(@NonNull Socket socket) throws IOException {
+ // Ensure creation of FD. See b/77548890 for more details.
+ socket.getSoLinger();
+
removeTransportModeTransforms(socket.getFileDescriptor$());
}
@@ -659,8 +665,8 @@ public final class IpSecManager {
* to create Network objects which are accessible to the Android system.
* @hide
*/
- @SystemApi
public static final class IpSecTunnelInterface implements AutoCloseable {
+ private final String mOpPackageName;
private final IIpSecService mService;
private final InetAddress mRemoteAddress;
private final InetAddress mLocalAddress;
@@ -682,13 +688,14 @@ public final class IpSecManager {
* tunneled traffic.
*
* @param address the local address for traffic inside the tunnel
+ * @param prefixLen length of the InetAddress prefix
* @hide
*/
- @SystemApi
@RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
- public void addAddress(@NonNull LinkAddress address) throws IOException {
+ public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
try {
- mService.addAddressToTunnelInterface(mResourceId, address);
+ mService.addAddressToTunnelInterface(
+ mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -700,22 +707,24 @@ public final class IpSecManager {
* <p>Remove an address which was previously added to the IpSecTunnelInterface
*
* @param address to be removed
+ * @param prefixLen length of the InetAddress prefix
* @hide
*/
- @SystemApi
@RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
- public void removeAddress(@NonNull LinkAddress address) throws IOException {
+ public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
try {
- mService.removeAddressFromTunnelInterface(mResourceId, address);
+ mService.removeAddressFromTunnelInterface(
+ mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
- private IpSecTunnelInterface(@NonNull IIpSecService service,
+ private IpSecTunnelInterface(@NonNull Context ctx, @NonNull IIpSecService service,
@NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress,
@NonNull Network underlyingNetwork)
throws ResourceUnavailableException, IOException {
+ mOpPackageName = ctx.getOpPackageName();
mService = service;
mLocalAddress = localAddress;
mRemoteAddress = remoteAddress;
@@ -727,7 +736,8 @@ public final class IpSecManager {
localAddress.getHostAddress(),
remoteAddress.getHostAddress(),
underlyingNetwork,
- new Binder());
+ new Binder(),
+ mOpPackageName);
switch (result.status) {
case Status.OK:
break;
@@ -756,7 +766,7 @@ public final class IpSecManager {
@Override
public void close() {
try {
- mService.deleteTunnelInterface(mResourceId);
+ mService.deleteTunnelInterface(mResourceId, mOpPackageName);
mResourceId = INVALID_RESOURCE_ID;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -795,13 +805,13 @@ public final class IpSecManager {
* @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
* @hide
*/
- @SystemApi
@NonNull
@RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress,
@NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork)
throws ResourceUnavailableException, IOException {
- return new IpSecTunnelInterface(mService, localAddress, remoteAddress, underlyingNetwork);
+ return new IpSecTunnelInterface(
+ mContext, mService, localAddress, remoteAddress, underlyingNetwork);
}
/**
@@ -821,13 +831,13 @@ public final class IpSecManager {
* layer failure.
* @hide
*/
- @SystemApi
@RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel,
@PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
try {
mService.applyTunnelModeTransform(
- tunnel.getResourceId(), direction, transform.getResourceId());
+ tunnel.getResourceId(), direction,
+ transform.getResourceId(), mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -839,7 +849,8 @@ public final class IpSecManager {
* @param context the application context for this manager
* @hide
*/
- public IpSecManager(IIpSecService service) {
+ public IpSecManager(Context ctx, IIpSecService service) {
+ mContext = ctx;
mService = checkNotNull(service, "missing service");
}
}
diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java
index 099fe02f..62f79965 100644
--- a/android/net/IpSecTransform.java
+++ b/android/net/IpSecTransform.java
@@ -22,7 +22,6 @@ import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
import android.content.Context;
import android.os.Binder;
import android.os.Handler;
@@ -130,7 +129,8 @@ public final class IpSecTransform implements AutoCloseable {
synchronized (this) {
try {
IIpSecService svc = getIpSecService();
- IpSecTransformResponse result = svc.createTransform(mConfig, new Binder());
+ IpSecTransformResponse result = svc.createTransform(
+ mConfig, new Binder(), mContext.getOpPackageName());
int status = result.status;
checkResultStatus(status);
mResourceId = result.resourceId;
@@ -249,7 +249,6 @@ public final class IpSecTransform implements AutoCloseable {
*
* @hide
*/
- @SystemApi
public static class NattKeepaliveCallback {
/** The specified {@code Network} is not connected. */
public static final int ERROR_INVALID_NETWORK = 1;
@@ -280,7 +279,6 @@ public final class IpSecTransform implements AutoCloseable {
*
* @hide
*/
- @SystemApi
@RequiresPermission(anyOf = {
android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
@@ -323,7 +321,6 @@ public final class IpSecTransform implements AutoCloseable {
*
* @hide
*/
- @SystemApi
@RequiresPermission(anyOf = {
android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
@@ -476,7 +473,6 @@ public final class IpSecTransform implements AutoCloseable {
* @throws IOException indicating other errors
* @hide
*/
- @SystemApi
@NonNull
@RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
public IpSecTransform buildTunnelModeTransform(
diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java
index 374b3abc..a8e81791 100644
--- a/android/net/NetworkCapabilities.java
+++ b/android/net/NetworkCapabilities.java
@@ -254,9 +254,8 @@ public final class NetworkCapabilities implements Parcelable {
/**
* Indicates that this network is not congested.
* <p>
- * When a network is congested, the device should defer network traffic that
- * can be done at a later time without breaking developer contracts.
- * @hide
+ * When a network is congested, applications should defer network traffic
+ * that can be done at a later time, such as uploading analytics.
*/
public static final int NET_CAPABILITY_NOT_CONGESTED = 20;
@@ -318,7 +317,7 @@ public final class NetworkCapabilities implements Parcelable {
/**
* Capabilities that suggest that a network is restricted.
- * {@see #maybeMarkCapabilitiesRestricted}.
+ * {@see #maybeMarkCapabilitiesRestricted}, {@see #FORCE_RESTRICTED_CAPABILITIES}
*/
@VisibleForTesting
/* package */ static final long RESTRICTED_CAPABILITIES =
@@ -329,7 +328,13 @@ public final class NetworkCapabilities implements Parcelable {
(1 << NET_CAPABILITY_IA) |
(1 << NET_CAPABILITY_IMS) |
(1 << NET_CAPABILITY_RCS) |
- (1 << NET_CAPABILITY_XCAP) |
+ (1 << NET_CAPABILITY_XCAP);
+
+ /**
+ * Capabilities that force network to be restricted.
+ * {@see #maybeMarkCapabilitiesRestricted}.
+ */
+ private static final long FORCE_RESTRICTED_CAPABILITIES =
(1 << NET_CAPABILITY_OEM_PAID);
/**
@@ -533,16 +538,21 @@ public final class NetworkCapabilities implements Parcelable {
* @hide
*/
public void maybeMarkCapabilitiesRestricted() {
+ // Check if we have any capability that forces the network to be restricted.
+ final boolean forceRestrictedCapability =
+ (mNetworkCapabilities & FORCE_RESTRICTED_CAPABILITIES) != 0;
+
// Verify there aren't any unrestricted capabilities. If there are we say
- // the whole thing is unrestricted.
+ // the whole thing is unrestricted unless it is forced to be restricted.
final boolean hasUnrestrictedCapabilities =
- ((mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0);
+ (mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0;
// Must have at least some restricted capabilities.
final boolean hasRestrictedCapabilities =
- ((mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0);
+ (mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0;
- if (hasRestrictedCapabilities && !hasUnrestrictedCapabilities) {
+ if (forceRestrictedCapability
+ || (hasRestrictedCapabilities && !hasUnrestrictedCapabilities)) {
removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
}
}
diff --git a/android/net/NetworkPolicy.java b/android/net/NetworkPolicy.java
index 1a28732e..e84c85ee 100644
--- a/android/net/NetworkPolicy.java
+++ b/android/net/NetworkPolicy.java
@@ -19,7 +19,7 @@ package android.net;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.BackupUtils;
-import android.util.Pair;
+import android.util.Range;
import android.util.RecurrenceRule;
import com.android.internal.util.Preconditions;
@@ -136,7 +136,7 @@ public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
return 0;
}
- public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+ public Iterator<Range<ZonedDateTime>> cycleIterator() {
return cycleRule.cycleIterator();
}
diff --git a/android/net/NetworkPolicyManager.java b/android/net/NetworkPolicyManager.java
index bf6b7e09..6546c391 100644
--- a/android/net/NetworkPolicyManager.java
+++ b/android/net/NetworkPolicyManager.java
@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.util.DebugUtils;
import android.util.Pair;
+import android.util.Range;
import com.google.android.collect.Sets;
@@ -258,8 +259,21 @@ public class NetworkPolicyManager {
}
/** {@hide} */
+ @Deprecated
public static Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator(NetworkPolicy policy) {
- return policy.cycleIterator();
+ final Iterator<Range<ZonedDateTime>> it = policy.cycleIterator();
+ return new Iterator<Pair<ZonedDateTime, ZonedDateTime>>() {
+ @Override
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ @Override
+ public Pair<ZonedDateTime, ZonedDateTime> next() {
+ final Range<ZonedDateTime> r = it.next();
+ return Pair.create(r.getLower(), r.getUpper());
+ }
+ };
}
/**
diff --git a/android/net/NetworkRequest.java b/android/net/NetworkRequest.java
index 3d9d6e29..bd4a27c2 100644
--- a/android/net/NetworkRequest.java
+++ b/android/net/NetworkRequest.java
@@ -168,9 +168,6 @@ public class NetworkRequest implements Parcelable {
* the requested network's required capabilities. Note that when searching
* for a network to satisfy a request, all capabilities requested must be
* satisfied.
- * <p>
- * If the given capability was previously added to the list of unwanted capabilities
- * then the capability will also be removed from the list of unwanted capabilities.
*
* @param capability The capability to add.
* @return The builder to facilitate chaining
@@ -182,8 +179,7 @@ public class NetworkRequest implements Parcelable {
}
/**
- * Removes (if found) the given capability from this builder instance from both required
- * and unwanted capabilities lists.
+ * Removes (if found) the given capability from this builder instance.
*
* @param capability The capability to remove.
* @return The builder to facilitate chaining.
@@ -231,6 +227,8 @@ public class NetworkRequest implements Parcelable {
*
* @param capability The capability to add to unwanted capability list.
* @return The builder to facilitate chaining.
+ *
+ * @removed
*/
public Builder addUnwantedCapability(@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.addUnwantedCapability(capability);
@@ -436,6 +434,15 @@ public class NetworkRequest implements Parcelable {
}
/**
+ * @see Builder#addUnwantedCapability(int)
+ *
+ * @removed
+ */
+ public boolean hasUnwantedCapability(@NetCapability int capability) {
+ return networkCapabilities.hasUnwantedCapability(capability);
+ }
+
+ /**
* @see Builder#addTransportType(int)
*/
public boolean hasTransport(@Transport int transportType) {
diff --git a/android/net/NetworkState.java b/android/net/NetworkState.java
index b00cb482..321f9718 100644
--- a/android/net/NetworkState.java
+++ b/android/net/NetworkState.java
@@ -26,6 +26,8 @@ import android.util.Slog;
* @hide
*/
public class NetworkState implements Parcelable {
+ private static final boolean SANITY_CHECK_ROAMING = false;
+
public static final NetworkState EMPTY = new NetworkState(null, null, null, null, null, null);
public final NetworkInfo networkInfo;
@@ -47,7 +49,7 @@ public class NetworkState implements Parcelable {
// This object is an atomic view of a network, so the various components
// should always agree on roaming state.
- if (networkInfo != null && networkCapabilities != null) {
+ if (SANITY_CHECK_ROAMING && networkInfo != null && networkCapabilities != null) {
if (networkInfo.isRoaming() == networkCapabilities
.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) {
Slog.wtf("NetworkState", "Roaming state disagreement between " + networkInfo
diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java
index d1904322..d5ff2dd6 100644
--- a/android/net/apf/ApfFilter.java
+++ b/android/net/apf/ApfFilter.java
@@ -16,21 +16,21 @@
package android.net.apf;
+import static android.net.util.NetworkConstants.*;
import static android.system.OsConstants.*;
-
import static com.android.internal.util.BitUtils.bytesToBEInt;
import static com.android.internal.util.BitUtils.getUint16;
import static com.android.internal.util.BitUtils.getUint32;
import static com.android.internal.util.BitUtils.getUint8;
-import static com.android.internal.util.BitUtils.uint16;
import static com.android.internal.util.BitUtils.uint32;
-import static com.android.internal.util.BitUtils.uint8;
-import android.os.SystemClock;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkUtils;
-import android.net.apf.ApfGenerator;
import android.net.apf.ApfGenerator.IllegalInstructionException;
import android.net.apf.ApfGenerator.Register;
import android.net.ip.IpClient;
@@ -39,31 +39,29 @@ import android.net.metrics.ApfStats;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.RaEvent;
import android.net.util.InterfaceParams;
+import android.os.PowerManager;
+import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.system.PacketSocketAddress;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.Pair;
-
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.internal.util.IndentingPrintWriter;
-
import java.io.FileDescriptor;
import java.io.IOException;
-import java.lang.Thread;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
-
import libcore.io.IoBridge;
/**
@@ -215,10 +213,6 @@ public class ApfFilter {
{ (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
- private static final int ICMP6_ROUTER_SOLICITATION = 133;
- private static final int ICMP6_ROUTER_ADVERTISEMENT = 134;
- private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
- private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
// NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
@@ -258,9 +252,26 @@ public class ApfFilter {
private long mUniqueCounter;
@GuardedBy("this")
private boolean mMulticastFilter;
+ @GuardedBy("this")
+ private boolean mInDozeMode;
private final boolean mDrop802_3Frames;
private final int[] mEthTypeBlackList;
+ // Detects doze mode state transitions.
+ private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) {
+ PowerManager powerManager =
+ (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ final boolean deviceIdle = powerManager.isDeviceIdleMode();
+ setDozeMode(deviceIdle);
+ }
+ }
+ };
+ private final Context mContext;
+
// Our IPv4 address, if we have just one, otherwise null.
@GuardedBy("this")
private byte[] mIPv4Address;
@@ -269,13 +280,14 @@ public class ApfFilter {
private int mIPv4PrefixLength;
@VisibleForTesting
- ApfFilter(ApfConfiguration config, InterfaceParams ifParams,
+ ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
IpClient.Callback ipClientCallback, IpConnectivityLog log) {
mApfCapabilities = config.apfCapabilities;
mIpClientCallback = ipClientCallback;
mInterfaceParams = ifParams;
mMulticastFilter = config.multicastFilter;
mDrop802_3Frames = config.ieee802_3Filter;
+ mContext = context;
// Now fill the black list from the passed array
mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
@@ -284,6 +296,10 @@ public class ApfFilter {
// TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
maybeStartFilter();
+
+ // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
+ mContext.registerReceiver(mDeviceIdleReceiver,
+ new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
}
private void log(String s) {
@@ -522,7 +538,7 @@ public class ApfFilter {
// to our packet socket. b/29586253
if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
- getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMP6_ROUTER_ADVERTISEMENT) {
+ getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) {
throw new InvalidRaException("Not an ICMP6 router advertisement");
}
@@ -889,10 +905,11 @@ public class ApfFilter {
private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
// Here's a basic summary of what the IPv6 filter program does:
//
- // if it's not ICMPv6:
- // if it's multicast and we're dropping multicast:
- // drop
- // pass
+ // if we're dropping multicast
+ // if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
+ // if it's multicast:
+ // drop
+ // pass
// if it's ICMPv6 RS to any:
// drop
// if it's ICMPv6 NA to ff02::1:
@@ -902,28 +919,44 @@ public class ApfFilter {
// Drop multicast if the multicast filter is enabled.
if (mMulticastFilter) {
- // Don't touch ICMPv6 multicast here, we deal with it in more detail later.
- String skipIpv6MulticastFilterLabel = "skipIPv6MulticastFilter";
- gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIpv6MulticastFilterLabel);
+ final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter";
+ final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast";
+
+ // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
+ // While awake, let all ICMPv6 multicasts through.
+ if (mInDozeMode) {
+ // Not ICMPv6? -> Proceed to multicast filtering
+ gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel);
+
+ // ICMPv6 but not ECHO? -> Skip the multicast filter.
+ // (ICMPv6 ECHO requests will go through the multicast filter below).
+ gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+ gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
+ } else {
+ gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
+ }
- // Drop all other packets sent to ff00::/8.
+ // Drop all other packets sent to ff00::/8 (multicast prefix).
+ gen.defineLabel(dropAllIPv6MulticastsLabel);
gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET);
gen.addJumpIfR0Equals(0xff, gen.DROP_LABEL);
- // Not multicast and not ICMPv6. Pass.
+ // Not multicast. Pass.
gen.addJump(gen.PASS_LABEL);
- gen.defineLabel(skipIpv6MulticastFilterLabel);
+ gen.defineLabel(skipIPv6MulticastFilterLabel);
} else {
// If not ICMPv6, pass.
gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, gen.PASS_LABEL);
}
+ // If we got this far, the packet is ICMPv6. Drop some specific types.
+
// Add unsolicited multicast neighbor announcements filter
String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
// Drop all router solicitations (b/32833400)
- gen.addJumpIfR0Equals(ICMP6_ROUTER_SOLICITATION, gen.DROP_LABEL);
+ gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, gen.DROP_LABEL);
// If not neighbor announcements, skip filter.
- gen.addJumpIfR0NotEquals(ICMP6_NEIGHBOR_ANNOUNCEMENT, skipUnsolicitedMulticastNALabel);
+ gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
// If to ff02::1, drop.
// TODO: Drop only if they don't contain the address of on-link neighbours.
gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
@@ -1168,9 +1201,9 @@ public class ApfFilter {
* Create an {@link ApfFilter} if {@code apfCapabilities} indicates support for packet
* filtering using APF programs.
*/
- public static ApfFilter maybeCreate(ApfConfiguration config,
+ public static ApfFilter maybeCreate(Context context, ApfConfiguration config,
InterfaceParams ifParams, IpClient.Callback ipClientCallback) {
- if (config == null || ifParams == null) return null;
+ if (context == null || config == null || ifParams == null) return null;
ApfCapabilities apfCapabilities = config.apfCapabilities;
if (apfCapabilities == null) return null;
if (apfCapabilities.apfVersionSupported == 0) return null;
@@ -1187,7 +1220,8 @@ public class ApfFilter {
Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
return null;
}
- return new ApfFilter(config, ifParams, ipClientCallback, new IpConnectivityLog());
+
+ return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
}
public synchronized void shutdown() {
@@ -1197,12 +1231,11 @@ public class ApfFilter {
mReceiveThread = null;
}
mRas.clear();
+ mContext.unregisterReceiver(mDeviceIdleReceiver);
}
public synchronized void setMulticastFilter(boolean isEnabled) {
- if (mMulticastFilter == isEnabled) {
- return;
- }
+ if (mMulticastFilter == isEnabled) return;
mMulticastFilter = isEnabled;
if (!isEnabled) {
mNumProgramUpdatesAllowingMulticast++;
@@ -1210,6 +1243,13 @@ public class ApfFilter {
installNewProgramLocked();
}
+ @VisibleForTesting
+ public synchronized void setDozeMode(boolean isEnabled) {
+ if (mInDozeMode == isEnabled) return;
+ mInDozeMode = isEnabled;
+ installNewProgramLocked();
+ }
+
/** Find the single IPv4 LinkAddress if there is one, otherwise return null. */
private static LinkAddress findIPv4LinkAddress(LinkProperties lp) {
LinkAddress ipv4Address = null;
diff --git a/android/net/dns/ResolvUtil.java b/android/net/dns/ResolvUtil.java
new file mode 100644
index 00000000..97d20f4b
--- /dev/null
+++ b/android/net/dns/ResolvUtil.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.dns;
+
+import android.net.Network;
+import android.net.NetworkUtils;
+import android.system.GaiException;
+import android.system.OsConstants;
+import android.system.StructAddrinfo;
+
+import libcore.io.Libcore;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+
+/**
+ * DNS resolution utility class.
+ *
+ * @hide
+ */
+public class ResolvUtil {
+ // Non-portable DNS resolution flag.
+ private static final long NETID_USE_LOCAL_NAMESERVERS = 0x80000000L;
+
+ private ResolvUtil() {}
+
+ public static InetAddress[] blockingResolveAllLocally(Network network, String name)
+ throws UnknownHostException {
+ final StructAddrinfo hints = new StructAddrinfo();
+ // Unnecessary, but expressly no AI_ADDRCONFIG.
+ hints.ai_flags = 0;
+ // Fetch all IP addresses at once to minimize re-resolution.
+ hints.ai_family = OsConstants.AF_UNSPEC;
+ hints.ai_socktype = OsConstants.SOCK_DGRAM;
+
+ final Network networkForResolv = getNetworkWithUseLocalNameserversFlag(network);
+
+ try {
+ return Libcore.os.android_getaddrinfo(name, hints, (int) networkForResolv.netId);
+ } catch (GaiException gai) {
+ gai.rethrowAsUnknownHostException(name + ": TLS-bypass resolution failed");
+ return null; // keep compiler quiet
+ }
+ }
+
+ public static Network getNetworkWithUseLocalNameserversFlag(Network network) {
+ final long netidForResolv = NETID_USE_LOCAL_NAMESERVERS | (long) network.netId;
+ return new Network((int) netidForResolv);
+ }
+}
diff --git a/android/net/http/X509TrustManagerExtensions.java b/android/net/http/X509TrustManagerExtensions.java
index e0fa63a5..f9b6dfce 100644
--- a/android/net/http/X509TrustManagerExtensions.java
+++ b/android/net/http/X509TrustManagerExtensions.java
@@ -21,7 +21,6 @@ import android.security.net.config.UserCertificateSource;
import com.android.org.conscrypt.TrustManagerImpl;
-import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.cert.CertificateException;
@@ -133,8 +132,6 @@ public class X509TrustManagerExtensions {
/**
* Returns {@code true} if the TrustManager uses the same trust configuration for the provided
* hostnames.
- *
- * @hide
*/
@SystemApi
public boolean isSameTrustConfiguration(String hostname1, String hostname2) {
diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java
index 9863370e..87249dfc 100644
--- a/android/net/ip/IpClient.java
+++ b/android/net/ip/IpClient.java
@@ -40,6 +40,7 @@ import android.net.util.MultinetworkPolicyTracker;
import android.net.util.NetdService;
import android.net.util.NetworkConstants;
import android.net.util.SharedLog;
+import android.os.ConditionVariable;
import android.os.INetworkManagementService;
import android.os.Message;
import android.os.RemoteException;
@@ -150,6 +151,28 @@ public class IpClient extends StateMachine {
public void setNeighborDiscoveryOffload(boolean enable) {}
}
+ public static class WaitForProvisioningCallback extends Callback {
+ private final ConditionVariable mCV = new ConditionVariable();
+ private LinkProperties mCallbackLinkProperties;
+
+ public LinkProperties waitForProvisioning() {
+ mCV.block();
+ return mCallbackLinkProperties;
+ }
+
+ @Override
+ public void onProvisioningSuccess(LinkProperties newLp) {
+ mCallbackLinkProperties = newLp;
+ mCV.open();
+ }
+
+ @Override
+ public void onProvisioningFailure(LinkProperties newLp) {
+ mCallbackLinkProperties = null;
+ mCV.open();
+ }
+ }
+
// Use a wrapper class to log in order to ensure complete and detailed
// logging. This method is lighter weight than annotations/reflection
// and has the following benefits:
@@ -281,6 +304,11 @@ public class IpClient extends StateMachine {
return this;
}
+ public Builder withoutMultinetworkPolicyTracker() {
+ mConfig.mUsingMultinetworkPolicyTracker = false;
+ return this;
+ }
+
public Builder withoutIpReachabilityMonitor() {
mConfig.mUsingIpReachabilityMonitor = false;
return this;
@@ -343,6 +371,7 @@ public class IpClient extends StateMachine {
/* package */ boolean mEnableIPv4 = true;
/* package */ boolean mEnableIPv6 = true;
+ /* package */ boolean mUsingMultinetworkPolicyTracker = true;
/* package */ boolean mUsingIpReachabilityMonitor = true;
/* package */ int mRequestedPreDhcpActionMs;
/* package */ InitialConfiguration mInitialConfig;
@@ -374,6 +403,7 @@ public class IpClient extends StateMachine {
return new StringJoiner(", ", getClass().getSimpleName() + "{", "}")
.add("mEnableIPv4: " + mEnableIPv4)
.add("mEnableIPv6: " + mEnableIPv6)
+ .add("mUsingMultinetworkPolicyTracker: " + mUsingMultinetworkPolicyTracker)
.add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
.add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
.add("mInitialConfig: " + mInitialConfig)
@@ -559,7 +589,6 @@ public class IpClient extends StateMachine {
private final NetlinkTracker mNetlinkTracker;
private final WakeupMessage mProvisioningTimeoutAlarm;
private final WakeupMessage mDhcpActionTimeoutAlarm;
- private final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
private final SharedLog mLog;
private final LocalLog mConnectivityPacketLog;
private final MessageHandlingLogger mMsgStateLogger;
@@ -573,6 +602,7 @@ public class IpClient extends StateMachine {
*/
private LinkProperties mLinkProperties;
private ProvisioningConfiguration mConfiguration;
+ private MultinetworkPolicyTracker mMultinetworkPolicyTracker;
private IpReachabilityMonitor mIpReachabilityMonitor;
private DhcpClient mDhcpClient;
private DhcpResults mDhcpResults;
@@ -685,9 +715,6 @@ public class IpClient extends StateMachine {
mLinkProperties = new LinkProperties();
mLinkProperties.setInterfaceName(mInterfaceName);
- mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(mContext, getHandler(),
- () -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
-
mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
@@ -719,8 +746,6 @@ public class IpClient extends StateMachine {
} catch (RemoteException e) {
logError("Couldn't register NetlinkTracker: %s", e);
}
-
- mMultinetworkPolicyTracker.start();
}
private void stopStateMachineUpdaters() {
@@ -729,8 +754,6 @@ public class IpClient extends StateMachine {
} catch (RemoteException e) {
logError("Couldn't unregister NetlinkTracker: %s", e);
}
-
- mMultinetworkPolicyTracker.shutdown();
}
@Override
@@ -1028,7 +1051,8 @@ public class IpClient extends StateMachine {
// Note that we can still be disconnected by IpReachabilityMonitor
// if the IPv6 default gateway (but not the IPv6 DNS servers; see
// accompanying code in IpReachabilityMonitor) is unreachable.
- final boolean ignoreIPv6ProvisioningLoss = !mMultinetworkPolicyTracker.getAvoidBadWifi();
+ final boolean ignoreIPv6ProvisioningLoss = (mMultinetworkPolicyTracker != null)
+ && !mMultinetworkPolicyTracker.getAvoidBadWifi();
// Additionally:
//
@@ -1490,7 +1514,7 @@ public class IpClient extends StateMachine {
mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
apfConfig.ethTypeBlackList =
mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
- mApfFilter = ApfFilter.maybeCreate(apfConfig, mInterfaceParams, mCallback);
+ mApfFilter = ApfFilter.maybeCreate(mContext, apfConfig, mInterfaceParams, mCallback);
// TODO: investigate the effects of any multicast filtering racing/interfering with the
// rest of this IP configuration startup.
if (mApfFilter == null) {
@@ -1520,6 +1544,13 @@ public class IpClient extends StateMachine {
return;
}
+ if (mConfiguration.mUsingMultinetworkPolicyTracker) {
+ mMultinetworkPolicyTracker = new MultinetworkPolicyTracker(
+ mContext, getHandler(),
+ () -> { mLog.log("OBSERVED AvoidBadWifi changed"); });
+ mMultinetworkPolicyTracker.start();
+ }
+
if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
doImmediateProvisioningFailure(
IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
@@ -1537,6 +1568,11 @@ public class IpClient extends StateMachine {
mIpReachabilityMonitor = null;
}
+ if (mMultinetworkPolicyTracker != null) {
+ mMultinetworkPolicyTracker.shutdown();
+ mMultinetworkPolicyTracker = null;
+ }
+
if (mDhcpClient != null) {
mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
mDhcpClient.doQuit();
diff --git a/android/net/ip/IpManager.java b/android/net/ip/IpManager.java
index 508a43d0..2eb36a22 100644
--- a/android/net/ip/IpManager.java
+++ b/android/net/ip/IpManager.java
@@ -114,35 +114,6 @@ public class IpManager extends IpClient {
public static class Callback extends IpClient.Callback {
}
- public static class WaitForProvisioningCallback extends Callback {
- private LinkProperties mCallbackLinkProperties;
-
- public LinkProperties waitForProvisioning() {
- synchronized (this) {
- try {
- wait();
- } catch (InterruptedException e) {}
- return mCallbackLinkProperties;
- }
- }
-
- @Override
- public void onProvisioningSuccess(LinkProperties newLp) {
- synchronized (this) {
- mCallbackLinkProperties = newLp;
- notify();
- }
- }
-
- @Override
- public void onProvisioningFailure(LinkProperties newLp) {
- synchronized (this) {
- mCallbackLinkProperties = null;
- notify();
- }
- }
- }
-
public IpManager(Context context, String ifName, Callback callback) {
super(context, ifName, callback);
}
diff --git a/android/net/metrics/ApfStats.java b/android/net/metrics/ApfStats.java
index 3b0dc7ef..76a781dd 100644
--- a/android/net/metrics/ApfStats.java
+++ b/android/net/metrics/ApfStats.java
@@ -20,7 +20,7 @@ import android.os.Parcel;
import android.os.Parcelable;
/**
- * An event logged for an interface with APF capabilities when its IpManager state machine exits.
+ * An event logged for an interface with APF capabilities when its IpClient state machine exits.
* {@hide}
*/
public final class ApfStats implements Parcelable {
diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java
index 984c9f81..53fd01f2 100644
--- a/android/net/util/NetworkConstants.java
+++ b/android/net/util/NetworkConstants.java
@@ -136,6 +136,8 @@ public final class NetworkConstants {
* - https://tools.ietf.org/html/rfc4861
*/
public static final int ICMPV6_HEADER_MIN_LEN = 4;
+ public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+ public static final int ICMPV6_ECHO_REPLY_TYPE = 129;
public static final int ICMPV6_ROUTER_SOLICITATION = 133;
public static final int ICMPV6_ROUTER_ADVERTISEMENT = 134;
public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135;
@@ -147,7 +149,6 @@ public final class NetworkConstants {
public static final int ICMPV6_ND_OPTION_TLLA = 2;
public static final int ICMPV6_ND_OPTION_MTU = 5;
- public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
/**
* UDP constants.
diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java
index b77b1ad5..f6c67c93 100644
--- a/android/net/wifi/WifiConfiguration.java
+++ b/android/net/wifi/WifiConfiguration.java
@@ -28,10 +28,12 @@ import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.BackupUtils;
import android.util.Log;
+import android.util.TimeUtils;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
@@ -611,37 +613,6 @@ public class WifiConfiguration implements Parcelable {
/**
* @hide
- * Last time the system tried to connect and failed.
- */
- public long lastConnectionFailure;
-
- /**
- * @hide
- * Last time the system tried to roam and failed because of authentication failure or DHCP
- * RENEW failure.
- */
- public long lastRoamingFailure;
-
- /** @hide */
- public static int ROAMING_FAILURE_IP_CONFIG = 1;
- /** @hide */
- public static int ROAMING_FAILURE_AUTH_FAILURE = 2;
-
- /**
- * @hide
- * Initial amount of time this Wifi configuration gets blacklisted for network switching
- * because of roaming failure
- */
- public long roamingFailureBlackListTimeMilli = 1000;
-
- /**
- * @hide
- * Last roaming failure reason code
- */
- public int lastRoamingFailureReason;
-
- /**
- * @hide
* Last time the system was disconnected to this configuration.
*/
public long lastDisconnected;
@@ -1620,8 +1591,9 @@ public class WifiConfiguration implements Parcelable {
}
if (mNetworkSelectionStatus.getConnectChoice() != null) {
sbuf.append(" connect choice: ").append(mNetworkSelectionStatus.getConnectChoice());
- sbuf.append(" connect choice set time: ").append(mNetworkSelectionStatus
- .getConnectChoiceTimestamp());
+ sbuf.append(" connect choice set time: ")
+ .append(TimeUtils.logTimeOfDay(
+ mNetworkSelectionStatus.getConnectChoiceTimestamp()));
}
sbuf.append(" hasEverConnected: ")
.append(mNetworkSelectionStatus.getHasEverConnected()).append("\n");
@@ -1724,7 +1696,7 @@ public class WifiConfiguration implements Parcelable {
sbuf.append(" networkSelectionBSSID="
+ mNetworkSelectionStatus.getNetworkSelectionBSSID());
}
- long now_ms = System.currentTimeMillis();
+ long now_ms = SystemClock.elapsedRealtime();
if (mNetworkSelectionStatus.getDisableTime() != NetworkSelectionStatus
.INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP) {
sbuf.append('\n');
@@ -1746,35 +1718,9 @@ public class WifiConfiguration implements Parcelable {
if (this.lastConnected != 0) {
sbuf.append('\n');
- long diff = now_ms - this.lastConnected;
- if (diff <= 0) {
- sbuf.append("lastConnected since <incorrect>");
- } else {
- sbuf.append("lastConnected: ").append(Long.toString(diff / 1000)).append("sec ");
- }
- }
- if (this.lastConnectionFailure != 0) {
- sbuf.append('\n');
- long diff = now_ms - this.lastConnectionFailure;
- if (diff <= 0) {
- sbuf.append("lastConnectionFailure since <incorrect> ");
- } else {
- sbuf.append("lastConnectionFailure: ").append(Long.toString(diff / 1000));
- sbuf.append("sec ");
- }
- }
- if (this.lastRoamingFailure != 0) {
- sbuf.append('\n');
- long diff = now_ms - this.lastRoamingFailure;
- if (diff <= 0) {
- sbuf.append("lastRoamingFailure since <incorrect> ");
- } else {
- sbuf.append("lastRoamingFailure: ").append(Long.toString(diff / 1000));
- sbuf.append("sec ");
- }
+ sbuf.append("lastConnected: ").append(TimeUtils.logTimeOfDay(this.lastConnected));
+ sbuf.append(" ");
}
- sbuf.append("roamingFailureBlackListTimeMilli: ").
- append(Long.toString(this.roamingFailureBlackListTimeMilli));
sbuf.append('\n');
if (this.linkedConfigurations != null) {
for (String key : this.linkedConfigurations.keySet()) {
@@ -2119,10 +2065,6 @@ public class WifiConfiguration implements Parcelable {
lastConnected = source.lastConnected;
lastDisconnected = source.lastDisconnected;
- lastConnectionFailure = source.lastConnectionFailure;
- lastRoamingFailure = source.lastRoamingFailure;
- lastRoamingFailureReason = source.lastRoamingFailureReason;
- roamingFailureBlackListTimeMilli = source.roamingFailureBlackListTimeMilli;
numScorerOverride = source.numScorerOverride;
numScorerOverrideAndSwitchedNetwork = source.numScorerOverrideAndSwitchedNetwork;
numAssociation = source.numAssociation;
@@ -2188,10 +2130,6 @@ public class WifiConfiguration implements Parcelable {
dest.writeInt(lastUpdateUid);
dest.writeString(creatorName);
dest.writeString(lastUpdateName);
- dest.writeLong(lastConnectionFailure);
- dest.writeLong(lastRoamingFailure);
- dest.writeInt(lastRoamingFailureReason);
- dest.writeLong(roamingFailureBlackListTimeMilli);
dest.writeInt(numScorerOverride);
dest.writeInt(numScorerOverrideAndSwitchedNetwork);
dest.writeInt(numAssociation);
@@ -2257,10 +2195,6 @@ public class WifiConfiguration implements Parcelable {
config.lastUpdateUid = in.readInt();
config.creatorName = in.readString();
config.lastUpdateName = in.readString();
- config.lastConnectionFailure = in.readLong();
- config.lastRoamingFailure = in.readLong();
- config.lastRoamingFailureReason = in.readInt();
- config.roamingFailureBlackListTimeMilli = in.readLong();
config.numScorerOverride = in.readInt();
config.numScorerOverrideAndSwitchedNetwork = in.readInt();
config.numAssociation = in.readInt();
diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java
index 433285bf..9c6c8a90 100644
--- a/android/net/wifi/WifiManager.java
+++ b/android/net/wifi/WifiManager.java
@@ -2141,7 +2141,8 @@ public class WifiManager {
}
/**
- * Sets the Wi-Fi AP Configuration.
+ * Sets the Wi-Fi AP Configuration. The AP configuration must either be open or
+ * WPA2 PSK networks.
* @return {@code true} if the operation succeeded, {@code false} otherwise
*
* @hide
@@ -2150,8 +2151,7 @@ public class WifiManager {
@RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) {
try {
- mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
- return true;
+ return mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/android/os/BatteryManager.java b/android/os/BatteryManager.java
index 63631618..954071a0 100644
--- a/android/os/BatteryManager.java
+++ b/android/os/BatteryManager.java
@@ -353,4 +353,20 @@ public class BatteryManager {
public static boolean isPlugWired(int plugType) {
return plugType == BATTERY_PLUGGED_USB || plugType == BATTERY_PLUGGED_AC;
}
+
+ /**
+ * Compute an approximation for how much time (in milliseconds) remains until the battery is
+ * fully charged. Returns -1 if no time can be computed: either there is not enough current
+ * data to make a decision or the battery is currently discharging.
+ *
+ * @return how much time is left, in milliseconds, until the battery is fully charged or -1 if
+ * the computation fails
+ */
+ public long computeChargeTimeRemaining() {
+ try {
+ return mBatteryStats.computeChargeTimeRemaining();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java
index 6ebb1026..1d232bfc 100644
--- a/android/os/BatteryStats.java
+++ b/android/os/BatteryStats.java
@@ -3814,6 +3814,9 @@ public abstract class BatteryStats implements Parcelable {
final BatterySipper bs = sippers.get(i);
String label;
switch (bs.drainType) {
+ case AMBIENT_DISPLAY:
+ label = "ambi";
+ break;
case IDLE:
label="idle";
break;
@@ -4975,6 +4978,9 @@ public abstract class BatteryStats implements Parcelable {
final BatterySipper bs = sippers.get(i);
pw.print(prefix);
switch (bs.drainType) {
+ case AMBIENT_DISPLAY:
+ pw.print(" Ambient display: ");
+ break;
case IDLE:
pw.print(" Idle: ");
break;
@@ -7777,6 +7783,9 @@ public abstract class BatteryStats implements Parcelable {
int n = SystemProto.PowerUseItem.UNKNOWN_SIPPER;
int uid = 0;
switch (bs.drainType) {
+ case AMBIENT_DISPLAY:
+ n = SystemProto.PowerUseItem.AMBIENT_DISPLAY;
+ break;
case IDLE:
n = SystemProto.PowerUseItem.IDLE;
break;
diff --git a/android/os/Build.java b/android/os/Build.java
index 8378a829..7162b8ad 100644
--- a/android/os/Build.java
+++ b/android/os/Build.java
@@ -907,6 +907,8 @@ public class Build {
* <li>{@link android.app.Service#startForeground Service.startForeground} requires
* that apps hold the permission
* {@link android.Manifest.permission#FOREGROUND_SERVICE}.</li>
+ * <li>{@link android.widget.LinearLayout} will always remeasure weighted children,
+ * even if there is no excess space.</li>
* </ul>
*/
public static final int P = CUR_DEVELOPMENT; // STOPSHIP Replace with the real version.
diff --git a/android/os/DeviceIdleManager.java b/android/os/DeviceIdleManager.java
new file mode 100644
index 00000000..9039f921
--- /dev/null
+++ b/android/os/DeviceIdleManager.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+
+/**
+ * Access to the service that keeps track of device idleness and drives low power mode based on
+ * that.
+ *
+ * @hide
+ */
+@TestApi
+@SystemService(Context.DEVICE_IDLE_CONTROLLER)
+public class DeviceIdleManager {
+ private final Context mContext;
+ private final IDeviceIdleController mService;
+
+ /**
+ * @hide
+ */
+ public DeviceIdleManager(@NonNull Context context, @NonNull IDeviceIdleController service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * @return package names the system has white-listed to opt out of power save restrictions,
+ * except for device idle mode.
+ */
+ public @NonNull String[] getSystemPowerWhitelistExceptIdle() {
+ try {
+ return mService.getSystemPowerWhitelistExceptIdle();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return new String[0];
+ }
+ }
+
+ /**
+ * @return package names the system has white-listed to opt out of power save restrictions for
+ * all modes.
+ */
+ public @NonNull String[] getSystemPowerWhitelist() {
+ try {
+ return mService.getSystemPowerWhitelist();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return new String[0];
+ }
+ }
+}
diff --git a/android/os/Environment.java b/android/os/Environment.java
index 03203d05..213260fa 100644
--- a/android/os/Environment.java
+++ b/android/os/Environment.java
@@ -16,6 +16,7 @@
package android.os;
+import android.annotation.TestApi;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.storage.StorageManager;
@@ -1033,6 +1034,7 @@ public class Environment {
*
* @hide
*/
+ @TestApi
public static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
diff --git a/android/os/MessageQueue.java b/android/os/MessageQueue.java
index 96e7a598..b1c33c2d 100644
--- a/android/os/MessageQueue.java
+++ b/android/os/MessageQueue.java
@@ -254,6 +254,7 @@ public final class MessageQueue {
} else if (record != null) {
record.mEvents = 0;
mFileDescriptorRecords.removeAt(index);
+ nativeSetFileDescriptorEvents(mPtr, fdNum, 0);
}
}
diff --git a/android/os/Parcel.java b/android/os/Parcel.java
index e3c48700..51429287 100644
--- a/android/os/Parcel.java
+++ b/android/os/Parcel.java
@@ -1857,26 +1857,7 @@ public final class Parcel {
int code = readExceptionCode();
if (code != 0) {
String msg = readString();
- String remoteStackTrace = null;
- final int remoteStackPayloadSize = readInt();
- if (remoteStackPayloadSize > 0) {
- remoteStackTrace = readString();
- }
- Exception e = createException(code, msg);
- // Attach remote stack trace if availalble
- if (remoteStackTrace != null) {
- RemoteException cause = new RemoteException(
- "Remote stack trace:\n" + remoteStackTrace, null, false, false);
- try {
- Throwable rootCause = ExceptionUtils.getRootCause(e);
- if (rootCause != null) {
- rootCause.initCause(cause);
- }
- } catch (RuntimeException ex) {
- Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex);
- }
- }
- SneakyThrow.sneakyThrow(e);
+ readException(code, msg);
}
}
@@ -1921,7 +1902,26 @@ public final class Parcel {
* @param msg The exception message.
*/
public final void readException(int code, String msg) {
- SneakyThrow.sneakyThrow(createException(code, msg));
+ String remoteStackTrace = null;
+ final int remoteStackPayloadSize = readInt();
+ if (remoteStackPayloadSize > 0) {
+ remoteStackTrace = readString();
+ }
+ Exception e = createException(code, msg);
+ // Attach remote stack trace if availalble
+ if (remoteStackTrace != null) {
+ RemoteException cause = new RemoteException(
+ "Remote stack trace:\n" + remoteStackTrace, null, false, false);
+ try {
+ Throwable rootCause = ExceptionUtils.getRootCause(e);
+ if (rootCause != null) {
+ rootCause.initCause(cause);
+ }
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Cannot set cause " + cause + " for " + e, ex);
+ }
+ }
+ SneakyThrow.sneakyThrow(e);
}
/**
diff --git a/android/os/ServiceManager.java b/android/os/ServiceManager.java
index 34c78455..165276d5 100644
--- a/android/os/ServiceManager.java
+++ b/android/os/ServiceManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,98 @@
package android.os;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BinderInternal;
+import com.android.internal.util.StatLogger;
+
+import java.util.HashMap;
import java.util.Map;
+/** @hide */
public final class ServiceManager {
+ private static final String TAG = "ServiceManager";
+ private static final Object sLock = new Object();
+
+ private static IServiceManager sServiceManager;
+
+ /**
+ * Cache for the "well known" services, such as WM and AM.
+ */
+ private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();
+
+ /**
+ * We do the "slow log" at most once every this interval.
+ */
+ private static final int SLOW_LOG_INTERVAL_MS = 5000;
+
+ /**
+ * We do the "stats log" at most once every this interval.
+ */
+ private static final int STATS_LOG_INTERVAL_MS = 5000;
+
+ /**
+ * Threshold in uS for a "slow" call, used on core UIDs. We use a more relax value to
+ * avoid logspam.
+ */
+ private static final long GET_SERVICE_SLOW_THRESHOLD_US_CORE =
+ SystemProperties.getInt("debug.servicemanager.slow_call_core_ms", 10) * 1000;
+
+ /**
+ * Threshold in uS for a "slow" call, used on non-core UIDs. We use a more relax value to
+ * avoid logspam.
+ */
+ private static final long GET_SERVICE_SLOW_THRESHOLD_US_NON_CORE =
+ SystemProperties.getInt("debug.servicemanager.slow_call_ms", 50) * 1000;
+
+ /**
+ * We log stats logging ever this many getService() calls.
+ */
+ private static final int GET_SERVICE_LOG_EVERY_CALLS_CORE =
+ SystemProperties.getInt("debug.servicemanager.log_calls_core", 100);
+
+ /**
+ * We log stats logging ever this many getService() calls.
+ */
+ private static final int GET_SERVICE_LOG_EVERY_CALLS_NON_CORE =
+ SystemProperties.getInt("debug.servicemanager.log_calls", 200);
+
+ @GuardedBy("sLock")
+ private static int sGetServiceAccumulatedUs;
+
+ @GuardedBy("sLock")
+ private static int sGetServiceAccumulatedCallCount;
+
+ @GuardedBy("sLock")
+ private static long sLastStatsLogUptime;
+
+ @GuardedBy("sLock")
+ private static long sLastSlowLogUptime;
+
+ @GuardedBy("sLock")
+ private static long sLastSlowLogActualTime;
+
+ interface Stats {
+ int GET_SERVICE = 0;
+
+ int COUNT = GET_SERVICE + 1;
+ }
+
+ public static final StatLogger sStatLogger = new StatLogger(new String[] {
+ "getService()",
+ });
+
+ private static IServiceManager getIServiceManager() {
+ if (sServiceManager != null) {
+ return sServiceManager;
+ }
+
+ // Find the service manager
+ sServiceManager = ServiceManagerNative
+ .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
+ return sServiceManager;
+ }
/**
* Returns a reference to a service with the given name.
@@ -27,14 +116,32 @@ public final class ServiceManager {
* @return a reference to the service, or <code>null</code> if the service doesn't exist
*/
public static IBinder getService(String name) {
+ try {
+ IBinder service = sCache.get(name);
+ if (service != null) {
+ return service;
+ } else {
+ return Binder.allowBlocking(rawGetService(name));
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in getService", e);
+ }
return null;
}
/**
- * Is not supposed to return null, but that is fine for layoutlib.
+ * Returns a reference to a service with the given name, or throws
+ * {@link NullPointerException} if none is found.
+ *
+ * @hide
*/
public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException {
- throw new ServiceNotFoundException(name);
+ final IBinder binder = getService(name);
+ if (binder != null) {
+ return binder;
+ } else {
+ throw new ServiceNotFoundException(name);
+ }
}
/**
@@ -45,7 +152,39 @@ public final class ServiceManager {
* @param service the service object
*/
public static void addService(String name, IBinder service) {
- // pass
+ addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
+ }
+
+ /**
+ * Place a new @a service called @a name into the service
+ * manager.
+ *
+ * @param name the name of the new service
+ * @param service the service object
+ * @param allowIsolated set to true to allow isolated sandboxed processes
+ * to access this service
+ */
+ public static void addService(String name, IBinder service, boolean allowIsolated) {
+ addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
+ }
+
+ /**
+ * Place a new @a service called @a name into the service
+ * manager.
+ *
+ * @param name the name of the new service
+ * @param service the service object
+ * @param allowIsolated set to true to allow isolated sandboxed processes
+ * @param dumpPriority supported dump priority levels as a bitmask
+ * to access this service
+ */
+ public static void addService(String name, IBinder service, boolean allowIsolated,
+ int dumpPriority) {
+ try {
+ getIServiceManager().addService(name, service, allowIsolated, dumpPriority);
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in addService", e);
+ }
}
/**
@@ -53,7 +192,17 @@ public final class ServiceManager {
* service manager. Non-blocking.
*/
public static IBinder checkService(String name) {
- return null;
+ try {
+ IBinder service = sCache.get(name);
+ if (service != null) {
+ return service;
+ } else {
+ return Binder.allowBlocking(getIServiceManager().checkService(name));
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in checkService", e);
+ return null;
+ }
}
/**
@@ -62,9 +211,12 @@ public final class ServiceManager {
* case of an exception
*/
public static String[] listServices() {
- // actual implementation returns null sometimes, so it's ok
- // to return null instead of an empty list.
- return null;
+ try {
+ return getIServiceManager().listServices(IServiceManager.DUMP_FLAG_PRIORITY_ALL);
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in listServices", e);
+ return null;
+ }
}
/**
@@ -76,7 +228,10 @@ public final class ServiceManager {
* @hide
*/
public static void initServiceCache(Map<String, IBinder> cache) {
- // pass
+ if (sCache.size() != 0) {
+ throw new IllegalStateException("setServiceCache may only be called once");
+ }
+ sCache.putAll(cache);
}
/**
@@ -87,9 +242,63 @@ public final class ServiceManager {
* @hide
*/
public static class ServiceNotFoundException extends Exception {
- // identical to the original implementation
public ServiceNotFoundException(String name) {
super("No service published for: " + name);
}
}
+
+ private static IBinder rawGetService(String name) throws RemoteException {
+ final long start = sStatLogger.getTime();
+
+ final IBinder binder = getIServiceManager().getService(name);
+
+ final int time = (int) sStatLogger.logDurationStat(Stats.GET_SERVICE, start);
+
+ final int myUid = Process.myUid();
+ final boolean isCore = UserHandle.isCore(myUid);
+
+ final long slowThreshold = isCore
+ ? GET_SERVICE_SLOW_THRESHOLD_US_CORE
+ : GET_SERVICE_SLOW_THRESHOLD_US_NON_CORE;
+
+ synchronized (sLock) {
+ sGetServiceAccumulatedUs += time;
+ sGetServiceAccumulatedCallCount++;
+
+ final long nowUptime = SystemClock.uptimeMillis();
+
+ // Was a slow call?
+ if (time >= slowThreshold) {
+ // We do a slow log:
+ // - At most once in every SLOW_LOG_INTERVAL_MS
+ // - OR it was slower than the previously logged slow call.
+ if ((nowUptime > (sLastSlowLogUptime + SLOW_LOG_INTERVAL_MS))
+ || (sLastSlowLogActualTime < time)) {
+ EventLogTags.writeServiceManagerSlow(time / 1000, name);
+
+ sLastSlowLogUptime = nowUptime;
+ sLastSlowLogActualTime = time;
+ }
+ }
+
+ // Every GET_SERVICE_LOG_EVERY_CALLS calls, log the total time spent in getService().
+
+ final int logInterval = isCore
+ ? GET_SERVICE_LOG_EVERY_CALLS_CORE
+ : GET_SERVICE_LOG_EVERY_CALLS_NON_CORE;
+
+ if ((sGetServiceAccumulatedCallCount >= logInterval)
+ && (nowUptime >= (sLastStatsLogUptime + STATS_LOG_INTERVAL_MS))) {
+
+ EventLogTags.writeServiceManagerStats(
+ sGetServiceAccumulatedCallCount, // Total # of getService() calls.
+ sGetServiceAccumulatedUs / 1000, // Total time spent in getService() calls.
+ (int) (nowUptime - sLastStatsLogUptime)); // Uptime duration since last log.
+ sGetServiceAccumulatedCallCount = 0;
+ sGetServiceAccumulatedUs = 0;
+ sLastStatsLogUptime = nowUptime;
+ }
+ }
+ return binder;
+ }
}
diff --git a/android/os/StrictMode.java b/android/os/StrictMode.java
index a93e25aa..59380fd3 100644
--- a/android/os/StrictMode.java
+++ b/android/os/StrictMode.java
@@ -39,6 +39,7 @@ import android.os.strictmode.InstanceCountViolation;
import android.os.strictmode.IntentReceiverLeakedViolation;
import android.os.strictmode.LeakedClosableViolation;
import android.os.strictmode.NetworkViolation;
+import android.os.strictmode.NonSdkApiUsedViolation;
import android.os.strictmode.ResourceMismatchViolation;
import android.os.strictmode.ServiceConnectionLeakedViolation;
import android.os.strictmode.SqliteObjectLeakedViolation;
@@ -76,6 +77,7 @@ import java.util.HashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
/**
* StrictMode is a developer tool which detects things you might be doing by accident and brings
@@ -262,6 +264,9 @@ public final class StrictMode {
/** @hide */
@TestApi public static final int DETECT_VM_UNTAGGED_SOCKET = 0x80 << 24; // for VmPolicy
+ /** @hide */
+ @TestApi public static final int DETECT_VM_NON_SDK_API_USAGE = 0x40 << 24; // for VmPolicy
+
private static final int ALL_VM_DETECT_BITS =
DETECT_VM_CURSOR_LEAKS
| DETECT_VM_CLOSABLE_LEAKS
@@ -271,7 +276,9 @@ public final class StrictMode {
| DETECT_VM_FILE_URI_EXPOSURE
| DETECT_VM_CLEARTEXT_NETWORK
| DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION
- | DETECT_VM_UNTAGGED_SOCKET;
+ | DETECT_VM_UNTAGGED_SOCKET
+ | DETECT_VM_NON_SDK_API_USAGE;
+
// Byte 3: Penalty
@@ -413,6 +420,13 @@ public final class StrictMode {
*/
private static final AtomicInteger sDropboxCallsInFlight = new AtomicInteger(0);
+ /**
+ * Callback supplied to dalvik / libcore to get informed of usages of java API that are not
+ * a part of the public SDK.
+ */
+ private static final Consumer<String> sNonSdkApiUsageConsumer =
+ message -> onVmPolicyViolation(new NonSdkApiUsedViolation(message));
+
private StrictMode() {}
/**
@@ -796,6 +810,23 @@ public final class StrictMode {
}
/**
+ * Detect reflective usage of APIs that are not part of the public Android SDK.
+ */
+ public Builder detectNonSdkApiUsage() {
+ return enable(DETECT_VM_NON_SDK_API_USAGE);
+ }
+
+ /**
+ * Permit reflective usage of APIs that are not part of the public Android SDK. Note
+ * that this <b>only</b> affects {@code StrictMode}, the underlying runtime may
+ * continue to restrict or warn on access to methods that are not part of the
+ * public SDK.
+ */
+ public Builder permitNonSdkApiUsage() {
+ return disable(DETECT_VM_NON_SDK_API_USAGE);
+ }
+
+ /**
* Detect everything that's potentially suspect.
*
* <p>In the Honeycomb release this includes leaks of SQLite cursors, Activities, and
@@ -826,6 +857,8 @@ public final class StrictMode {
detectContentUriWithoutPermission();
detectUntaggedSockets();
}
+
+ // TODO: Decide whether to detect non SDK API usage beyond a certain API level.
return this;
}
@@ -1848,6 +1881,13 @@ public final class StrictMode {
} else if (networkPolicy != NETWORK_POLICY_ACCEPT) {
Log.w(TAG, "Dropping requested network policy due to missing service!");
}
+
+
+ if ((sVmPolicy.mask & DETECT_VM_NON_SDK_API_USAGE) != 0) {
+ VMRuntime.setNonSdkApiUsageConsumer(sNonSdkApiUsageConsumer);
+ } else {
+ VMRuntime.setNonSdkApiUsageConsumer(null);
+ }
}
}
@@ -2576,6 +2616,8 @@ public final class StrictMode {
return DETECT_VM_CONTENT_URI_WITHOUT_PERMISSION;
} else if (mViolation instanceof UntaggedSocketViolation) {
return DETECT_VM_UNTAGGED_SOCKET;
+ } else if (mViolation instanceof NonSdkApiUsedViolation) {
+ return DETECT_VM_NON_SDK_API_USAGE;
}
throw new IllegalStateException("missing violation bit");
}
diff --git a/android/os/SystemProperties.java b/android/os/SystemProperties.java
index 8eb39c02..7d3ba6a3 100644
--- a/android/os/SystemProperties.java
+++ b/android/os/SystemProperties.java
@@ -19,6 +19,7 @@ package android.os;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.util.Log;
import android.util.MutableInt;
@@ -35,6 +36,7 @@ import java.util.HashMap;
* {@hide}
*/
@SystemApi
+@TestApi
public class SystemProperties {
private static final String TAG = "SystemProperties";
private static final boolean TRACK_KEY_ACCESS = false;
@@ -110,6 +112,7 @@ public class SystemProperties {
*/
@NonNull
@SystemApi
+ @TestApi
public static String get(@NonNull String key, @Nullable String def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get(key, def);
diff --git a/android/os/UserHandle.java b/android/os/UserHandle.java
index 094f0046..4d4f31de 100644
--- a/android/os/UserHandle.java
+++ b/android/os/UserHandle.java
@@ -82,6 +82,7 @@ public final class UserHandle implements Parcelable {
public static final int USER_SERIAL_SYSTEM = 0;
/** @hide A user handle to indicate the "system" user of the device */
+ @TestApi
public static final UserHandle SYSTEM = new UserHandle(USER_SYSTEM);
/**
diff --git a/android/os/UserManager.java b/android/os/UserManager.java
index a9eb3600..9b20ed2e 100644
--- a/android/os/UserManager.java
+++ b/android/os/UserManager.java
@@ -1149,6 +1149,7 @@ public class UserManager {
* primary user are two separate users. Previously system user and primary user are combined as
* a single owner user. see @link {android.os.UserHandle#USER_OWNER}
*/
+ @TestApi
public static boolean isSplitSystemUser() {
return RoSystemProperties.FW_SYSTEM_USER_SPLIT;
}
diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java
index 17d83db6..32707190 100644
--- a/android/os/WorkSource.java
+++ b/android/os/WorkSource.java
@@ -924,13 +924,17 @@ public class WorkSource implements Parcelable {
/** @hide */
@VisibleForTesting
public int[] getUids() {
- return mUids;
+ int[] uids = new int[mSize];
+ System.arraycopy(mUids, 0, uids, 0, mSize);
+ return uids;
}
/** @hide */
@VisibleForTesting
public String[] getTags() {
- return mTags;
+ String[] tags = new String[mSize];
+ System.arraycopy(mTags, 0, tags, 0, mSize);
+ return tags;
}
/** @hide */
diff --git a/android/os/ZygoteProcess.java b/android/os/ZygoteProcess.java
index b9dd376f..673da507 100644
--- a/android/os/ZygoteProcess.java
+++ b/android/os/ZygoteProcess.java
@@ -166,6 +166,11 @@ public class ZygoteProcess {
private List<String> mApiBlacklistExemptions = Collections.emptyList();
/**
+ * Proportion of hidden API accesses that should be logged to the event log; 0 - 0x10000.
+ */
+ private int mHiddenApiAccessLogSampleRate;
+
+ /**
* The state of the connection to the primary zygote.
*/
private ZygoteState primaryZygoteState;
@@ -467,7 +472,8 @@ public class ZygoteProcess {
* <p>The list of exemptions will take affect for all new processes forked from the zygote after
* this call.
*
- * @param exemptions List of hidden API exemption prefixes.
+ * @param exemptions List of hidden API exemption prefixes. Any matching members are treated as
+ * whitelisted/public APIs (i.e. allowed, no logging of usage).
*/
public void setApiBlacklistExemptions(List<String> exemptions) {
synchronized (mLock) {
@@ -477,6 +483,21 @@ public class ZygoteProcess {
}
}
+ /**
+ * Set the precentage of detected hidden API accesses that are logged to the event log.
+ *
+ * <p>This rate will take affect for all new processes forked from the zygote after this call.
+ *
+ * @param rate An integer between 0 and 0x10000 inclusive. 0 means no event logging.
+ */
+ public void setHiddenApiAccessLogSampleRate(int rate) {
+ synchronized (mLock) {
+ mHiddenApiAccessLogSampleRate = rate;
+ maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
+ maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
+ }
+ }
+
@GuardedBy("mLock")
private void maybeSetApiBlacklistExemptions(ZygoteState state, boolean sendIfEmpty) {
if (state == null || state.isClosed()) {
@@ -504,6 +525,29 @@ public class ZygoteProcess {
}
}
+ private void maybeSetHiddenApiAccessLogSampleRate(ZygoteState state) {
+ if (state == null || state.isClosed()) {
+ return;
+ }
+ if (mHiddenApiAccessLogSampleRate == -1) {
+ return;
+ }
+ try {
+ state.writer.write(Integer.toString(1));
+ state.writer.newLine();
+ state.writer.write("--hidden-api-log-sampling-rate="
+ + Integer.toString(mHiddenApiAccessLogSampleRate));
+ state.writer.newLine();
+ state.writer.flush();
+ int status = state.inputStream.readInt();
+ if (status != 0) {
+ Slog.e(LOG_TAG, "Failed to set hidden API log sampling rate; status " + status);
+ }
+ } catch (IOException ioe) {
+ Slog.e(LOG_TAG, "Failed to set hidden API log sampling rate", ioe);
+ }
+ }
+
/**
* Tries to open socket to Zygote process if not already open. If
* already open, does nothing. May block and retry. Requires that mLock be held.
@@ -519,6 +563,7 @@ public class ZygoteProcess {
throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
}
maybeSetApiBlacklistExemptions(primaryZygoteState, false);
+ maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
}
if (primaryZygoteState.matches(abi)) {
return primaryZygoteState;
@@ -532,6 +577,7 @@ public class ZygoteProcess {
throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
}
maybeSetApiBlacklistExemptions(secondaryZygoteState, false);
+ maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
}
if (secondaryZygoteState.matches(abi)) {
diff --git a/android/os/storage/DiskInfo.java b/android/os/storage/DiskInfo.java
index 91141074..d493cceb 100644
--- a/android/os/storage/DiskInfo.java
+++ b/android/os/storage/DiskInfo.java
@@ -17,6 +17,7 @@
package android.os.storage;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
@@ -93,7 +94,7 @@ public class DiskInfo implements Parcelable {
return true;
}
- public String getDescription() {
+ public @Nullable String getDescription() {
final Resources res = Resources.getSystem();
if ((flags & FLAG_SD) != 0) {
if (isInteresting(label)) {
@@ -112,6 +113,17 @@ public class DiskInfo implements Parcelable {
}
}
+ public @Nullable String getShortDescription() {
+ final Resources res = Resources.getSystem();
+ if (isSd()) {
+ return res.getString(com.android.internal.R.string.storage_sd_card);
+ } else if (isUsb()) {
+ return res.getString(com.android.internal.R.string.storage_usb_drive);
+ } else {
+ return null;
+ }
+ }
+
public boolean isAdoptable() {
return (flags & FLAG_ADOPTABLE) != 0;
}
diff --git a/android/security/keystore/SessionExpiredException.java b/android/os/strictmode/NonSdkApiUsedViolation.java
index 7c8d5e4f..2f0cb50c 100644
--- a/android/security/keystore/SessionExpiredException.java
+++ b/android/os/strictmode/NonSdkApiUsedViolation.java
@@ -14,14 +14,15 @@
* limitations under the License.
*/
-package android.security.keystore;
+package android.os.strictmode;
/**
- * @deprecated Use {@link android.security.keystore.recovery.SessionExpiredException}.
- * @hide
+ * Subclass of {@code Violation} that is used when a process accesses
+ * a non SDK API.
*/
-public class SessionExpiredException extends RecoveryControllerException {
- public SessionExpiredException(String msg) {
- super(msg);
+public final class NonSdkApiUsedViolation extends Violation {
+ /** @hide */
+ public NonSdkApiUsedViolation(String message) {
+ super(message);
}
}
diff --git a/android/provider/Settings.java b/android/provider/Settings.java
index 68fc6c16..5b7adf01 100644
--- a/android/provider/Settings.java
+++ b/android/provider/Settings.java
@@ -1179,6 +1179,23 @@ public final class Settings {
public static final String ACTION_ZEN_MODE_SETTINGS = "android.settings.ZEN_MODE_SETTINGS";
/**
+ * Activity Action: Show Zen Mode visual effects configuration settings.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ZEN_MODE_BLOCKED_EFFECTS_SETTINGS =
+ "android.settings.ZEN_MODE_BLOCKED_EFFECTS_SETTINGS";
+
+ /**
+ * Activity Action: Show Zen Mode onboarding activity.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ZEN_MODE_ONBOARDING = "android.settings.ZEN_MODE_ONBOARDING";
+
+ /**
* Activity Action: Show Zen Mode (aka Do Not Disturb) priority configuration settings.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@@ -3113,6 +3130,9 @@ public final class Settings {
*/
public static final String DISPLAY_COLOR_MODE = "display_color_mode";
+ private static final Validator DISPLAY_COLOR_MODE_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 2);
+
/**
* The amount of time in milliseconds before the device goes to sleep or begins
* to dream after a period of inactivity. This value is also known as the
@@ -3133,9 +3153,6 @@ public final class Settings {
*/
public static final String SCREEN_BRIGHTNESS = "screen_brightness";
- private static final Validator SCREEN_BRIGHTNESS_VALIDATOR =
- new SettingsValidators.InclusiveIntegerRangeValidator(0, 255);
-
/**
* The screen backlight brightness between 0 and 255.
* @hide
@@ -3753,17 +3770,6 @@ public final class Settings {
new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
- * User-selected RTT mode. When on, outgoing and incoming calls will be answered as RTT
- * calls when supported by the device and carrier. Boolean value.
- * 0 = OFF
- * 1 = ON
- */
- public static final String RTT_CALLING_MODE = "rtt_calling_mode";
-
- /** @hide */
- public static final Validator RTT_CALLING_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
-
- /**
* Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
* boolean (1 or 0).
*/
@@ -4071,7 +4077,6 @@ public final class Settings {
FONT_SCALE,
DIM_SCREEN,
SCREEN_OFF_TIMEOUT,
- SCREEN_BRIGHTNESS,
SCREEN_BRIGHTNESS_MODE,
SCREEN_AUTO_BRIGHTNESS_ADJ,
SCREEN_BRIGHTNESS_FOR_VR,
@@ -4088,7 +4093,6 @@ public final class Settings {
DTMF_TONE_WHEN_DIALING,
DTMF_TONE_TYPE_WHEN_DIALING,
HEARING_AID,
- RTT_CALLING_MODE,
TTY_MODE,
MASTER_MONO,
SOUND_EFFECTS_ENABLED,
@@ -4108,6 +4112,7 @@ public final class Settings {
SHOW_BATTERY_PERCENT,
NOTIFICATION_VIBRATION_INTENSITY,
HAPTIC_FEEDBACK_INTENSITY,
+ DISPLAY_COLOR_MODE
};
/**
@@ -4220,6 +4225,7 @@ public final class Settings {
PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED);
PRIVATE_SETTINGS.add(EGG_MODE);
PRIVATE_SETTINGS.add(SHOW_BATTERY_PERCENT);
+ PRIVATE_SETTINGS.add(DISPLAY_COLOR_MODE);
}
/**
@@ -4241,8 +4247,8 @@ public final class Settings {
VALIDATORS.put(NEXT_ALARM_FORMATTED, NEXT_ALARM_FORMATTED_VALIDATOR);
VALIDATORS.put(FONT_SCALE, FONT_SCALE_VALIDATOR);
VALIDATORS.put(DIM_SCREEN, DIM_SCREEN_VALIDATOR);
+ VALIDATORS.put(DISPLAY_COLOR_MODE, DISPLAY_COLOR_MODE_VALIDATOR);
VALIDATORS.put(SCREEN_OFF_TIMEOUT, SCREEN_OFF_TIMEOUT_VALIDATOR);
- VALIDATORS.put(SCREEN_BRIGHTNESS, SCREEN_BRIGHTNESS_VALIDATOR);
VALIDATORS.put(SCREEN_BRIGHTNESS_FOR_VR, SCREEN_BRIGHTNESS_FOR_VR_VALIDATOR);
VALIDATORS.put(SCREEN_BRIGHTNESS_MODE, SCREEN_BRIGHTNESS_MODE_VALIDATOR);
VALIDATORS.put(MODE_RINGER_STREAMS_AFFECTED, MODE_RINGER_STREAMS_AFFECTED_VALIDATOR);
@@ -4287,7 +4293,6 @@ public final class Settings {
VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR);
VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR);
VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR);
- VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR);
VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR);
VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR);
@@ -6660,6 +6665,17 @@ public final class Settings {
private static final Validator TTY_MODE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
+ * User-selected RTT mode. When on, outgoing and incoming calls will be answered as RTT
+ * calls when supported by the device and carrier. Boolean value.
+ * 0 = OFF
+ * 1 = ON
+ */
+ public static final String RTT_CALLING_MODE = "rtt_calling_mode";
+
+ private static final Validator RTT_CALLING_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
+
+ /**
+ /**
* Controls whether settings backup is enabled.
* Type: int ( 0 = disabled, 1 = enabled )
* @hide
@@ -7383,6 +7399,17 @@ public final class Settings {
BOOLEAN_VALIDATOR;
/**
+ * Whether the swipe up gesture to switch apps should be enabled.
+ *
+ * @hide
+ */
+ public static final String SWIPE_UP_TO_SWITCH_APPS_ENABLED =
+ "swipe_up_to_switch_apps_enabled";
+
+ private static final Validator SWIPE_UP_TO_SWITCH_APPS_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
+ /**
* Whether or not the smart camera lift trigger that launches the camera when the user moves
* the phone into a position for taking photos should be enabled.
*
@@ -7885,6 +7912,7 @@ public final class Settings {
PREFERRED_TTY_MODE,
ENHANCED_VOICE_PRIVACY_ENABLED,
TTY_MODE_ENABLED,
+ RTT_CALLING_MODE,
INCALL_POWER_BUTTON_BEHAVIOR,
NIGHT_DISPLAY_CUSTOM_START_TIME,
NIGHT_DISPLAY_CUSTOM_END_TIME,
@@ -7892,6 +7920,7 @@ public final class Settings {
NIGHT_DISPLAY_AUTO_MODE,
SYNC_PARENT_SOUNDS,
CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED,
+ SWIPE_UP_TO_SWITCH_APPS_ENABLED,
CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED,
SYSTEM_NAVIGATION_KEYS_ENABLED,
QS_TILES,
@@ -8014,6 +8043,7 @@ public final class Settings {
VALIDATORS.put(ENHANCED_VOICE_PRIVACY_ENABLED,
ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR);
VALIDATORS.put(TTY_MODE_ENABLED, TTY_MODE_ENABLED_VALIDATOR);
+ VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
VALIDATORS.put(INCALL_POWER_BUTTON_BEHAVIOR, INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR);
VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_START_TIME,
NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR);
@@ -8024,6 +8054,8 @@ public final class Settings {
VALIDATORS.put(SYNC_PARENT_SOUNDS, SYNC_PARENT_SOUNDS_VALIDATOR);
VALIDATORS.put(CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED,
CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR);
+ VALIDATORS.put(SWIPE_UP_TO_SWITCH_APPS_ENABLED,
+ SWIPE_UP_TO_SWITCH_APPS_ENABLED_VALIDATOR);
VALIDATORS.put(CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED,
CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR);
VALIDATORS.put(SYSTEM_NAVIGATION_KEYS_ENABLED,
@@ -8935,6 +8967,20 @@ public final class Settings {
/** {@hide} */
public static final String NETSTATS_UID_TAG_DELETE_AGE = "netstats_uid_tag_delete_age";
+ /** {@hide} */
+ public static final String NETPOLICY_QUOTA_ENABLED = "netpolicy_quota_enabled";
+ /** {@hide} */
+ public static final String NETPOLICY_QUOTA_UNLIMITED = "netpolicy_quota_unlimited";
+ /** {@hide} */
+ public static final String NETPOLICY_QUOTA_LIMITED = "netpolicy_quota_limited";
+ /** {@hide} */
+ public static final String NETPOLICY_QUOTA_FRAC_JOBS = "netpolicy_quota_frac_jobs";
+ /** {@hide} */
+ public static final String NETPOLICY_QUOTA_FRAC_MULTIPATH = "netpolicy_quota_frac_multipath";
+
+ /** {@hide} */
+ public static final String NETPOLICY_OVERRIDE_ENABLED = "netpolicy_override_enabled";
+
/**
* User preference for which network(s) should be used. Only the
* connectivity service should touch this.
@@ -9309,6 +9355,15 @@ public final class Settings {
"network_metered_multipath_preference";
/**
+ * Default daily multipath budget used by ConnectivityManager.getMultipathPreference()
+ * on metered networks. This default quota is only used if quota could not be determined
+ * from data plan or data limit/warning set by the user.
+ * @hide
+ */
+ public static final String NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES =
+ "network_default_daily_multipath_quota_bytes";
+
+ /**
* Network watchlist last report time.
* @hide
*/
@@ -10797,6 +10852,15 @@ public final class Settings {
= "time_only_mode_constants";
/**
+ * Whether of not to send keycode sleep for ungaze when Home is the foreground activity on
+ * watch type devices.
+ * Type: int (0 for false, 1 for true)
+ * Default: 0
+ * @hide
+ */
+ public static final String UNGAZE_SLEEP_ENABLED = "ungaze_sleep_enabled";
+
+ /**
* Whether or not Network Watchlist feature is enabled.
* Type: int (0 for false, 1 for true)
* Default: 0
@@ -11076,6 +11140,14 @@ public final class Settings {
public static final String ALWAYS_FINISH_ACTIVITIES = "always_finish_activities";
/**
+ * If nonzero, all system error dialogs will be hidden. For example, the
+ * crash and ANR dialogs will not be shown, and the system will just proceed
+ * as if they had been accepted by the user.
+ * @hide
+ */
+ public static final String HIDE_ERROR_DIALOGS = "hide_error_dialogs";
+
+ /**
* Use Dock audio output for media:
* 0 = disabled
* 1 = enabled
@@ -11694,6 +11766,38 @@ public final class Settings {
"hidden_api_blacklist_exemptions";
/**
+ * Sampling rate for hidden API access event logs, as an integer in the range 0 to 0x10000
+ * inclusive.
+ *
+ * @hide
+ */
+ public static final String HIDDEN_API_ACCESS_LOG_SAMPLING_RATE =
+ "hidden_api_access_log_sampling_rate";
+
+ /**
+ * Hidden API enforcement policy for apps targeting SDK versions prior to the latest
+ * version.
+ *
+ * Values correspond to @{@link
+ * android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy}
+ *
+ * @hide
+ */
+ public static final String HIDDEN_API_POLICY_PRE_P_APPS =
+ "hidden_api_policy_pre_p_apps";
+
+ /**
+ * Hidden API enforcement policy for apps targeting the current SDK version.
+ *
+ * Values correspond to @{@link
+ * android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy}
+ *
+ * @hide
+ */
+ public static final String HIDDEN_API_POLICY_P_APPS =
+ "hidden_api_policy_p_apps";
+
+ /**
* Timeout for a single {@link android.media.soundtrigger.SoundTriggerDetectionService}
* operation (in ms).
*
@@ -12532,6 +12636,19 @@ public final class Settings {
*/
public static final String SWAP_ENABLED = "swap_enabled";
+ /**
+ * Blacklist of GNSS satellites.
+ *
+ * This is a list of integers separated by commas to represent pairs of (constellation,
+ * svid). Thus, the number of integers should be even.
+ *
+ * E.g.: "3,0,5,24" denotes (constellation=3, svid=0) and (constellation=5, svid=24) are
+ * blacklisted. Note that svid=0 denotes all svids in the
+ * constellation are blacklisted.
+ *
+ * @hide
+ */
+ public static final String GNSS_SATELLITE_BLACKLIST = "gnss_satellite_blacklist";
}
/**
diff --git a/android/se/omapi/Channel.java b/android/se/omapi/Channel.java
index c8efede3..5db3c1a9 100644
--- a/android/se/omapi/Channel.java
+++ b/android/se/omapi/Channel.java
@@ -39,7 +39,7 @@ import java.io.IOException;
*
* @see <a href="http://globalplatform.org">GlobalPlatform Open Mobile API</a>
*/
-public class Channel {
+public final class Channel implements java.nio.channels.Channel {
private static final String TAG = "OMAPI.Channel";
private Session mSession;
@@ -64,7 +64,7 @@ public class Channel {
* before closing the channel.
*/
public void close() {
- if (!isClosed()) {
+ if (isOpen()) {
synchronized (mLock) {
try {
mChannel.close();
@@ -76,21 +76,21 @@ public class Channel {
}
/**
- * Tells if this channel is closed.
+ * Tells if this channel is open.
*
- * @return <code>true</code> if the channel is closed or in case of an error.
- * <code>false</code> otherwise.
+ * @return <code>false</code> if the channel is closed or in case of an error.
+ * <code>true</code> otherwise.
*/
- public boolean isClosed() {
+ public boolean isOpen() {
if (!mService.isConnected()) {
Log.e(TAG, "service not connected to system");
- return true;
+ return false;
}
try {
- return mChannel.isClosed();
+ return !mChannel.isClosed();
} catch (RemoteException e) {
Log.e(TAG, "Exception in isClosed()");
- return true;
+ return false;
}
}
diff --git a/android/se/omapi/Reader.java b/android/se/omapi/Reader.java
index 9be3da6c..80262f75 100644
--- a/android/se/omapi/Reader.java
+++ b/android/se/omapi/Reader.java
@@ -37,7 +37,7 @@ import java.io.IOException;
*
* @see <a href="http://globalplatform.org">GlobalPlatform Open Mobile API</a>
*/
-public class Reader {
+public final class Reader {
private static final String TAG = "OMAPI.Reader";
private final String mName;
diff --git a/android/se/omapi/SEService.java b/android/se/omapi/SEService.java
index 311dc4c7..14727f02 100644
--- a/android/se/omapi/SEService.java
+++ b/android/se/omapi/SEService.java
@@ -32,6 +32,7 @@ import android.os.RemoteException;
import android.util.Log;
import java.util.HashMap;
+import java.util.concurrent.Executor;
/**
* The SEService realises the communication to available Secure Elements on the
@@ -40,7 +41,7 @@ import java.util.HashMap;
*
* @see <a href="http://simalliance.org">SIMalliance Open Mobile API v3.0</a>
*/
-public class SEService {
+public final class SEService {
/**
* Error code used with ServiceSpecificException.
@@ -62,11 +63,11 @@ public class SEService {
/**
* Interface to send call-backs to the application when the service is connected.
*/
- public interface SecureElementListener {
+ public interface OnConnectedListener {
/**
* Called by the framework when the service is connected.
*/
- void onServiceConnected();
+ void onConnected();
}
/**
@@ -74,16 +75,22 @@ public class SEService {
* SEService could be bound to the backend.
*/
private class SEListener extends ISecureElementListener.Stub {
- public SecureElementListener mListener = null;
+ public OnConnectedListener mListener = null;
+ public Executor mExecutor = null;
@Override
public IBinder asBinder() {
return this;
}
- public void onServiceConnected() {
- if (mListener != null) {
- mListener.onServiceConnected();
+ public void onConnected() {
+ if (mListener != null && mExecutor != null) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mListener.onConnected();
+ }
+ });
}
}
}
@@ -116,22 +123,26 @@ public class SEService {
* the specified listener is called or if isConnected() returns
* <code>true</code>. <br>
* The call-back object passed as a parameter will have its
- * onServiceConnected() method called when the connection actually happen.
+ * onConnected() method called when the connection actually happen.
*
* @param context
* the context of the calling application. Cannot be
* <code>null</code>.
* @param listener
- * a SecureElementListener object.
+ * a OnConnectedListener object.
+ * @param executor
+ * an Executor which will be used when invoking the callback.
*/
- public SEService(@NonNull Context context, @NonNull SecureElementListener listener) {
+ public SEService(@NonNull Context context, @NonNull Executor executor,
+ @NonNull OnConnectedListener listener) {
- if (context == null) {
- throw new NullPointerException("context must not be null");
+ if (context == null || listener == null || executor == null) {
+ throw new NullPointerException("Arguments must not be null");
}
mContext = context;
mSEListener.mListener = listener;
+ mSEListener.mExecutor = executor;
mConnection = new ServiceConnection() {
@@ -140,7 +151,7 @@ public class SEService {
mSecureElementService = ISecureElementService.Stub.asInterface(service);
if (mSEListener != null) {
- mSEListener.onServiceConnected();
+ mSEListener.onConnected();
}
Log.i(TAG, "Service onServiceConnected");
}
@@ -171,12 +182,12 @@ public class SEService {
}
/**
- * Returns the list of available Secure Element readers.
+ * Returns an array of available Secure Element readers.
* There must be no duplicated objects in the returned list.
* All available readers shall be listed even if no card is inserted.
*
- * @return The readers list, as an array of Readers. If there are no
- * readers the returned array is of length 0.
+ * @return An array of Readers. If there are no readers the returned array
+ * is of length 0.
*/
public @NonNull Reader[] getReaders() {
if (mSecureElementService == null) {
@@ -212,7 +223,8 @@ public class SEService {
* (including any binding to an underlying service).
* As a result isConnected() will return false after shutdown() was called.
* After this method call, the SEService object is not connected.
- * It is recommended to call this method in the termination method of the calling application
+ * This method should be called when connection to the Secure Element is not needed
+ * or in the termination method of the calling application
* (or part of this application) which is bound to this SEService.
*/
public void shutdown() {
diff --git a/android/se/omapi/Session.java b/android/se/omapi/Session.java
index adfeddd5..d5f8c82b 100644
--- a/android/se/omapi/Session.java
+++ b/android/se/omapi/Session.java
@@ -39,7 +39,7 @@ import java.util.NoSuchElementException;
*
* @see <a href="http://simalliance.org">SIMalliance Open Mobile API v3.0</a>
*/
-public class Session {
+public final class Session {
private final Object mLock = new Object();
private final SEService mService;
@@ -225,6 +225,32 @@ public class Session {
}
/**
+ * This method is provided to ease the development of mobile application and for compliancy
+ * with existing applications.
+ * This method is equivalent to openBasicChannel(aid, P2=0x00)
+ *
+ * @param aid the AID of the Applet to be selected on this channel, as a
+ * byte array, or null if no Applet is to be selected.
+ * @throws IOException if there is a communication problem to the reader or
+ * the Secure Element.
+ * @throws IllegalStateException if the Secure Element session is used after
+ * being closed.
+ * @throws IllegalArgumentException if the aid's length is not within 5 to
+ * 16 (inclusive).
+ * @throws SecurityException if the calling application cannot be granted
+ * access to this AID or the default Applet on this
+ * session.
+ * @throws NoSuchElementException if the AID on the Secure Element is not available or cannot be
+ * selected.
+ * @throws UnsupportedOperationException if the given P2 parameter is not
+ * supported by the device
+ * @return an instance of Channel if available or null.
+ */
+ public @Nullable Channel openBasicChannel(@Nullable byte[] aid) throws IOException {
+ return openBasicChannel(aid, (byte) 0x00);
+ }
+
+ /**
* Open a logical channel with the Secure Element, selecting the Applet represented by
* the given AID. If the AID is null, which means no Applet is to be selected on this
* channel, the default Applet is used. It's up to the Secure Element to choose which
@@ -304,4 +330,32 @@ public class Session {
}
}
}
+
+ /**
+ * This method is provided to ease the development of mobile application and for compliancy
+ * with existing applications.
+ * This method is equivalent to openLogicalChannel(aid, P2=0x00)
+ *
+ * @param aid the AID of the Applet to be selected on this channel, as a
+ * byte array.
+ * @throws IOException if there is a communication problem to the reader or
+ * the Secure Element.
+ * @throws IllegalStateException if the Secure Element is used after being
+ * closed.
+ * @throws IllegalArgumentException if the aid's length is not within 5 to
+ * 16 (inclusive).
+ * @throws SecurityException if the calling application cannot be granted
+ * access to this AID or the default Applet on this
+ * session.
+ * @throws NoSuchElementException if the AID on the Secure Element is not
+ * available or cannot be selected or a logical channel is already
+ * open to a non-multiselectable Applet.
+ * @throws UnsupportedOperationException if the given P2 parameter is not
+ * supported by the device.
+ * @return an instance of Channel. Null if the Secure Element is unable to
+ * provide a new logical channel.
+ */
+ public @Nullable Channel openLogicalChannel(@Nullable byte[] aid) throws IOException {
+ return openLogicalChannel(aid, (byte) 0x00);
+ }
}
diff --git a/android/security/ConfirmationCallback.java b/android/security/ConfirmationCallback.java
index 4670bce3..fd027f0f 100644
--- a/android/security/ConfirmationCallback.java
+++ b/android/security/ConfirmationCallback.java
@@ -33,22 +33,22 @@ public abstract class ConfirmationCallback {
*
* @param dataThatWasConfirmed the data that was confirmed, see above for the format.
*/
- public void onConfirmedByUser(@NonNull byte[] dataThatWasConfirmed) {}
+ public void onConfirmed(@NonNull byte[] dataThatWasConfirmed) {}
/**
* Called when the requested prompt was dismissed (not accepted) by the user.
*/
- public void onDismissedByUser() {}
+ public void onDismissed() {}
/**
* Called when the requested prompt was dismissed by the application.
*/
- public void onDismissedByApplication() {}
+ public void onCanceled() {}
/**
* Called when the requested prompt was dismissed because of a low-level error.
*
- * @param e an exception representing the error.
+ * @param e a throwable representing the error.
*/
- public void onError(Exception e) {}
+ public void onError(Throwable e) {}
}
diff --git a/android/security/ConfirmationDialog.java b/android/security/ConfirmationPrompt.java
index 1697106c..5330cffe 100644
--- a/android/security/ConfirmationDialog.java
+++ b/android/security/ConfirmationPrompt.java
@@ -68,7 +68,7 @@ import java.util.concurrent.Executor;
* {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally
* since it'll use it in a later step.
* <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the
- * {@link ConfirmationCallback#onConfirmedByUser onConfirmedByUser(byte[])} callback as the
+ * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the
* <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the
* user, the <code>extraData</code> parameter, and possibly other data.
* <li> The application signs the <i>Confirmation Response</i> with the previously created key and
@@ -82,8 +82,8 @@ import java.util.concurrent.Executor;
* last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it
* along the nonce in the <code>extraData</code> blob.
*/
-public class ConfirmationDialog {
- private static final String TAG = "ConfirmationDialog";
+public class ConfirmationPrompt {
+ private static final String TAG = "ConfirmationPrompt";
private CharSequence mPromptText;
private byte[] mExtraData;
@@ -97,15 +97,15 @@ public class ConfirmationDialog {
ConfirmationCallback callback) {
switch (responseCode) {
case KeyStore.CONFIRMATIONUI_OK:
- callback.onConfirmedByUser(dataThatWasConfirmed);
+ callback.onConfirmed(dataThatWasConfirmed);
break;
case KeyStore.CONFIRMATIONUI_CANCELED:
- callback.onDismissedByUser();
+ callback.onDismissed();
break;
case KeyStore.CONFIRMATIONUI_ABORTED:
- callback.onDismissedByApplication();
+ callback.onCanceled();
break;
case KeyStore.CONFIRMATIONUI_SYSTEM_ERROR:
@@ -145,21 +145,25 @@ public class ConfirmationDialog {
};
/**
- * A builder that collects arguments, to be shown on the system-provided confirmation dialog.
+ * A builder that collects arguments, to be shown on the system-provided confirmation prompt.
*/
- public static class Builder {
+ public static final class Builder {
+ private Context mContext;
private CharSequence mPromptText;
private byte[] mExtraData;
/**
- * Creates a builder for the confirmation dialog.
+ * Creates a builder for the confirmation prompt.
+ *
+ * @param context the application context
*/
- public Builder() {
+ public Builder(Context context) {
+ mContext = context;
}
/**
- * Sets the prompt text for the dialog.
+ * Sets the prompt text for the prompt.
*
* @param promptText the text to present in the prompt.
* @return the builder.
@@ -170,7 +174,7 @@ public class ConfirmationDialog {
}
/**
- * Sets the extra data for the dialog.
+ * Sets the extra data for the prompt.
*
* @param extraData data to include in the response data.
* @return the builder.
@@ -181,24 +185,23 @@ public class ConfirmationDialog {
}
/**
- * Creates a {@link ConfirmationDialog} with the arguments supplied to this builder.
+ * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder.
*
- * @param context the application context
- * @return a {@link ConfirmationDialog}
+ * @return a {@link ConfirmationPrompt}
* @throws IllegalArgumentException if any of the required fields are not set.
*/
- public ConfirmationDialog build(Context context) {
+ public ConfirmationPrompt build() {
if (TextUtils.isEmpty(mPromptText)) {
throw new IllegalArgumentException("prompt text must be set and non-empty");
}
if (mExtraData == null) {
throw new IllegalArgumentException("extraData must be set");
}
- return new ConfirmationDialog(context, mPromptText, mExtraData);
+ return new ConfirmationPrompt(mContext, mPromptText, mExtraData);
}
}
- private ConfirmationDialog(Context context, CharSequence promptText, byte[] extraData) {
+ private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) {
mContext = context;
mPromptText = promptText;
mExtraData = extraData;
@@ -227,10 +230,10 @@ public class ConfirmationDialog {
return uiOptionsAsFlags;
}
- private boolean isAccessibilityServiceRunning() {
+ private static boolean isAccessibilityServiceRunning(Context context) {
boolean serviceRunning = false;
try {
- ContentResolver contentResolver = mContext.getContentResolver();
+ ContentResolver contentResolver = context.getContentResolver();
int a11yEnabled = Settings.Secure.getInt(contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED);
if (a11yEnabled == 1) {
@@ -249,12 +252,12 @@ public class ConfirmationDialog {
* When the prompt is no longer being presented, one of the methods in
* {@link ConfirmationCallback} is called on the supplied callback object.
*
- * Confirmation dialogs may not be available when accessibility services are running so this
+ * Confirmation prompts may not be available when accessibility services are running so this
* may fail with a {@link ConfirmationNotAvailableException} exception even if
* {@link #isSupported} returns {@code true}.
*
* @param executor the executor identifying the thread that will receive the callback.
- * @param callback the callback to use when the dialog is done showing.
+ * @param callback the callback to use when the prompt is done showing.
* @throws IllegalArgumentException if the prompt text is too long or malfomed.
* @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
* @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
@@ -265,7 +268,7 @@ public class ConfirmationDialog {
if (mCallback != null) {
throw new ConfirmationAlreadyPresentingException();
}
- if (isAccessibilityServiceRunning()) {
+ if (isAccessibilityServiceRunning(mContext)) {
throw new ConfirmationNotAvailableException();
}
mCallback = callback;
@@ -301,7 +304,7 @@ public class ConfirmationDialog {
* Cancels a prompt currently being displayed.
*
* On success, the
- * {@link ConfirmationCallback#onDismissedByApplication onDismissedByApplication()} method on
+ * {@link ConfirmationCallback#onCanceled onCanceled()} method on
* the supplied callback object will be called asynchronously.
*
* @throws IllegalStateException if no prompt is currently being presented.
@@ -324,9 +327,13 @@ public class ConfirmationDialog {
/**
* Checks if the device supports confirmation prompts.
*
+ * @param context the application context.
* @return true if confirmation prompts are supported by the device.
*/
- public static boolean isSupported() {
+ public static boolean isSupported(Context context) {
+ if (isAccessibilityServiceRunning(context)) {
+ return false;
+ }
return KeyStore.getInstance().isConfirmationPromptSupported();
}
}
diff --git a/android/security/KeyStoreException.java b/android/security/KeyStoreException.java
index 88e768ce..30389a29 100644
--- a/android/security/KeyStoreException.java
+++ b/android/security/KeyStoreException.java
@@ -16,12 +16,15 @@
package android.security;
+import android.annotation.TestApi;
+
/**
* KeyStore/keymaster exception with positive error codes coming from the KeyStore and negative
* ones from keymaster.
*
* @hide
*/
+@TestApi
public class KeyStoreException extends Exception {
private final int mErrorCode;
diff --git a/android/security/keystore/BackwardsCompat.java b/android/security/keystore/BackwardsCompat.java
deleted file mode 100644
index cf5fe1f0..00000000
--- a/android/security/keystore/BackwardsCompat.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Function;
-
-/**
- * Helpers for converting classes between old and new API, so we can preserve backwards
- * compatibility while teamfooding. This will be removed soon.
- *
- * @hide
- */
-class BackwardsCompat {
-
-
- static KeychainProtectionParams toLegacyKeychainProtectionParams(
- android.security.keystore.recovery.KeyChainProtectionParams keychainProtectionParams
- ) {
- return new KeychainProtectionParams.Builder()
- .setUserSecretType(keychainProtectionParams.getUserSecretType())
- .setSecret(keychainProtectionParams.getSecret())
- .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
- .setKeyDerivationParams(
- toLegacyKeyDerivationParams(
- keychainProtectionParams.getKeyDerivationParams()))
- .build();
- }
-
- static KeyDerivationParams toLegacyKeyDerivationParams(
- android.security.keystore.recovery.KeyDerivationParams keyDerivationParams
- ) {
- return new KeyDerivationParams(
- keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
- }
-
- static WrappedApplicationKey toLegacyWrappedApplicationKey(
- android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey
- ) {
- return new WrappedApplicationKey.Builder()
- .setAlias(wrappedApplicationKey.getAlias())
- .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
- .build();
- }
-
- static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams(
- KeyDerivationParams keyDerivationParams
- ) {
- return android.security.keystore.recovery.KeyDerivationParams.createSha256Params(
- keyDerivationParams.getSalt());
- }
-
- static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey(
- WrappedApplicationKey wrappedApplicationKey
- ) {
- return new android.security.keystore.recovery.WrappedApplicationKey.Builder()
- .setAlias(wrappedApplicationKey.getAlias())
- .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
- .build();
- }
-
- static List<android.security.keystore.recovery.WrappedApplicationKey>
- fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys
- ) {
- return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey);
- }
-
- static List<android.security.keystore.recovery.KeyChainProtectionParams>
- fromLegacyKeychainProtectionParams(
- List<KeychainProtectionParams> keychainProtectionParams) {
- return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam);
- }
-
- static android.security.keystore.recovery.KeyChainProtectionParams
- fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) {
- return new android.security.keystore.recovery.KeyChainProtectionParams.Builder()
- .setUserSecretType(keychainProtectionParams.getUserSecretType())
- .setSecret(keychainProtectionParams.getSecret())
- .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
- .setKeyDerivationParams(
- fromLegacyKeyDerivationParams(
- keychainProtectionParams.getKeyDerivationParams()))
- .build();
- }
-
- static KeychainSnapshot toLegacyKeychainSnapshot(
- android.security.keystore.recovery.KeyChainSnapshot keychainSnapshot
- ) {
- return new KeychainSnapshot.Builder()
- .setCounterId(keychainSnapshot.getCounterId())
- .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob())
- .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey())
- .setSnapshotVersion(keychainSnapshot.getSnapshotVersion())
- .setMaxAttempts(keychainSnapshot.getMaxAttempts())
- .setServerParams(keychainSnapshot.getServerParams())
- .setKeychainProtectionParams(
- map(keychainSnapshot.getKeyChainProtectionParams(),
- BackwardsCompat::toLegacyKeychainProtectionParams))
- .setWrappedApplicationKeys(
- map(keychainSnapshot.getWrappedApplicationKeys(),
- BackwardsCompat::toLegacyWrappedApplicationKey))
- .build();
- }
-
- static <A, B> List<B> map(List<A> as, Function<A, B> f) {
- ArrayList<B> bs = new ArrayList<>(as.size());
- for (A a : as) {
- bs.add(f.apply(a));
- }
- return bs;
- }
-}
diff --git a/android/security/keystore/KeyDerivationParams.java b/android/security/keystore/KeyDerivationParams.java
deleted file mode 100644
index e475dc36..00000000
--- a/android/security/keystore/KeyDerivationParams.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.KeyDerivationParams}.
- * @hide
- */
-public final class KeyDerivationParams implements Parcelable {
- private final int mAlgorithm;
- private byte[] mSalt;
-
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
- public @interface KeyDerivationAlgorithm {
- }
-
- /**
- * Salted SHA256
- */
- public static final int ALGORITHM_SHA256 = 1;
-
- /**
- * Argon2ID
- * @hide
- */
- // TODO: add Argon2ID support.
- public static final int ALGORITHM_ARGON2ID = 2;
-
- /**
- * Creates instance of the class to to derive key using salted SHA256 hash.
- */
- public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
- return new KeyDerivationParams(ALGORITHM_SHA256, salt);
- }
-
- KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
- mAlgorithm = algorithm;
- mSalt = Preconditions.checkNotNull(salt);
- }
-
- /**
- * Gets algorithm.
- */
- public @KeyDerivationAlgorithm int getAlgorithm() {
- return mAlgorithm;
- }
-
- /**
- * Gets salt.
- */
- public @NonNull byte[] getSalt() {
- return mSalt;
- }
-
- public static final Parcelable.Creator<KeyDerivationParams> CREATOR =
- new Parcelable.Creator<KeyDerivationParams>() {
- public KeyDerivationParams createFromParcel(Parcel in) {
- return new KeyDerivationParams(in);
- }
-
- public KeyDerivationParams[] newArray(int length) {
- return new KeyDerivationParams[length];
- }
- };
-
- /**
- * @hide
- */
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(mAlgorithm);
- out.writeByteArray(mSalt);
- }
-
- /**
- * @hide
- */
- protected KeyDerivationParams(Parcel in) {
- mAlgorithm = in.readInt();
- mSalt = in.createByteArray();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java
index c0d0fb00..b2e0f675 100644
--- a/android/security/keystore/KeyGenParameterSpec.java
+++ b/android/security/keystore/KeyGenParameterSpec.java
@@ -19,6 +19,7 @@ package android.security.keystore;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.security.GateKeeper;
@@ -594,6 +595,14 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
/**
* Returns {@code true} if the key is authorized to be used only if a test of user presence has
* been performed between the {@code Signature.initSign()} and {@code Signature.sign()} calls.
+ * It requires that the KeyStore implementation have a direct way to validate the user presence
+ * for example a KeyStore hardware backed strongbox can use a button press that is observable
+ * in hardware. A test for user presence is tangential to authentication. The test can be part
+ * of an authentication step as long as this step can be validated by the hardware protecting
+ * the key and cannot be spoofed. For example, a physical button press can be used as a test of
+ * user presence if the other pins connected to the button are not able to simulate a button
+ * press. There must be no way for the primary processor to fake a button press, or that
+ * button must not be used as a test of user presence.
*/
public boolean isUserPresenceRequired() {
return mUserPresenceRequired;
@@ -673,8 +682,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
}
/**
- * Returns {@code true} if the screen must be unlocked for this key to be used for encryption or
- * signing. Decryption and signature verification will still be available when the screen is
+ * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
+ * signing. Encryption and signature verification will still be available when the screen is
* locked.
*
* @see Builder#setUnlockedDeviceRequired(boolean)
@@ -1180,6 +1189,14 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
/**
* Sets whether a test of user presence is required to be performed between the
* {@code Signature.initSign()} and {@code Signature.sign()} method calls.
+ * It requires that the KeyStore implementation have a direct way to validate the user
+ * presence for example a KeyStore hardware backed strongbox can use a button press that
+ * is observable in hardware. A test for user presence is tangential to authentication. The
+ * test can be part of an authentication step as long as this step can be validated by the
+ * hardware protecting the key and cannot be spoofed. For example, a physical button press
+ * can be used as a test of user presence if the other pins connected to the button are not
+ * able to simulate a button press.There must be no way for the primary processor to fake a
+ * button press, or that button must not be used as a test of user presence.
*/
@NonNull
public Builder setUserPresenceRequired(boolean required) {
@@ -1227,6 +1244,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu
*
* Sets whether to include a temporary unique ID field in the attestation certificate.
*/
+ @TestApi
@NonNull
public Builder setUniqueIdIncluded(boolean uniqueIdIncluded) {
mUniqueIdIncluded = uniqueIdIncluded;
diff --git a/android/security/keystore/KeyProtection.java b/android/security/keystore/KeyProtection.java
index 4daf30ce..fdcad85b 100644
--- a/android/security/keystore/KeyProtection.java
+++ b/android/security/keystore/KeyProtection.java
@@ -19,6 +19,7 @@ package android.security.keystore;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.security.GateKeeper;
@@ -445,6 +446,14 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
/**
* Returns {@code true} if the key is authorized to be used only if a test of user presence has
* been performed between the {@code Signature.initSign()} and {@code Signature.sign()} calls.
+ * It requires that the KeyStore implementation have a direct way to validate the user presence
+ * for example a KeyStore hardware backed strongbox can use a button press that is observable
+ * in hardware. A test for user presence is tangential to authentication. The test can be part
+ * of an authentication step as long as this step can be validated by the hardware protecting
+ * the key and cannot be spoofed. For example, a physical button press can be used as a test of
+ * user presence if the other pins connected to the button are not able to simulate a button
+ * press. There must be no way for the primary processor to fake a button press, or that
+ * button must not be used as a test of user presence.
*/
public boolean isUserPresenceRequired() {
return mUserPresenceRequred;
@@ -493,6 +502,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
* @see KeymasterUtils#addUserAuthArgs
* @hide
*/
+ @TestApi
public long getBoundToSpecificSecureUserId() {
return mBoundToSecureUserId;
}
@@ -508,8 +518,8 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
}
/**
- * Returns {@code true} if the screen must be unlocked for this key to be used for encryption or
- * signing. Decryption and signature verification will still be available when the screen is
+ * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or
+ * signing. Encryption and signature verification will still be available when the screen is
* locked.
*
* @see Builder#setUnlockedDeviceRequired(boolean)
@@ -840,7 +850,15 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
/**
* Sets whether a test of user presence is required to be performed between the
- * {@code Signature.initSign()} and {@code Signature.sign()} method calls.
+ * {@code Signature.initSign()} and {@code Signature.sign()} method calls. It requires that
+ * the KeyStore implementation have a direct way to validate the user presence for example
+ * a KeyStore hardware backed strongbox can use a button press that is observable in
+ * hardware. A test for user presence is tangential to authentication. The test can be part
+ * of an authentication step as long as this step can be validated by the hardware
+ * protecting the key and cannot be spoofed. For example, a physical button press can be
+ * used as a test of user presence if the other pins connected to the button are not able
+ * to simulate a button press. There must be no way for the primary processor to fake a
+ * button press, or that button must not be used as a test of user presence.
*/
@NonNull
public Builder setUserPresenceRequired(boolean required) {
@@ -910,6 +928,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs {
* @see KeyProtection#getBoundToSpecificSecureUserId()
* @hide
*/
+ @TestApi
public Builder setBoundToSpecificSecureUserId(long secureUserId) {
mBoundToSecureUserId = secureUserId;
return this;
diff --git a/android/security/keystore/KeychainProtectionParams.java b/android/security/keystore/KeychainProtectionParams.java
deleted file mode 100644
index 19a087d5..00000000
--- a/android/security/keystore/KeychainProtectionParams.java
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.KeyChainProtectionParams}.
- * @hide
- */
-public final class KeychainProtectionParams implements Parcelable {
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
- public @interface UserSecretType {
- }
-
- /**
- * Lockscreen secret is required to recover KeyStore.
- */
- public static final int TYPE_LOCKSCREEN = 100;
-
- /**
- * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
- */
- public static final int TYPE_CUSTOM_PASSWORD = 101;
-
- /** @hide */
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
- public @interface LockScreenUiFormat {
- }
-
- /**
- * Pin with digits only.
- */
- public static final int TYPE_PIN = 1;
-
- /**
- * Password. String with latin-1 characters only.
- */
- public static final int TYPE_PASSWORD = 2;
-
- /**
- * Pattern with 3 by 3 grid.
- */
- public static final int TYPE_PATTERN = 3;
-
- @UserSecretType
- private Integer mUserSecretType;
-
- @LockScreenUiFormat
- private Integer mLockScreenUiFormat;
-
- /**
- * Parameters of the key derivation function, including algorithm, difficulty, salt.
- */
- private KeyDerivationParams mKeyDerivationParams;
- private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
-
- /**
- * @param secret Constructor creates a reference to the secret. Caller must use
- * @link {#clearSecret} to overwrite its value in memory.
- * @hide
- */
- public KeychainProtectionParams(@UserSecretType int userSecretType,
- @LockScreenUiFormat int lockScreenUiFormat,
- @NonNull KeyDerivationParams keyDerivationParams,
- @NonNull byte[] secret) {
- mUserSecretType = userSecretType;
- mLockScreenUiFormat = lockScreenUiFormat;
- mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
- mSecret = Preconditions.checkNotNull(secret);
- }
-
- private KeychainProtectionParams() {
-
- }
-
- /**
- * @see TYPE_LOCKSCREEN
- * @see TYPE_CUSTOM_PASSWORD
- */
- public @UserSecretType int getUserSecretType() {
- return mUserSecretType;
- }
-
- /**
- * Specifies UX shown to user during recovery.
- * Default value is {@code TYPE_LOCKSCREEN}
- *
- * @see TYPE_PIN
- * @see TYPE_PASSWORD
- * @see TYPE_PATTERN
- */
- public @LockScreenUiFormat int getLockScreenUiFormat() {
- return mLockScreenUiFormat;
- }
-
- /**
- * Specifies function used to derive symmetric key from user input
- * Format is defined in separate util class.
- */
- public @NonNull KeyDerivationParams getKeyDerivationParams() {
- return mKeyDerivationParams;
- }
-
- /**
- * Secret derived from user input.
- * Default value is empty array
- *
- * @return secret or empty array
- */
- public @NonNull byte[] getSecret() {
- return mSecret;
- }
-
- /**
- * Builder for creating {@link KeychainProtectionParams}.
- */
- public static class Builder {
- private KeychainProtectionParams mInstance = new KeychainProtectionParams();
-
- /**
- * Sets user secret type.
- *
- * @see TYPE_LOCKSCREEN
- * @see TYPE_CUSTOM_PASSWORD
- * @param userSecretType The secret type
- * @return This builder.
- */
- public Builder setUserSecretType(@UserSecretType int userSecretType) {
- mInstance.mUserSecretType = userSecretType;
- return this;
- }
-
- /**
- * Sets UI format.
- *
- * @see TYPE_PIN
- * @see TYPE_PASSWORD
- * @see TYPE_PATTERN
- * @param lockScreenUiFormat The UI format
- * @return This builder.
- */
- public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
- mInstance.mLockScreenUiFormat = lockScreenUiFormat;
- return this;
- }
-
- /**
- * Sets parameters of the key derivation function.
- *
- * @param keyDerivationParams Key derivation Params
- * @return This builder.
- */
- public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
- keyDerivationParams) {
- mInstance.mKeyDerivationParams = keyDerivationParams;
- return this;
- }
-
- /**
- * Secret derived from user input, or empty array.
- *
- * @param secret The secret.
- * @return This builder.
- */
- public Builder setSecret(@NonNull byte[] secret) {
- mInstance.mSecret = secret;
- return this;
- }
-
-
- /**
- * Creates a new {@link KeychainProtectionParams} instance.
- * The instance will include default values, if {@link setSecret}
- * or {@link setUserSecretType} were not called.
- *
- * @return new instance
- * @throws NullPointerException if some required fields were not set.
- */
- @NonNull public KeychainProtectionParams build() {
- if (mInstance.mUserSecretType == null) {
- mInstance.mUserSecretType = TYPE_LOCKSCREEN;
- }
- Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
- Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
- if (mInstance.mSecret == null) {
- mInstance.mSecret = new byte[]{};
- }
- return mInstance;
- }
- }
-
- /**
- * Removes secret from memory than object is no longer used.
- * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
- */
- @Override
- protected void finalize() throws Throwable {
- clearSecret();
- super.finalize();
- }
-
- /**
- * Fills mSecret with zeroes.
- */
- public void clearSecret() {
- Arrays.fill(mSecret, (byte) 0);
- }
-
- public static final Parcelable.Creator<KeychainProtectionParams> CREATOR =
- new Parcelable.Creator<KeychainProtectionParams>() {
- public KeychainProtectionParams createFromParcel(Parcel in) {
- return new KeychainProtectionParams(in);
- }
-
- public KeychainProtectionParams[] newArray(int length) {
- return new KeychainProtectionParams[length];
- }
- };
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(mUserSecretType);
- out.writeInt(mLockScreenUiFormat);
- out.writeTypedObject(mKeyDerivationParams, flags);
- out.writeByteArray(mSecret);
- }
-
- /**
- * @hide
- */
- protected KeychainProtectionParams(Parcel in) {
- mUserSecretType = in.readInt();
- mLockScreenUiFormat = in.readInt();
- mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
- mSecret = in.createByteArray();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/keystore/KeychainSnapshot.java b/android/security/keystore/KeychainSnapshot.java
deleted file mode 100644
index cf18fd1c..00000000
--- a/android/security/keystore/KeychainSnapshot.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-import java.util.List;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.KeyChainSnapshot}.
- * @hide
- */
-public final class KeychainSnapshot implements Parcelable {
- private static final int DEFAULT_MAX_ATTEMPTS = 10;
- private static final long DEFAULT_COUNTER_ID = 1L;
-
- private int mSnapshotVersion;
- private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
- private long mCounterId = DEFAULT_COUNTER_ID;
- private byte[] mServerParams;
- private byte[] mPublicKey;
- private List<KeychainProtectionParams> mKeychainProtectionParams;
- private List<WrappedApplicationKey> mEntryRecoveryData;
- private byte[] mEncryptedRecoveryKeyBlob;
-
- /**
- * @hide
- * Deprecated, consider using builder.
- */
- public KeychainSnapshot(
- int snapshotVersion,
- @NonNull List<KeychainProtectionParams> keychainProtectionParams,
- @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
- @NonNull byte[] encryptedRecoveryKeyBlob) {
- mSnapshotVersion = snapshotVersion;
- mKeychainProtectionParams =
- Preconditions.checkCollectionElementsNotNull(keychainProtectionParams,
- "keychainProtectionParams");
- mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
- "wrappedApplicationKeys");
- mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
- }
-
- private KeychainSnapshot() {
-
- }
-
- /**
- * Snapshot version for given account. It is incremented when user secret or list of application
- * keys changes.
- */
- public int getSnapshotVersion() {
- return mSnapshotVersion;
- }
-
- /**
- * Number of user secret guesses allowed during Keychain recovery.
- */
- public int getMaxAttempts() {
- return mMaxAttempts;
- }
-
- /**
- * CounterId which is rotated together with user secret.
- */
- public long getCounterId() {
- return mCounterId;
- }
-
- /**
- * Server parameters.
- */
- public @NonNull byte[] getServerParams() {
- return mServerParams;
- }
-
- /**
- * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
- *
- * See implementation for binary key format
- */
- // TODO: document key format.
- public @NonNull byte[] getTrustedHardwarePublicKey() {
- return mPublicKey;
- }
-
- /**
- * UI and key derivation parameters. Note that combination of secrets may be used.
- */
- public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() {
- return mKeychainProtectionParams;
- }
-
- /**
- * List of application keys, with key material encrypted by
- * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
- */
- public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
- return mEntryRecoveryData;
- }
-
- /**
- * Recovery key blob, encrypted by user secret and recovery service public key.
- */
- public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
- return mEncryptedRecoveryKeyBlob;
- }
-
- public static final Parcelable.Creator<KeychainSnapshot> CREATOR =
- new Parcelable.Creator<KeychainSnapshot>() {
- public KeychainSnapshot createFromParcel(Parcel in) {
- return new KeychainSnapshot(in);
- }
-
- public KeychainSnapshot[] newArray(int length) {
- return new KeychainSnapshot[length];
- }
- };
-
- /**
- * Builder for creating {@link KeychainSnapshot}.
- *
- * @hide
- */
- public static class Builder {
- private KeychainSnapshot mInstance = new KeychainSnapshot();
-
- /**
- * Snapshot version for given account.
- *
- * @param snapshotVersion The snapshot version
- * @return This builder.
- */
- public Builder setSnapshotVersion(int snapshotVersion) {
- mInstance.mSnapshotVersion = snapshotVersion;
- return this;
- }
-
- /**
- * Sets the number of user secret guesses allowed during Keychain recovery.
- *
- * @param maxAttempts The maximum number of guesses.
- * @return This builder.
- */
- public Builder setMaxAttempts(int maxAttempts) {
- mInstance.mMaxAttempts = maxAttempts;
- return this;
- }
-
- /**
- * Sets counter id.
- *
- * @param counterId The counter id.
- * @return This builder.
- */
- public Builder setCounterId(long counterId) {
- mInstance.mCounterId = counterId;
- return this;
- }
-
- /**
- * Sets server parameters.
- *
- * @param serverParams The server parameters
- * @return This builder.
- */
- public Builder setServerParams(byte[] serverParams) {
- mInstance.mServerParams = serverParams;
- return this;
- }
-
- /**
- * Sets public key used to encrypt recovery blob.
- *
- * @param publicKey The public key
- * @return This builder.
- */
- public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
- mInstance.mPublicKey = publicKey;
- return this;
- }
-
- /**
- * Sets UI and key derivation parameters
- *
- * @param recoveryMetadata The UI and key derivation parameters
- * @return This builder.
- */
- public Builder setKeychainProtectionParams(
- @NonNull List<KeychainProtectionParams> recoveryMetadata) {
- mInstance.mKeychainProtectionParams = recoveryMetadata;
- return this;
- }
-
- /**
- * List of application keys.
- *
- * @param entryRecoveryData List of application keys
- * @return This builder.
- */
- public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
- mInstance.mEntryRecoveryData = entryRecoveryData;
- return this;
- }
-
- /**
- * Sets recovery key blob
- *
- * @param encryptedRecoveryKeyBlob The recovery key blob.
- * @return This builder.
- */
- public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
- mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
- return this;
- }
-
-
- /**
- * Creates a new {@link KeychainSnapshot} instance.
- *
- * @return new instance
- * @throws NullPointerException if some required fields were not set.
- */
- @NonNull public KeychainSnapshot build() {
- Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams,
- "recoveryMetadata");
- Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
- "entryRecoveryData");
- Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
- Preconditions.checkNotNull(mInstance.mServerParams);
- Preconditions.checkNotNull(mInstance.mPublicKey);
- return mInstance;
- }
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeInt(mSnapshotVersion);
- out.writeTypedList(mKeychainProtectionParams);
- out.writeByteArray(mEncryptedRecoveryKeyBlob);
- out.writeTypedList(mEntryRecoveryData);
- }
-
- /**
- * @hide
- */
- protected KeychainSnapshot(Parcel in) {
- mSnapshotVersion = in.readInt();
- mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR);
- mEncryptedRecoveryKeyBlob = in.createByteArray();
- mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/keystore/RecoveryClaim.java b/android/security/keystore/RecoveryClaim.java
deleted file mode 100644
index 12be607a..00000000
--- a/android/security/keystore/RecoveryClaim.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoverySession}.
- * @hide
- */
-public class RecoveryClaim {
-
- private final RecoverySession mRecoverySession;
- private final byte[] mClaimBytes;
-
- RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
- mRecoverySession = recoverySession;
- mClaimBytes = claimBytes;
- }
-
- /**
- * Returns the session associated with the recovery attempt. This is used to match the symmetric
- * key, which remains internal to the framework, for decrypting the claim response.
- *
- * @return The session data.
- */
- public RecoverySession getRecoverySession() {
- return mRecoverySession;
- }
-
- /**
- * Returns the encrypted claim's bytes.
- *
- * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
- * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
- * to the device.
- */
- public byte[] getClaimBytes() {
- return mClaimBytes;
- }
-}
diff --git a/android/security/keystore/RecoveryController.java b/android/security/keystore/RecoveryController.java
deleted file mode 100644
index ca67e35b..00000000
--- a/android/security/keystore/RecoveryController.java
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.PendingIntent;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
-import android.util.Log;
-
-import com.android.internal.widget.ILockSettings;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoveryController}.
- * @hide
- */
-public class RecoveryController {
- private static final String TAG = "RecoveryController";
-
- /** Key has been successfully synced. */
- public static final int RECOVERY_STATUS_SYNCED = 0;
- /** Waiting for recovery agent to sync the key. */
- public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
- /** Recovery account is not available. */
- public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
- /** Key cannot be synced. */
- public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
-
- /**
- * Failed because no snapshot is yet pending to be synced for the user.
- *
- * @hide
- */
- public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
-
- /**
- * Failed due to an error internal to the recovery service. This is unexpected and indicates
- * either a problem with the logic in the service, or a problem with a dependency of the
- * service (such as AndroidKeyStore).
- *
- * @hide
- */
- public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
-
- /**
- * Failed because the user does not have a lock screen set.
- *
- * @hide
- */
- public static final int ERROR_INSECURE_USER = 23;
-
- /**
- * Error thrown when attempting to use a recovery session that has since been closed.
- *
- * @hide
- */
- public static final int ERROR_SESSION_EXPIRED = 24;
-
- /**
- * Failed because the provided certificate was not a valid X509 certificate.
- *
- * @hide
- */
- public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
-
- /**
- * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
- * the data has become corrupted, the data has been tampered with, etc.
- *
- * @hide
- */
- public static final int ERROR_DECRYPTION_FAILED = 26;
-
-
- private final ILockSettings mBinder;
-
- private RecoveryController(ILockSettings binder) {
- mBinder = binder;
- }
-
- /**
- * Deprecated.
- * Gets a new instance of the class.
- */
- public static RecoveryController getInstance() {
- throw new UnsupportedOperationException("using Deprecated RecoveryController version");
- }
-
- /**
- * Initializes key recovery service for the calling application. RecoveryController
- * randomly chooses one of the keys from the list and keeps it to use for future key export
- * operations. Collection of all keys in the list must be signed by the provided {@code
- * rootCertificateAlias}, which must also be present in the list of root certificates
- * preinstalled on the device. The random selection allows RecoveryController to select
- * which of a set of remote recovery service devices will be used.
- *
- * <p>In addition, RecoveryController enforces a delay of three months between
- * consecutive initialization attempts, to limit the ability of an attacker to often switch
- * remote recovery devices and significantly increase number of recovery attempts.
- *
- * @param rootCertificateAlias alias of a root certificate preinstalled on the device
- * @param signedPublicKeyList binary blob a list of X509 certificates and signature
- * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void initRecoveryService(
- @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
- throws BadCertificateFormatException, InternalRecoveryServiceException {
- throw new UnsupportedOperationException("Deprecated initRecoveryService method called");
-
- }
-
- /**
- * Returns data necessary to store all recoverable keys for given account. Key material is
- * encrypted with user secret and recovery public key.
- *
- * @param account specific to Recovery agent.
- * @return Data necessary to recover keystore.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account)
- throws InternalRecoveryServiceException {
- try {
- return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getKeyChainSnapshot());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
- return null;
- }
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
- * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
- * most one registered listener at any time.
- *
- * @param intent triggered when new snapshot is available. Unregisters listener if the value is
- * {@code null}.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
- throws InternalRecoveryServiceException {
- try {
- mBinder.setSnapshotCreatedPendingIntent(intent);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
- * version. Version zero is used, if no snapshots were created for the account.
- *
- * @return Map from recovery agent accounts to snapshot versions.
- * @see KeychainSnapshot#getSnapshotVersion
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
- throws InternalRecoveryServiceException {
- throw new UnsupportedOperationException();
- }
-
- /**
- * Server parameters used to generate new recovery key blobs. This value will be included in
- * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
- * in vaultParams {@link #startRecoverySession}
- *
- * @param serverParams included in recovery key blob.
- * @see #getRecoveryData
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
- try {
- mBinder.setServerParams(serverParams);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Updates recovery status for given keys. It is used to notify keystore that key was
- * successfully stored on the server or there were an error. Application can check this value
- * using {@code getRecoveyStatus}.
- *
- * @param packageName Application whose recoverable keys' statuses are to be updated.
- * @param aliases List of application-specific key aliases. If the array is empty, updates the
- * status for all existing recoverable keys.
- * @param status Status specific to recovery agent.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void setRecoveryStatus(
- @NonNull String packageName, @Nullable String[] aliases, int status)
- throws NameNotFoundException, InternalRecoveryServiceException {
- try {
- for (String alias : aliases) {
- mBinder.setRecoveryStatus(alias, status);
- }
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
- * Negative status values are reserved for recovery agent specific codes. List of common codes:
- *
- * <ul>
- * <li>{@link #RECOVERY_STATUS_SYNCED}
- * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
- * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
- * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
- * </ul>
- *
- * @return {@code Map} from KeyStore alias to recovery status.
- * @see #setRecoveryStatus
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException {
- try {
- // IPC doesn't support generic Maps.
- @SuppressWarnings("unchecked")
- Map<String, Integer> result =
- (Map<String, Integer>) mBinder.getRecoveryStatus();
- return result;
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
- * is necessary to recover data.
- *
- * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link
- * KeychainProtectionParams#TYPE_CUSTOM_PASSWORD}
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void setRecoverySecretTypes(
- @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes)
- throws InternalRecoveryServiceException {
- try {
- mBinder.setRecoverySecretTypes(secretTypes);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
- * necessary to generate KeychainSnapshot.
- *
- * @return list of recovery secret types
- * @see KeychainSnapshot
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
- throws InternalRecoveryServiceException {
- try {
- return mBinder.getRecoverySecretTypes();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
- * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
- * called.
- *
- * @return list of recovery secret types
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- @NonNull
- public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
- throws InternalRecoveryServiceException {
- throw new UnsupportedOperationException();
- }
-
- /**
- * Initializes recovery session and returns a blob with proof of recovery secrets possession.
- * The method generates symmetric key for a session, which trusted remote device can use to
- * return recovery key.
- *
- * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
- * used to create the recovery blob on the source device.
- * Keystore will verify the certificate using root of trust.
- * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
- * Used to limit number of guesses.
- * @param vaultChallenge Data passed from server for this recovery session and used to prevent
- * replay attacks
- * @param secrets Secrets provided by user, the method only uses type and secret fields.
- * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
- * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
- * key and parameters necessary to identify the counter with the number of failed recovery
- * attempts.
- * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect
- * format.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- @NonNull public RecoveryClaim startRecoverySession(
- @NonNull byte[] verifierPublicKey,
- @NonNull byte[] vaultParams,
- @NonNull byte[] vaultChallenge,
- @NonNull List<KeychainProtectionParams> secrets)
- throws BadCertificateFormatException, InternalRecoveryServiceException {
- try {
- RecoverySession recoverySession = RecoverySession.newInstance(this);
- byte[] recoveryClaim =
- mBinder.startRecoverySession(
- recoverySession.getSessionId(),
- verifierPublicKey,
- vaultParams,
- vaultChallenge,
- BackwardsCompat.fromLegacyKeychainProtectionParams(secrets));
- return new RecoveryClaim(recoverySession, recoveryClaim);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
- throw new BadCertificateFormatException(e.getMessage());
- }
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Imports keys.
- *
- * @param session Related recovery session, as originally created by invoking
- * {@link #startRecoverySession(byte[], byte[], byte[], List)}.
- * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
- * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
- * and session. KeyStore only uses package names from the application info in {@link
- * WrappedApplicationKey}. Caller is responsibility to perform certificates check.
- * @return Map from alias to raw key material.
- * @throws SessionExpiredException if {@code session} has since been closed.
- * @throws DecryptionFailedException if unable to decrypt the snapshot.
- * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
- */
- public Map<String, byte[]> recoverKeys(
- @NonNull RecoverySession session,
- @NonNull byte[] recoveryKeyBlob,
- @NonNull List<WrappedApplicationKey> applicationKeys)
- throws SessionExpiredException, DecryptionFailedException,
- InternalRecoveryServiceException {
- try {
- return (Map<String, byte[]>) mBinder.recoverKeys(
- session.getSessionId(),
- recoveryKeyBlob,
- BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys));
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- if (e.errorCode == ERROR_DECRYPTION_FAILED) {
- throw new DecryptionFailedException(e.getMessage());
- }
- if (e.errorCode == ERROR_SESSION_EXPIRED) {
- throw new SessionExpiredException(e.getMessage());
- }
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- /**
- * Deletes all data associated with {@code session}. Should not be invoked directly but via
- * {@link RecoverySession#close()}.
- *
- * @hide
- */
- void closeSession(RecoverySession session) {
- try {
- mBinder.closeSession(session.getSessionId());
- } catch (RemoteException | ServiceSpecificException e) {
- Log.e(TAG, "Unexpected error trying to close session", e);
- }
- }
-
- /**
- * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
- * raw material of the key.
- *
- * @param alias The key alias.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
- * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
- * lock screen.
- */
- public byte[] generateAndStoreKey(@NonNull String alias)
- throws InternalRecoveryServiceException, LockScreenRequiredException {
- throw new UnsupportedOperationException();
- }
-
- /**
- * Removes a key called {@code alias} from the recoverable key store.
- *
- * @param alias The key alias.
- * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
- * service.
- */
- public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
- try {
- mBinder.removeKey(alias);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- } catch (ServiceSpecificException e) {
- throw wrapUnexpectedServiceSpecificException(e);
- }
- }
-
- private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
- ServiceSpecificException e) {
- if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
- return new InternalRecoveryServiceException(e.getMessage());
- }
-
- // Should never happen. If it does, it's a bug, and we need to update how the method that
- // called this throws its exceptions.
- return new InternalRecoveryServiceException("Unexpected error code for method: "
- + e.errorCode, e);
- }
-}
diff --git a/android/security/keystore/RecoveryControllerException.java b/android/security/keystore/RecoveryControllerException.java
deleted file mode 100644
index f990c236..00000000
--- a/android/security/keystore/RecoveryControllerException.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import java.security.GeneralSecurityException;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoveryController}.
- * @hide
- */
-public abstract class RecoveryControllerException extends GeneralSecurityException {
- RecoveryControllerException() { }
-
- RecoveryControllerException(String msg) {
- super(msg);
- }
-
- public RecoveryControllerException(String message, Throwable cause) {
- super(message, cause);
- }
-}
diff --git a/android/security/keystore/RecoverySession.java b/android/security/keystore/RecoverySession.java
deleted file mode 100644
index 8a3e06b7..00000000
--- a/android/security/keystore/RecoverySession.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import java.security.SecureRandom;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.RecoverySession}.
- * @hide
- */
-public class RecoverySession implements AutoCloseable {
-
- private static final int SESSION_ID_LENGTH_BYTES = 16;
-
- private final String mSessionId;
- private final RecoveryController mRecoveryController;
-
- private RecoverySession(RecoveryController recoveryController, String sessionId) {
- mRecoveryController = recoveryController;
- mSessionId = sessionId;
- }
-
- /**
- * A new session, started by {@code recoveryManager}.
- */
- static RecoverySession newInstance(RecoveryController recoveryController) {
- return new RecoverySession(recoveryController, newSessionId());
- }
-
- /**
- * Returns a new random session ID.
- */
- private static String newSessionId() {
- SecureRandom secureRandom = new SecureRandom();
- byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
- secureRandom.nextBytes(sessionId);
- StringBuilder sb = new StringBuilder();
- for (byte b : sessionId) {
- sb.append(Byte.toHexString(b, /*upperCase=*/ false));
- }
- return sb.toString();
- }
-
- /**
- * An internal session ID, used by the framework to match recovery claims to snapshot responses.
- */
- String getSessionId() {
- return mSessionId;
- }
-
- @Override
- public void close() {
- mRecoveryController.closeSession(this);
- }
-}
diff --git a/android/security/keystore/UserPresenceUnavailableException.java b/android/security/keystore/UserPresenceUnavailableException.java
index cf4099ef..1b053a5c 100644
--- a/android/security/keystore/UserPresenceUnavailableException.java
+++ b/android/security/keystore/UserPresenceUnavailableException.java
@@ -16,13 +16,13 @@
package android.security.keystore;
-import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
/**
* Indicates the condition that a proof of user-presence was
* requested but this proof was not presented.
*/
-public class UserPresenceUnavailableException extends InvalidAlgorithmParameterException {
+public class UserPresenceUnavailableException extends InvalidKeyException {
/**
* Constructs a {@code UserPresenceUnavailableException} without a detail message or cause.
*/
diff --git a/android/security/keystore/WrappedApplicationKey.java b/android/security/keystore/WrappedApplicationKey.java
deleted file mode 100644
index 2ce8c7d3..00000000
--- a/android/security/keystore/WrappedApplicationKey.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.keystore;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.Preconditions;
-
-/**
- * @deprecated Use {@link android.security.keystore.recovery.WrappedApplicationKey}.
- * @hide
- */
-public final class WrappedApplicationKey implements Parcelable {
- private String mAlias;
- // The only supported format is AES-256 symmetric key.
- private byte[] mEncryptedKeyMaterial;
-
- /**
- * Builder for creating {@link WrappedApplicationKey}.
- */
- public static class Builder {
- private WrappedApplicationKey mInstance = new WrappedApplicationKey();
-
- /**
- * Sets Application-specific alias of the key.
- *
- * @param alias The alias.
- * @return This builder.
- */
- public Builder setAlias(@NonNull String alias) {
- mInstance.mAlias = alias;
- return this;
- }
-
- /**
- * Sets key material encrypted by recovery key.
- *
- * @param encryptedKeyMaterial The key material
- * @return This builder
- */
-
- public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
- mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
- return this;
- }
-
- /**
- * Creates a new {@link WrappedApplicationKey} instance.
- *
- * @return new instance
- * @throws NullPointerException if some required fields were not set.
- */
- @NonNull public WrappedApplicationKey build() {
- Preconditions.checkNotNull(mInstance.mAlias);
- Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
- return mInstance;
- }
- }
-
- private WrappedApplicationKey() {
-
- }
-
- /**
- * Deprecated - consider using Builder.
- * @hide
- */
- public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
- mAlias = Preconditions.checkNotNull(alias);
- mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
- }
-
- /**
- * Application-specific alias of the key.
- *
- * @see java.security.KeyStore.aliases
- */
- public @NonNull String getAlias() {
- return mAlias;
- }
-
- /** Key material encrypted by recovery key. */
- public @NonNull byte[] getEncryptedKeyMaterial() {
- return mEncryptedKeyMaterial;
- }
-
- public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
- new Parcelable.Creator<WrappedApplicationKey>() {
- public WrappedApplicationKey createFromParcel(Parcel in) {
- return new WrappedApplicationKey(in);
- }
-
- public WrappedApplicationKey[] newArray(int length) {
- return new WrappedApplicationKey[length];
- }
- };
-
- /**
- * @hide
- */
- @Override
- public void writeToParcel(Parcel out, int flags) {
- out.writeString(mAlias);
- out.writeByteArray(mEncryptedKeyMaterial);
- }
-
- /**
- * @hide
- */
- protected WrappedApplicationKey(Parcel in) {
- mAlias = in.readString();
- mEncryptedKeyMaterial = in.createByteArray();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-}
diff --git a/android/security/keystore/recovery/RecoveryController.java b/android/security/keystore/recovery/RecoveryController.java
index 281822a3..b84843bf 100644
--- a/android/security/keystore/recovery/RecoveryController.java
+++ b/android/security/keystore/recovery/RecoveryController.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -250,6 +251,16 @@ public class RecoveryController {
*/
public static final int ERROR_INVALID_CERTIFICATE = 28;
+
+ /**
+ * Failed because the provided certificate contained serial version which is lower that the
+ * version device is already initialized with. It is not possible to downgrade serial version of
+ * the provided certificate.
+ *
+ * @hide
+ */
+ public static final int ERROR_DOWNGRADE_CERTIFICATE = 29;
+
private final ILockSettings mBinder;
private final KeyStore mKeyStore;
@@ -278,6 +289,18 @@ public class RecoveryController {
}
/**
+ * Checks whether the recoverable key store is currently available.
+ *
+ * <p>If it returns true, the device must currently be using a screen lock that is supported for
+ * use with the recoverable key store, i.e. AOSP PIN, pattern or password.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public static boolean isRecoverableKeyStoreEnabled(@NonNull Context context) {
+ KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
+ return keyguardManager != null && keyguardManager.isDeviceSecure();
+ }
+
+ /**
* @deprecated Use {@link #initRecoveryService(String, byte[], byte[])} instead.
*/
@Deprecated
@@ -340,6 +363,10 @@ public class RecoveryController {
|| e.errorCode == ERROR_INVALID_CERTIFICATE) {
throw new CertificateException("Invalid certificate for recovery service", e);
}
+ if (e.errorCode == ERROR_DOWNGRADE_CERTIFICATE) {
+ throw new CertificateException(
+ "Downgrading certificate serial version isn't supported.", e);
+ }
throw wrapUnexpectedServiceSpecificException(e);
}
}
diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
index 3726e66d..32737c54 100644
--- a/android/service/notification/NotificationListenerService.java
+++ b/android/service/notification/NotificationListenerService.java
@@ -28,6 +28,7 @@ import android.app.Notification.Builder;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
+import android.app.Person;
import android.app.Service;
import android.companion.CompanionDeviceManager;
import android.content.ComponentName;
@@ -1195,13 +1196,13 @@ public abstract class NotificationListenerService extends Service {
*/
private void maybePopulatePeople(Notification notification) {
if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) {
- ArrayList<Notification.Person> people = notification.extras.getParcelableArrayList(
+ ArrayList<Person> people = notification.extras.getParcelableArrayList(
Notification.EXTRA_PEOPLE_LIST);
if (people != null && people.isEmpty()) {
int size = people.size();
String[] peopleArray = new String[size];
for (int i = 0; i < size; i++) {
- Notification.Person person = people.get(i);
+ Person person = people.get(i);
peopleArray[i] = person.resolveToLegacyUri();
}
notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray);
diff --git a/android/service/notification/ZenModeConfig.java b/android/service/notification/ZenModeConfig.java
index 3830b7ac..7b01f7a4 100644
--- a/android/service/notification/ZenModeConfig.java
+++ b/android/service/notification/ZenModeConfig.java
@@ -83,7 +83,8 @@ public class ZenModeConfig implements Parcelable {
private static final int DAY_MINUTES = 24 * 60;
private static final int ZERO_VALUE_MS = 10 * SECONDS_MS;
- // Default allow categories set in readXml() from default_zen_mode_config.xml, fallback values:
+ // Default allow categories set in readXml() from default_zen_mode_config.xml,
+ // fallback/upgrade values:
private static final boolean DEFAULT_ALLOW_ALARMS = true;
private static final boolean DEFAULT_ALLOW_MEDIA = true;
private static final boolean DEFAULT_ALLOW_SYSTEM = false;
@@ -97,7 +98,7 @@ public class ZenModeConfig implements Parcelable {
private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS =
Policy.getAllSuppressedVisualEffects();
- public static final int XML_VERSION = 6;
+ public static final int XML_VERSION = 7;
public static final String ZEN_TAG = "zen";
private static final String ZEN_ATT_VERSION = "version";
private static final String ZEN_ATT_USER = "user";
@@ -1487,14 +1488,18 @@ public class ZenModeConfig implements Parcelable {
/**
* Returns a description of the current do not disturb settings from config.
* - If turned on manually and end time is known, returns end time.
+ * - If turned on manually and end time is on forever until turned off, return null if
+ * describeForeverCondition is false, else return String describing indefinite behavior
* - If turned on by an automatic rule, returns the automatic rule name.
* - If on due to an app, returns the app name.
* - If there's a combination of rules/apps that trigger, then shows the one that will
* last the longest if applicable.
- * @return null if do not disturb is off.
+ * @return null if DND is off or describeForeverCondition is false and
+ * DND is on forever (until turned off)
*/
- public static String getDescription(Context context, boolean zenOn, ZenModeConfig config) {
- if (!zenOn) {
+ public static String getDescription(Context context, boolean zenOn, ZenModeConfig config,
+ boolean describeForeverCondition) {
+ if (!zenOn || config == null) {
return null;
}
@@ -1513,8 +1518,11 @@ public class ZenModeConfig implements Parcelable {
} else {
if (id == null) {
// Do not disturb manually triggered to remain on forever until turned off
- // No subtext
- return null;
+ if (describeForeverCondition) {
+ return context.getString(R.string.zen_mode_forever);
+ } else {
+ return null;
+ }
} else {
latestEndTime = tryParseCountdownConditionId(id);
if (latestEndTime > 0) {
diff --git a/android/service/textclassifier/TextClassifierService.java b/android/service/textclassifier/TextClassifierService.java
index f1bb72cf..b461c0da 100644
--- a/android/service/textclassifier/TextClassifierService.java
+++ b/android/service/textclassifier/TextClassifierService.java
@@ -17,6 +17,7 @@
package android.service.textclassifier;
import android.Manifest;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -97,7 +98,8 @@ public abstract class TextClassifierService extends Service {
Preconditions.checkNotNull(request);
Preconditions.checkNotNull(callback);
TextClassifierService.this.onSuggestSelection(
- sessionId, request, mCancellationSignal,
+ request.getText(), request.getStartIndex(), request.getEndIndex(),
+ TextSelection.Options.from(sessionId, request), mCancellationSignal,
new Callback<TextSelection>() {
@Override
public void onSuccess(TextSelection result) {
@@ -130,7 +132,8 @@ public abstract class TextClassifierService extends Service {
Preconditions.checkNotNull(request);
Preconditions.checkNotNull(callback);
TextClassifierService.this.onClassifyText(
- sessionId, request, mCancellationSignal,
+ request.getText(), request.getStartIndex(), request.getEndIndex(),
+ TextClassification.Options.from(sessionId, request), mCancellationSignal,
new Callback<TextClassification>() {
@Override
public void onSuccess(TextClassification result) {
@@ -161,7 +164,8 @@ public abstract class TextClassifierService extends Service {
Preconditions.checkNotNull(request);
Preconditions.checkNotNull(callback);
TextClassifierService.this.onGenerateLinks(
- sessionId, request, mCancellationSignal,
+ request.getText(), TextLinks.Options.from(sessionId, request),
+ mCancellationSignal,
new Callback<TextLinks>() {
@Override
public void onSuccess(TextLinks result) {
@@ -234,6 +238,25 @@ public abstract class TextClassifierService extends Service {
@NonNull CancellationSignal cancellationSignal,
@NonNull Callback<TextSelection> callback);
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ public void onSuggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextSelection> callback) {
+ final TextClassificationSessionId sessionId = options.getSessionId();
+ final TextSelection.Request request = options.getRequest() != null
+ ? options.getRequest()
+ : new TextSelection.Request.Builder(
+ text, selectionStartIndex, selectionEndIndex)
+ .setDefaultLocales(options.getDefaultLocales())
+ .build();
+ onSuggestSelection(sessionId, request, cancellationSignal, callback);
+ }
+
/**
* Classifies the specified text and returns a {@link TextClassification} object that can be
* used to generate a widget for handling the classified text.
@@ -249,6 +272,26 @@ public abstract class TextClassifierService extends Service {
@NonNull CancellationSignal cancellationSignal,
@NonNull Callback<TextClassification> callback);
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ public void onClassifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable TextClassification.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextClassification> callback) {
+ final TextClassificationSessionId sessionId = options.getSessionId();
+ final TextClassification.Request request = options.getRequest() != null
+ ? options.getRequest()
+ : new TextClassification.Request.Builder(
+ text, startIndex, endIndex)
+ .setDefaultLocales(options.getDefaultLocales())
+ .setReferenceTime(options.getReferenceTime())
+ .build();
+ onClassifyText(sessionId, request, cancellationSignal, callback);
+ }
+
/**
* Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
* links information.
@@ -264,6 +307,23 @@ public abstract class TextClassifierService extends Service {
@NonNull CancellationSignal cancellationSignal,
@NonNull Callback<TextLinks> callback);
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ public void onGenerateLinks(
+ @NonNull CharSequence text,
+ @Nullable TextLinks.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextLinks> callback) {
+ final TextClassificationSessionId sessionId = options.getSessionId();
+ final TextLinks.Request request = options.getRequest() != null
+ ? options.getRequest()
+ : new TextLinks.Request.Builder(text)
+ .setDefaultLocales(options.getDefaultLocales())
+ .setEntityConfig(options.getEntityConfig())
+ .build();
+ onGenerateLinks(sessionId, request, cancellationSignal, callback);
+ }
+
/**
* Writes the selection event.
* This is called when a selection event occurs. e.g. user changed selection; or smart selection
@@ -283,17 +343,17 @@ public abstract class TextClassifierService extends Service {
* @param context the text classification context
* @param sessionId the session's Id
*/
- public abstract void onCreateTextClassificationSession(
+ public void onCreateTextClassificationSession(
@NonNull TextClassificationContext context,
- @NonNull TextClassificationSessionId sessionId);
+ @NonNull TextClassificationSessionId sessionId) {}
/**
* Destroys the text classification session identified by the specified sessionId.
*
* @param sessionId the id of the session to destroy
*/
- public abstract void onDestroyTextClassificationSession(
- @NonNull TextClassificationSessionId sessionId);
+ public void onDestroyTextClassificationSession(
+ @NonNull TextClassificationSessionId sessionId) {}
/**
* Returns a TextClassifier that runs in this service's process.
diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
index a1327301..7f75f0a6 100644
--- a/android/service/wallpaper/WallpaperService.java
+++ b/android/service/wallpaper/WallpaperService.java
@@ -813,7 +813,7 @@ public abstract class WallpaperService extends Service {
}
final int relayoutResult = mSession.relayout(
mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
- View.VISIBLE, 0, mWinFrame, mOverscanInsets, mContentInsets,
+ View.VISIBLE, 0, -1, mWinFrame, mOverscanInsets, mContentInsets,
mVisibleInsets, mStableInsets, mOutsets, mBackdropFrame,
mDisplayCutout, mMergedConfiguration, mSurfaceHolder.mSurface);
diff --git a/android/support/v4/media/session/MediaControllerCompat.java b/android/support/v4/media/session/MediaControllerCompat.java
index 5e6f4eac..4a4ad322 100644
--- a/android/support/v4/media/session/MediaControllerCompat.java
+++ b/android/support/v4/media/session/MediaControllerCompat.java
@@ -667,13 +667,13 @@ public final class MediaControllerCompat {
public static abstract class Callback implements IBinder.DeathRecipient {
private final Object mCallbackObj;
MessageHandler mHandler;
- boolean mHasExtraCallback;
+ IMediaControllerCallback mIControllerCallback;
public Callback() {
if (android.os.Build.VERSION.SDK_INT >= 21) {
mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this));
} else {
- mCallbackObj = new StubCompat(this);
+ mCallbackObj = mIControllerCallback = new StubCompat(this);
}
}
@@ -789,6 +789,14 @@ public final class MediaControllerCompat {
public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
}
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public IMediaControllerCallback getIControllerCallback() {
+ return mIControllerCallback;
+ }
+
@Override
public void binderDied() {
onSessionDestroyed();
@@ -837,7 +845,8 @@ public final class MediaControllerCompat {
public void onSessionEvent(String event, Bundle extras) {
MediaControllerCompat.Callback callback = mCallback.get();
if (callback != null) {
- if (callback.mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) {
+ if (callback.mIControllerCallback != null
+ && android.os.Build.VERSION.SDK_INT < 23) {
// Ignore. ExtraCallback will handle this.
} else {
callback.onSessionEvent(event, extras);
@@ -849,7 +858,7 @@ public final class MediaControllerCompat {
public void onPlaybackStateChanged(Object stateObj) {
MediaControllerCompat.Callback callback = mCallback.get();
if (callback != null) {
- if (callback.mHasExtraCallback) {
+ if (callback.mIControllerCallback != null) {
// Ignore. ExtraCallback will handle this.
} else {
callback.onPlaybackStateChanged(
@@ -1944,7 +1953,7 @@ public final class MediaControllerCompat {
if (mExtraBinder != null) {
ExtraCallback extraCallback = new ExtraCallback(callback);
mCallbackMap.put(callback, extraCallback);
- callback.mHasExtraCallback = true;
+ callback.mIControllerCallback = extraCallback;
try {
mExtraBinder.registerCallbackListener(extraCallback);
} catch (RemoteException e) {
@@ -1952,7 +1961,7 @@ public final class MediaControllerCompat {
}
} else {
synchronized (mPendingCallbacks) {
- callback.mHasExtraCallback = false;
+ callback.mIControllerCallback = null;
mPendingCallbacks.add(callback);
}
}
@@ -1965,7 +1974,7 @@ public final class MediaControllerCompat {
try {
ExtraCallback extraCallback = mCallbackMap.remove(callback);
if (extraCallback != null) {
- callback.mHasExtraCallback = false;
+ callback.mIControllerCallback = null;
mExtraBinder.unregisterCallbackListener(extraCallback);
}
} catch (RemoteException e) {
@@ -2173,7 +2182,7 @@ public final class MediaControllerCompat {
for (Callback callback : mPendingCallbacks) {
ExtraCallback extraCallback = new ExtraCallback(callback);
mCallbackMap.put(callback, extraCallback);
- callback.mHasExtraCallback = true;
+ callback.mIControllerCallback = extraCallback;
try {
mExtraBinder.registerCallbackListener(extraCallback);
} catch (RemoteException e) {
diff --git a/android/support/v4/media/session/PlaybackStateCompat.java b/android/support/v4/media/session/PlaybackStateCompat.java
index e6420ea0..b9c51caa 100644
--- a/android/support/v4/media/session/PlaybackStateCompat.java
+++ b/android/support/v4/media/session/PlaybackStateCompat.java
@@ -15,7 +15,6 @@
*/
package android.support.v4.media.session;
-
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.os.Build;
diff --git a/android/system/Os.java b/android/system/Os.java
index 404b8221..6301ff9f 100644
--- a/android/system/Os.java
+++ b/android/system/Os.java
@@ -186,17 +186,6 @@ public final class Os {
public static int getgid() { return Libcore.os.getgid(); }
/**
- * See <a href="http://man7.org/linux/man-pages/man2/getgroups.2.html">getgroups(2)</a>.
- *
- * <p>Should the number of groups change during the execution of this call, the call may
- * return an arbitrary subset. This may be worth reconsidering should this be exposed
- * as public API.
- *
- * @hide
- */
- public static int[] getgroups() throws ErrnoException { return Libcore.os.getgroups(); }
-
- /**
* See <a href="http://man7.org/linux/man-pages/man3/getenv.3.html">getenv(3)</a>.
*/
public static String getenv(String name) { return Libcore.os.getenv(name); }
@@ -507,13 +496,6 @@ public final class Os {
public static void setgid(int gid) throws ErrnoException { Libcore.os.setgid(gid); }
/**
- * See <a href="http://man7.org/linux/man-pages/man2/setgroups.2.html">setgroups(2)</a>.
- *
- * @hide
- */
- public static void setgroups(int[] gids) throws ErrnoException { Libcore.os.setgroups(gids); }
-
- /**
* See <a href="http://man7.org/linux/man-pages/man2/setpgid.2.html">setpgid(2)</a>.
*/
/** @hide */ public static void setpgid(int pid, int pgid) throws ErrnoException { Libcore.os.setpgid(pid, pgid); }
diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java
index 36333e44..3bf951d4 100644
--- a/android/telecom/Connection.java
+++ b/android/telecom/Connection.java
@@ -2600,7 +2600,6 @@ public abstract class Connection extends Conferenceable {
}
/**
- *
* Request audio routing to a specific bluetooth device. Calling this method may result in
* the device routing audio to a different bluetooth device than the one specified if the
* bluetooth stack is unable to route audio to the requested device.
@@ -2611,13 +2610,13 @@ public abstract class Connection extends Conferenceable {
* Used by self-managed {@link ConnectionService}s which wish to use bluetooth audio for a
* self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.)
* <p>
- * See also {@link InCallService#requestBluetoothAudio(String)}
- * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by
- * {@link BluetoothDevice#getAddress()}.
+ * See also {@link InCallService#requestBluetoothAudio(BluetoothDevice)}
+ * @param bluetoothDevice The bluetooth device to connect to.
*/
- public void requestBluetoothAudio(@NonNull String bluetoothAddress) {
+ public void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) {
for (Listener l : mListeners) {
- l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH, bluetoothAddress);
+ l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH,
+ bluetoothDevice.getAddress());
}
}
diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java
index af65c65a..bd25ab2b 100644
--- a/android/telecom/InCallService.java
+++ b/android/telecom/InCallService.java
@@ -428,12 +428,11 @@ public abstract class InCallService extends Service {
* A list of available devices can be obtained via
* {@link CallAudioState#getSupportedBluetoothDevices()}
*
- * @param bluetoothAddress The address of the bluetooth device to connect to, as returned by
- * {@link BluetoothDevice#getAddress()}.
+ * @param bluetoothDevice The bluetooth device to connect to.
*/
- public final void requestBluetoothAudio(@NonNull String bluetoothAddress) {
+ public final void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) {
if (mPhone != null) {
- mPhone.requestBluetoothAudio(bluetoothAddress);
+ mPhone.requestBluetoothAudio(bluetoothDevice.getAddress());
}
}
diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java
index 95eb14ad..b3a3bf21 100644
--- a/android/telecom/PhoneAccount.java
+++ b/android/telecom/PhoneAccount.java
@@ -129,6 +129,9 @@ public final class PhoneAccount implements Parcelable {
* <p>
* By default, Self-Managed {@link PhoneAccount}s do not log their calls to the call log.
* Setting this extra to {@code true} provides a means for them to log their calls.
+ * <p>
+ * Note: Only calls where the {@link Call.Details#getHandle()} {@link Uri#getScheme()} is
+ * {@link #SCHEME_SIP} or {@link #SCHEME_TEL} will be logged at the current time.
*/
public static final String EXTRA_LOG_SELF_MANAGED_CALLS =
"android.telecom.extra.LOG_SELF_MANAGED_CALLS";
diff --git a/android/telephony/AccessNetworkConstants.java b/android/telephony/AccessNetworkConstants.java
index cac9f2b5..3b773b3a 100644
--- a/android/telephony/AccessNetworkConstants.java
+++ b/android/telephony/AccessNetworkConstants.java
@@ -16,8 +16,6 @@
package android.telephony;
-import android.annotation.SystemApi;
-
/**
* Contains access network related constants.
*/
@@ -39,7 +37,6 @@ public final class AccessNetworkConstants {
* Wireless transportation type
* @hide
*/
- @SystemApi
public static final class TransportType {
/** Wireless Wide Area Networks (i.e. Cellular) */
public static final int WWAN = 1;
diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java
index 4683161d..e2441316 100644
--- a/android/telephony/CarrierConfigManager.java
+++ b/android/telephony/CarrierConfigManager.java
@@ -1684,6 +1684,14 @@ public class CarrierConfigManager {
"data_warning_threshold_bytes_long";
/**
+ * Controls if the device should automatically notify the user as they reach
+ * their cellular data warning. When set to {@code false} the carrier is
+ * expected to have implemented their own notification mechanism.
+ */
+ public static final String KEY_DATA_WARNING_NOTIFICATION_BOOL =
+ "data_warning_notification_bool";
+
+ /**
* Controls the cellular data limit.
* <p>
* If the user uses more than this amount of data in their billing cycle, as defined by
@@ -1698,6 +1706,22 @@ public class CarrierConfigManager {
"data_limit_threshold_bytes_long";
/**
+ * Controls if the device should automatically notify the user as they reach
+ * their cellular data limit. When set to {@code false} the carrier is
+ * expected to have implemented their own notification mechanism.
+ */
+ public static final String KEY_DATA_LIMIT_NOTIFICATION_BOOL =
+ "data_limit_notification_bool";
+
+ /**
+ * Controls if the device should automatically notify the user when rapid
+ * cellular data usage is observed. When set to {@code false} the carrier is
+ * expected to have implemented their own notification mechanism.
+ */
+ public static final String KEY_DATA_RAPID_NOTIFICATION_BOOL =
+ "data_rapid_notification_bool";
+
+ /**
* Offset to be reduced from rsrp threshold while calculating signal strength level.
* @hide
*/
@@ -1954,7 +1978,7 @@ public class CarrierConfigManager {
sDefaults.putBoolean(KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_VOLTE_OVERRIDE_WFC_PROVISIONING_BOOL, false);
- sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, false);
+ sDefaults.putBoolean(KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL, true);
sDefaults.putBoolean(KEY_CARRIER_ALLOW_TURNOFF_IMS_BOOL, true);
sDefaults.putBoolean(KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_INSTANT_LETTERING_AVAILABLE_BOOL, false);
@@ -2165,7 +2189,10 @@ public class CarrierConfigManager {
sDefaults.putInt(KEY_MONTHLY_DATA_CYCLE_DAY_INT, DATA_CYCLE_USE_PLATFORM_DEFAULT);
sDefaults.putLong(KEY_DATA_WARNING_THRESHOLD_BYTES_LONG, DATA_CYCLE_USE_PLATFORM_DEFAULT);
+ sDefaults.putBoolean(KEY_DATA_WARNING_NOTIFICATION_BOOL, true);
sDefaults.putLong(KEY_DATA_LIMIT_THRESHOLD_BYTES_LONG, DATA_CYCLE_USE_PLATFORM_DEFAULT);
+ sDefaults.putBoolean(KEY_DATA_LIMIT_NOTIFICATION_BOOL, true);
+ sDefaults.putBoolean(KEY_DATA_RAPID_NOTIFICATION_BOOL, true);
// Rat families: {GPRS, EDGE}, {EVDO, EVDO_A, EVDO_B}, {UMTS, HSPA, HSDPA, HSUPA, HSPAP},
// {LTE, LTE_CA}
@@ -2187,7 +2214,7 @@ public class CarrierConfigManager {
sDefaults.putStringArray(KEY_FILTERED_CNAP_NAMES_STRING_ARRAY, null);
sDefaults.putBoolean(KEY_EDITABLE_WFC_ROAMING_MODE_BOOL, false);
sDefaults.putBoolean(KEY_STK_DISABLE_LAUNCH_BROWSER_BOOL, false);
- sDefaults.putBoolean(KEY_PERSIST_LPP_MODE_BOOL, false);
+ sDefaults.putBoolean(KEY_PERSIST_LPP_MODE_BOOL, true);
sDefaults.putStringArray(KEY_CARRIER_WIFI_STRING_ARRAY, null);
sDefaults.putInt(KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT, -1);
sDefaults.putInt(KEY_EMERGENCY_NOTIFICATION_DELAY_INT, -1);
diff --git a/android/telephony/NetworkRegistrationState.java b/android/telephony/NetworkRegistrationState.java
index bba779d0..0e2e0cea 100644
--- a/android/telephony/NetworkRegistrationState.java
+++ b/android/telephony/NetworkRegistrationState.java
@@ -18,7 +18,6 @@ package android.telephony;
import android.annotation.IntDef;
import android.annotation.Nullable;
-import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -31,7 +30,6 @@ import java.util.Objects;
* Description of a mobile network registration state
* @hide
*/
-@SystemApi
public class NetworkRegistrationState implements Parcelable {
/**
* Network domain
diff --git a/android/telephony/NetworkService.java b/android/telephony/NetworkService.java
index 35682a74..b431590d 100644
--- a/android/telephony/NetworkService.java
+++ b/android/telephony/NetworkService.java
@@ -17,7 +17,6 @@
package android.telephony;
import android.annotation.CallSuper;
-import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
@@ -47,7 +46,6 @@ import java.util.List;
* </service>
* @hide
*/
-@SystemApi
public abstract class NetworkService extends Service {
private final String TAG = NetworkService.class.getSimpleName();
@@ -206,8 +204,10 @@ public abstract class NetworkService extends Service {
}
}
- /** @hide */
- protected NetworkService() {
+ /**
+ * Default constructor.
+ */
+ public NetworkService() {
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
diff --git a/android/telephony/NetworkServiceCallback.java b/android/telephony/NetworkServiceCallback.java
index dbad02fd..ad3b00fd 100644
--- a/android/telephony/NetworkServiceCallback.java
+++ b/android/telephony/NetworkServiceCallback.java
@@ -17,7 +17,6 @@
package android.telephony;
import android.annotation.IntDef;
-import android.annotation.SystemApi;
import android.os.RemoteException;
import android.telephony.NetworkService.NetworkServiceProvider;
@@ -33,7 +32,6 @@ import java.lang.ref.WeakReference;
*
* @hide
*/
-@SystemApi
public class NetworkServiceCallback {
private static final String mTag = NetworkServiceCallback.class.getSimpleName();
diff --git a/android/telephony/ServiceState.java b/android/telephony/ServiceState.java
index e971d08a..8ffdb21c 100644
--- a/android/telephony/ServiceState.java
+++ b/android/telephony/ServiceState.java
@@ -17,7 +17,6 @@
package android.telephony;
import android.annotation.IntDef;
-import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Bundle;
import android.os.Parcel;
@@ -977,11 +976,13 @@ public class ServiceState implements Parcelable {
}
/** @hide */
+ @TestApi
public void setCellBandwidths(int[] bandwidths) {
mCellBandwidths = bandwidths;
}
/** @hide */
+ @TestApi
public void setChannelNumber(int channelNumber) {
mChannelNumber = channelNumber;
}
@@ -1172,6 +1173,7 @@ public class ServiceState implements Parcelable {
}
/** @hide */
+ @TestApi
public void setRilVoiceRadioTechnology(int rt) {
if (rt == RIL_RADIO_TECHNOLOGY_LTE_CA) {
rt = RIL_RADIO_TECHNOLOGY_LTE;
@@ -1181,6 +1183,7 @@ public class ServiceState implements Parcelable {
}
/** @hide */
+ @TestApi
public void setRilDataRadioTechnology(int rt) {
if (rt == RIL_RADIO_TECHNOLOGY_LTE_CA) {
rt = RIL_RADIO_TECHNOLOGY_LTE;
@@ -1530,7 +1533,6 @@ public class ServiceState implements Parcelable {
* @return List of registration states
* @hide
*/
- @SystemApi
public List<NetworkRegistrationState> getNetworkRegistrationStates() {
synchronized (mNetworkRegistrationStates) {
return new ArrayList<>(mNetworkRegistrationStates);
@@ -1544,7 +1546,6 @@ public class ServiceState implements Parcelable {
* @return List of registration states.
* @hide
*/
- @SystemApi
public List<NetworkRegistrationState> getNetworkRegistrationStates(int transportType) {
List<NetworkRegistrationState> list = new ArrayList<>();
@@ -1567,7 +1568,6 @@ public class ServiceState implements Parcelable {
* @return The matching NetworkRegistrationState.
* @hide
*/
- @SystemApi
public NetworkRegistrationState getNetworkRegistrationStates(int transportType, int domain) {
synchronized (mNetworkRegistrationStates) {
for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) {
diff --git a/android/telephony/SubscriptionManager.java b/android/telephony/SubscriptionManager.java
index 754fe687..a9389bea 100644
--- a/android/telephony/SubscriptionManager.java
+++ b/android/telephony/SubscriptionManager.java
@@ -477,6 +477,9 @@ public class SubscriptionManager {
* <p>
* Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
* the user is interested in.
+ * <p>
+ * Receivers should protect themselves by checking that the sender holds the
+ * {@code android.permission.MANAGE_SUBSCRIPTION_PLANS} permission.
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
@SystemApi
@@ -1719,6 +1722,8 @@ public class SubscriptionManager {
* </ul>
*
* @param subId the subscriber this relationship applies to
+ * @throws SecurityException if the caller doesn't meet the requirements
+ * outlined above.
*/
@SystemApi
public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) {
@@ -1744,10 +1749,13 @@ public class SubscriptionManager {
* {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
* </ul>
*
- * @param subId the subscriber this relationship applies to
+ * @param subId the subscriber this relationship applies to. An empty list
+ * may be sent to clear any existing plans.
* @param plans the list of plans. The first plan is always the primary and
* most important plan. Any additional plans are secondary and
* may not be displayed or used by decision making logic.
+ * @throws SecurityException if the caller doesn't meet the requirements
+ * outlined above.
*/
@SystemApi
public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) {
@@ -1788,6 +1796,8 @@ public class SubscriptionManager {
* be automatically cleared, or {@code 0} to leave in the
* requested state until explicitly cleared, or the next reboot,
* whichever happens first.
+ * @throws SecurityException if the caller doesn't meet the requirements
+ * outlined above.
*/
@SystemApi
public void setSubscriptionOverrideUnmetered(int subId, boolean overrideUnmetered,
@@ -1822,6 +1832,8 @@ public class SubscriptionManager {
* be automatically cleared, or {@code 0} to leave in the
* requested state until explicitly cleared, or the next reboot,
* whichever happens first.
+ * @throws SecurityException if the caller doesn't meet the requirements
+ * outlined above.
*/
@SystemApi
public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested,
diff --git a/android/telephony/SubscriptionPlan.java b/android/telephony/SubscriptionPlan.java
index 4ffb70ba..e8bbe42e 100644
--- a/android/telephony/SubscriptionPlan.java
+++ b/android/telephony/SubscriptionPlan.java
@@ -24,7 +24,7 @@ import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.Pair;
+import android.util.Range;
import android.util.RecurrenceRule;
import com.android.internal.util.Preconditions;
@@ -209,7 +209,7 @@ public final class SubscriptionPlan implements Parcelable {
* any recurrence rules. The iterator starts from the currently active cycle
* and walks backwards through time.
*/
- public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+ public Iterator<Range<ZonedDateTime>> cycleIterator() {
return cycleRule.cycleIterator();
}
@@ -227,6 +227,9 @@ public final class SubscriptionPlan implements Parcelable {
/**
* Start defining a {@link SubscriptionPlan} that covers a very specific
* window of time, and never automatically recurs.
+ *
+ * @param start The exact time at which the plan starts.
+ * @param end The exact time at which the plan ends.
*/
public static Builder createNonrecurring(ZonedDateTime start, ZonedDateTime end) {
if (!end.isAfter(start)) {
@@ -237,28 +240,43 @@ public final class SubscriptionPlan implements Parcelable {
}
/**
- * Start defining a {@link SubscriptionPlan} that will recur
- * automatically every month. It will always recur on the same day of a
- * particular month. When a particular month ends before the defined
- * recurrence day, the plan will recur on the last instant of that
- * month.
+ * Start defining a {@link SubscriptionPlan} that starts at a specific
+ * time, and automatically recurs after each specific period of time,
+ * repeating indefinitely.
+ * <p>
+ * When the given period is set to exactly one month, the plan will
+ * always recur on the day of the month defined by
+ * {@link ZonedDateTime#getDayOfMonth()}. When a particular month ends
+ * before this day, the plan will recur on the last possible instant of
+ * that month.
+ *
+ * @param start The exact time at which the plan starts.
+ * @param period The period after which the plan automatically recurs.
*/
+ public static Builder createRecurring(ZonedDateTime start, Period period) {
+ if (period.isZero() || period.isNegative()) {
+ throw new IllegalArgumentException("Period " + period + " must be positive");
+ }
+ return new Builder(start, null, period);
+ }
+
+ /** {@hide} */
+ @SystemApi
+ @Deprecated
public static Builder createRecurringMonthly(ZonedDateTime start) {
return new Builder(start, null, Period.ofMonths(1));
}
- /**
- * Start defining a {@link SubscriptionPlan} that will recur
- * automatically every week.
- */
+ /** {@hide} */
+ @SystemApi
+ @Deprecated
public static Builder createRecurringWeekly(ZonedDateTime start) {
return new Builder(start, null, Period.ofDays(7));
}
- /**
- * Start defining a {@link SubscriptionPlan} that will recur
- * automatically every day.
- */
+ /** {@hide} */
+ @SystemApi
+ @Deprecated
public static Builder createRecurringDaily(ZonedDateTime start) {
return new Builder(start, null, Period.ofDays(1));
}
diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java
index e15d35b5..956a5b14 100644
--- a/android/telephony/TelephonyManager.java
+++ b/android/telephony/TelephonyManager.java
@@ -2968,7 +2968,7 @@ public class TelephonyManager {
IPhoneSubInfo info = getSubscriberInfo();
if (info == null)
return null;
- return info.getGroupIdLevel1(mContext.getOpPackageName());
+ return info.getGroupIdLevel1ForSubscriber(getSubId(), mContext.getOpPackageName());
} catch (RemoteException ex) {
return null;
} catch (NullPointerException ex) {
@@ -5156,7 +5156,12 @@ public class TelephonyManager {
* {@link #AUTHTYPE_EAP_SIM}
* @param data authentication challenge data, base64 encoded.
* See 3GPP TS 31.102 7.1.2 for more details.
- * @return the response of authentication, or null if not available
+ * @return the response of authentication. This value will be null in the following cases:
+ * Authentication error, incorrect MAC
+ * Authentication error, security context not supported
+ * Key freshness failure
+ * Authentication error, no memory space available
+ * Authentication error, no memory space available in EFMUK
*/
// TODO(b/73660190): This should probably require MODIFY_PHONE_STATE, not
// READ_PRIVILEGED_PHONE_STATE. It certainly shouldn't reference the permission in Javadoc since
@@ -5177,7 +5182,13 @@ public class TelephonyManager {
* {@link #AUTHTYPE_EAP_SIM}
* @param data authentication challenge data, base64 encoded.
* See 3GPP TS 31.102 7.1.2 for more details.
- * @return the response of authentication, or null if not available
+ * @return the response of authentication. This value will be null in the following cases only
+ * (see 3GPP TS 31.102 7.3.1):
+ * Authentication error, incorrect MAC
+ * Authentication error, security context not supported
+ * Key freshness failure
+ * Authentication error, no memory space available
+ * Authentication error, no memory space available in EFMUK
* @hide
*/
public String getIccAuthentication(int subId, int appType, int authType, String data) {
diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java
index 25f51333..acc24704 100644
--- a/android/telephony/data/DataCallResponse.java
+++ b/android/telephony/data/DataCallResponse.java
@@ -19,7 +19,6 @@ package android.telephony.data;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SystemApi;
import android.net.LinkAddress;
import android.os.Parcel;
import android.os.Parcelable;
@@ -34,7 +33,6 @@ import java.util.Objects;
*
* @hide
*/
-@SystemApi
public final class DataCallResponse implements Parcelable {
private final int mStatus;
private final int mSuggestedRetryTime;
diff --git a/android/telephony/data/DataProfile.java b/android/telephony/data/DataProfile.java
index e8597b22..dd274c56 100644
--- a/android/telephony/data/DataProfile.java
+++ b/android/telephony/data/DataProfile.java
@@ -16,7 +16,6 @@
package android.telephony.data;
-import android.annotation.SystemApi;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -30,7 +29,6 @@ import com.android.internal.telephony.RILConstants;
*
* @hide
*/
-@SystemApi
public final class DataProfile implements Parcelable {
// The types indicating the data profile is used on GSM (3GPP) or CDMA (3GPP2) network.
diff --git a/android/telephony/data/DataService.java b/android/telephony/data/DataService.java
index e8c1cb11..0835f7d9 100644
--- a/android/telephony/data/DataService.java
+++ b/android/telephony/data/DataService.java
@@ -20,7 +20,6 @@ import android.annotation.CallSuper;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.net.LinkProperties;
@@ -55,7 +54,6 @@ import java.util.List;
* </service>
* @hide
*/
-@SystemApi
public abstract class DataService extends Service {
private static final String TAG = DataService.class.getSimpleName();
@@ -429,8 +427,10 @@ public abstract class DataService extends Service {
}
}
- /** @hide */
- protected DataService() {
+ /**
+ * Default constructor.
+ */
+ public DataService() {
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
diff --git a/android/telephony/data/DataServiceCallback.java b/android/telephony/data/DataServiceCallback.java
index 4af31b5e..bff82608 100644
--- a/android/telephony/data/DataServiceCallback.java
+++ b/android/telephony/data/DataServiceCallback.java
@@ -17,7 +17,6 @@
package android.telephony.data;
import android.annotation.IntDef;
-import android.annotation.SystemApi;
import android.net.LinkProperties;
import android.os.RemoteException;
import android.telephony.Rlog;
@@ -35,7 +34,6 @@ import java.util.List;
*
* @hide
*/
-@SystemApi
public class DataServiceCallback {
private static final String TAG = DataServiceCallback.class.getSimpleName();
@@ -125,7 +123,6 @@ public class DataServiceCallback {
*
* @param result The result code. Must be one of the {@link ResultCode}.
*/
- @SystemApi
public void onSetDataProfileComplete(@ResultCode int result) {
IDataServiceCallback callback = mCallback.get();
if (callback != null) {
diff --git a/android/telephony/ims/stub/ImsFeatureConfiguration.java b/android/telephony/ims/stub/ImsFeatureConfiguration.java
index 2f52c0ac..dfb6e2ce 100644
--- a/android/telephony/ims/stub/ImsFeatureConfiguration.java
+++ b/android/telephony/ims/stub/ImsFeatureConfiguration.java
@@ -77,6 +77,11 @@ public final class ImsFeatureConfiguration implements Parcelable {
result = 31 * result + featureType;
return result;
}
+
+ @Override
+ public String toString() {
+ return "{s=" + slotId + ", f=" + featureType + "}";
+ }
}
/**
diff --git a/android/telephony/mbms/DownloadRequest.java b/android/telephony/mbms/DownloadRequest.java
index 602c796a..9e3302bd 100644
--- a/android/telephony/mbms/DownloadRequest.java
+++ b/android/telephony/mbms/DownloadRequest.java
@@ -18,6 +18,7 @@ package android.telephony.mbms;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
@@ -184,6 +185,7 @@ public final class DownloadRequest implements Parcelable {
* @hide
*/
@SystemApi
+ @TestApi
public Builder setServiceId(String serviceId) {
fileServiceId = serviceId;
return this;
diff --git a/android/text/MeasuredParagraph.java b/android/text/MeasuredParagraph.java
index 96edfa31..c2c3182c 100644
--- a/android/text/MeasuredParagraph.java
+++ b/android/text/MeasuredParagraph.java
@@ -303,10 +303,9 @@ public class MeasuredParagraph {
*
* This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
*/
- public void getBounds(@NonNull Paint paint, @IntRange(from = 0) int start,
- @IntRange(from = 0) int end, @NonNull Rect bounds) {
- nGetBounds(mNativePtr, mCopiedBuffer, paint.getNativeInstance(), start, end,
- paint.getBidiFlags(), bounds);
+ public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
+ @NonNull Rect bounds) {
+ nGetBounds(mNativePtr, mCopiedBuffer, start, end, bounds);
}
/**
@@ -743,6 +742,6 @@ public class MeasuredParagraph {
@CriticalNative
private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);
- private static native void nGetBounds(long nativePtr, char[] buf, long paintPtr, int start,
- int end, int bidiFlag, Rect rect);
+ private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
+ Rect rect);
}
diff --git a/android/text/PrecomputedText.java b/android/text/PrecomputedText.java
index 413df05d..369f357d 100644
--- a/android/text/PrecomputedText.java
+++ b/android/text/PrecomputedText.java
@@ -16,6 +16,7 @@
package android.text;
+import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -44,13 +45,17 @@ import java.util.Objects;
* <pre>
* An example usage is:
* <code>
- * void asyncSetText(final TextView textView, final String longString, Handler bgThreadHandler) {
+ * static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) {
* // construct precompute related parameters using the TextView that we will set the text on.
- * final PrecomputedText.Params params = textView.getTextParams();
- * bgThreadHandler.post(() -> {
- * final PrecomputedText precomputedText =
- * PrecomputedText.create(expensiveLongString, params);
+ * final PrecomputedText.Params params = textView.getTextMetricsParams();
+ * final Reference textViewRef = new WeakReference<>(textView);
+ * bgExecutor.submit(() -> {
+ * TextView textView = textViewRef.get();
+ * if (textView == null) return;
+ * final PrecomputedText precomputedText = PrecomputedText.create(longString, params);
* textView.post(() -> {
+ * TextView textView = textViewRef.get();
+ * if (textView == null) return;
* textView.setText(precomputedText);
* });
* });
@@ -363,6 +368,7 @@ public class PrecomputedText implements Spannable {
/**
* Return the underlying text.
+ * @hide
*/
public @NonNull CharSequence getText() {
return mText;
@@ -451,32 +457,65 @@ public class PrecomputedText implements Spannable {
+ ", gave " + pos);
}
- /** @hide */
- public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
+ /**
+ * Returns text width for the given range.
+ * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
+ * IllegalArgumentException will be thrown.
+ *
+ * @param start the inclusive start offset in the text
+ * @param end the exclusive end offset in the text
+ * @return the text width
+ * @throws IllegalArgumentException if start and end offset are in the different paragraph.
+ */
+ public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start,
+ @IntRange(from = 0) int end) {
+ Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
+ Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
+ Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
+
+ if (start == end) {
+ return 0;
+ }
final int paraIndex = findParaIndex(start);
final int paraStart = getParagraphStart(paraIndex);
final int paraEnd = getParagraphEnd(paraIndex);
if (start < paraStart || paraEnd < end) {
- throw new RuntimeException("Cannot measured across the paragraph:"
+ throw new IllegalArgumentException("Cannot measured across the paragraph:"
+ "para: (" + paraStart + ", " + paraEnd + "), "
+ "request: (" + start + ", " + end + ")");
}
return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
}
- /** @hide */
+ /**
+ * Retrieves the text bounding box for the given range.
+ * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
+ * IllegalArgumentException will be thrown.
+ *
+ * @param start the inclusive start offset in the text
+ * @param end the exclusive end offset in the text
+ * @param bounds the output rectangle
+ * @throws IllegalArgumentException if start and end offset are in the different paragraph.
+ */
public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
@NonNull Rect bounds) {
+ Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
+ Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
+ Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
+ Preconditions.checkNotNull(bounds);
+ if (start == end) {
+ bounds.set(0, 0, 0, 0);
+ return;
+ }
final int paraIndex = findParaIndex(start);
final int paraStart = getParagraphStart(paraIndex);
final int paraEnd = getParagraphEnd(paraIndex);
if (start < paraStart || paraEnd < end) {
- throw new RuntimeException("Cannot measured across the paragraph:"
+ throw new IllegalArgumentException("Cannot measured across the paragraph:"
+ "para: (" + paraStart + ", " + paraEnd + "), "
+ "request: (" + start + ", " + end + ")");
}
- getMeasuredParagraph(paraIndex).getBounds(mParams.mPaint,
- start - paraStart, end - paraStart, bounds);
+ getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
}
/**
diff --git a/android/text/Selection.java b/android/text/Selection.java
index 34456580..5256e471 100644
--- a/android/text/Selection.java
+++ b/android/text/Selection.java
@@ -180,7 +180,7 @@ public class Selection {
* Remove the selection or cursor, if any, from the text.
*/
public static final void removeSelection(Spannable text) {
- text.removeSpan(SELECTION_START);
+ text.removeSpan(SELECTION_START, Spanned.SPAN_INTERMEDIATE);
text.removeSpan(SELECTION_END);
removeMemory(text);
}
diff --git a/android/text/Spannable.java b/android/text/Spannable.java
index 39b78eb0..8315b2aa 100644
--- a/android/text/Spannable.java
+++ b/android/text/Spannable.java
@@ -46,6 +46,19 @@ extends Spanned
public void removeSpan(Object what);
/**
+ * Remove the specified object from the range of text to which it
+ * was attached, if any. It is OK to remove an object that was never
+ * attached in the first place.
+ *
+ * See {@link Spanned} for an explanation of what the flags mean.
+ *
+ * @hide
+ */
+ default void removeSpan(Object what, int flags) {
+ removeSpan(what);
+ }
+
+ /**
* Factory used by TextView to create new {@link Spannable Spannables}. You can subclass
* it to provide something other than {@link SpannableString}.
*
diff --git a/android/text/SpannableStringBuilder.java b/android/text/SpannableStringBuilder.java
index d41dfdcd..41a9c45a 100644
--- a/android/text/SpannableStringBuilder.java
+++ b/android/text/SpannableStringBuilder.java
@@ -312,7 +312,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
// The following condition indicates that the span would become empty
(textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) {
mIndexOfSpan.remove(mSpans[i]);
- removeSpan(i);
+ removeSpan(i, 0 /* flags */);
return true;
}
return resolveGap(mSpanStarts[i]) <= end && (i & 1) != 0 &&
@@ -472,7 +472,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
}
// Note: caller is responsible for removing the mIndexOfSpan entry.
- private void removeSpan(int i) {
+ private void removeSpan(int i, int flags) {
Object object = mSpans[i];
int start = mSpanStarts[i];
@@ -496,7 +496,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
// Invariants must be restored before sending span removed notifications.
restoreInvariants();
- sendSpanRemoved(object, start, end);
+ if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) {
+ sendSpanRemoved(object, start, end);
+ }
}
// Documentation from interface
@@ -782,10 +784,19 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
* Remove the specified markup object from the buffer.
*/
public void removeSpan(Object what) {
+ removeSpan(what, 0 /* flags */);
+ }
+
+ /**
+ * Remove the specified markup object from the buffer.
+ *
+ * @hide
+ */
+ public void removeSpan(Object what, int flags) {
if (mIndexOfSpan == null) return;
Integer i = mIndexOfSpan.remove(what);
if (i != null) {
- removeSpan(i.intValue());
+ removeSpan(i.intValue(), flags);
}
}
diff --git a/android/text/SpannableStringInternal.java b/android/text/SpannableStringInternal.java
index 5dd1a52b..bcc2fda8 100644
--- a/android/text/SpannableStringInternal.java
+++ b/android/text/SpannableStringInternal.java
@@ -249,6 +249,13 @@ import java.lang.reflect.Array;
}
/* package */ void removeSpan(Object what) {
+ removeSpan(what, 0 /* flags */);
+ }
+
+ /**
+ * @hide
+ */
+ public void removeSpan(Object what, int flags) {
int count = mSpanCount;
Object[] spans = mSpans;
int[] data = mSpanData;
@@ -262,11 +269,13 @@ import java.lang.reflect.Array;
System.arraycopy(spans, i + 1, spans, i, c);
System.arraycopy(data, (i + 1) * COLUMNS,
- data, i * COLUMNS, c * COLUMNS);
+ data, i * COLUMNS, c * COLUMNS);
mSpanCount--;
- sendSpanRemoved(what, ostart, oend);
+ if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) {
+ sendSpanRemoved(what, ostart, oend);
+ }
return;
}
}
diff --git a/android/text/format/Formatter.java b/android/text/format/Formatter.java
index ad3b4b6d..de86a66a 100644
--- a/android/text/format/Formatter.java
+++ b/android/text/format/Formatter.java
@@ -40,6 +40,10 @@ public final class Formatter {
public static final int FLAG_SHORTER = 1 << 0;
/** {@hide} */
public static final int FLAG_CALCULATE_ROUNDED = 1 << 1;
+ /** {@hide} */
+ public static final int FLAG_SI_UNITS = 1 << 2;
+ /** {@hide} */
+ public static final int FLAG_IEC_UNITS = 1 << 3;
/** {@hide} */
public static class BytesResult {
@@ -90,7 +94,7 @@ public final class Formatter {
if (context == null) {
return "";
}
- final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
+ final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SI_UNITS);
return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
res.value, res.units));
}
@@ -103,41 +107,43 @@ public final class Formatter {
if (context == null) {
return "";
}
- final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
+ final BytesResult res = formatBytes(context.getResources(), sizeBytes,
+ FLAG_SI_UNITS | FLAG_SHORTER);
return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
res.value, res.units));
}
/** {@hide} */
public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
+ final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000;
final boolean isNegative = (sizeBytes < 0);
float result = isNegative ? -sizeBytes : sizeBytes;
int suffix = com.android.internal.R.string.byteShort;
long mult = 1;
if (result > 900) {
suffix = com.android.internal.R.string.kilobyteShort;
- mult = 1000;
- result = result / 1000;
+ mult = unit;
+ result = result / unit;
}
if (result > 900) {
suffix = com.android.internal.R.string.megabyteShort;
- mult *= 1000;
- result = result / 1000;
+ mult *= unit;
+ result = result / unit;
}
if (result > 900) {
suffix = com.android.internal.R.string.gigabyteShort;
- mult *= 1000;
- result = result / 1000;
+ mult *= unit;
+ result = result / unit;
}
if (result > 900) {
suffix = com.android.internal.R.string.terabyteShort;
- mult *= 1000;
- result = result / 1000;
+ mult *= unit;
+ result = result / unit;
}
if (result > 900) {
suffix = com.android.internal.R.string.petabyteShort;
- mult *= 1000;
- result = result / 1000;
+ mult *= unit;
+ result = result / unit;
}
// Note we calculate the rounded long by ourselves, but still let String.format()
// compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
diff --git a/android/text/method/LinkMovementMethod.java b/android/text/method/LinkMovementMethod.java
index f3323588..e60377b4 100644
--- a/android/text/method/LinkMovementMethod.java
+++ b/android/text/method/LinkMovementMethod.java
@@ -219,7 +219,7 @@ public class LinkMovementMethod extends ScrollingMovementMethod {
links[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
if (widget.getContext().getApplicationInfo().targetSdkVersion
- > Build.VERSION_CODES.O_MR1) {
+ >= Build.VERSION_CODES.P) {
// Selection change will reposition the toolbar. Hide it for a few ms for a
// smoother transition.
widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
diff --git a/android/text/util/Linkify.java b/android/text/util/Linkify.java
index 9c6a3f5f..c905f495 100644
--- a/android/text/util/Linkify.java
+++ b/android/text/util/Linkify.java
@@ -100,13 +100,20 @@ public class Linkify {
* take an options mask. Note that this uses the
* {@link android.webkit.WebView#findAddress(String) findAddress()} method in
* {@link android.webkit.WebView} for finding addresses, which has various
- * limitations.
+ * limitations and has been deprecated.
+ * @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
+ * TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative
+ * is available.
*/
+ @Deprecated
public static final int MAP_ADDRESSES = 0x08;
/**
* Bit mask indicating that all available patterns should be matched in
* methods that take an options mask
+ * <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated.
+ * Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)}
+ * instead and avoid it even when targeting API levels where no alternative is available.
*/
public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
diff --git a/android/util/LruCache.java b/android/util/LruCache.java
index 52086065..40154880 100644
--- a/android/util/LruCache.java
+++ b/android/util/LruCache.java
@@ -20,10 +20,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
/**
- * BEGIN LAYOUTLIB CHANGE
- * This is a custom version that doesn't use the non standard LinkedHashMap#eldest.
- * END LAYOUTLIB CHANGE
- *
* A cache that holds strong references to a limited number of values. Each time
* a value is accessed, it is moved to the head of a queue. When a value is
* added to a full cache, the value at the end of that queue is evicted and may
@@ -91,9 +87,8 @@ public class LruCache<K, V> {
/**
* Sets the size of the cache.
- * @param maxSize The new maximum size.
*
- * @hide
+ * @param maxSize The new maximum size.
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
@@ -190,10 +185,13 @@ public class LruCache<K, V> {
}
/**
+ * Remove the eldest entries until the total of remaining entries is at or
+ * below the requested size.
+ *
* @param maxSize the maximum size of the cache before returning. May be -1
- * to evict even 0-sized elements.
+ * to evict even 0-sized elements.
*/
- private void trimToSize(int maxSize) {
+ public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
@@ -207,16 +205,7 @@ public class LruCache<K, V> {
break;
}
- // BEGIN LAYOUTLIB CHANGE
- // get the last item in the linked list.
- // This is not efficient, the goal here is to minimize the changes
- // compared to the platform version.
- Map.Entry<K, V> toEvict = null;
- for (Map.Entry<K, V> entry : map.entrySet()) {
- toEvict = entry;
- }
- // END LAYOUTLIB CHANGE
-
+ Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
diff --git a/android/util/RecurrenceRule.java b/android/util/RecurrenceRule.java
index 9f115eba..9c898766 100644
--- a/android/util/RecurrenceRule.java
+++ b/android/util/RecurrenceRule.java
@@ -149,6 +149,10 @@ public class RecurrenceRule implements Parcelable {
}
};
+ public boolean isRecurring() {
+ return period != null;
+ }
+
@Deprecated
public boolean isMonthly() {
return start != null
@@ -158,7 +162,7 @@ public class RecurrenceRule implements Parcelable {
&& period.getDays() == 0;
}
- public Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator() {
+ public Iterator<Range<ZonedDateTime>> cycleIterator() {
if (period != null) {
return new RecurringIterator();
} else {
@@ -166,7 +170,7 @@ public class RecurrenceRule implements Parcelable {
}
}
- private class NonrecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+ private class NonrecurringIterator implements Iterator<Range<ZonedDateTime>> {
boolean hasNext;
public NonrecurringIterator() {
@@ -179,13 +183,13 @@ public class RecurrenceRule implements Parcelable {
}
@Override
- public Pair<ZonedDateTime, ZonedDateTime> next() {
+ public Range<ZonedDateTime> next() {
hasNext = false;
- return new Pair<>(start, end);
+ return new Range<>(start, end);
}
}
- private class RecurringIterator implements Iterator<Pair<ZonedDateTime, ZonedDateTime>> {
+ private class RecurringIterator implements Iterator<Range<ZonedDateTime>> {
int i;
ZonedDateTime cycleStart;
ZonedDateTime cycleEnd;
@@ -231,12 +235,12 @@ public class RecurrenceRule implements Parcelable {
}
@Override
- public Pair<ZonedDateTime, ZonedDateTime> next() {
+ public Range<ZonedDateTime> next() {
if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd);
- Pair<ZonedDateTime, ZonedDateTime> p = new Pair<>(cycleStart, cycleEnd);
+ Range<ZonedDateTime> r = new Range<>(cycleStart, cycleEnd);
i--;
updateCycle();
- return p;
+ return r;
}
}
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java
index 66a9c6c0..f59c0b50 100644
--- a/android/view/DisplayCutout.java
+++ b/android/view/DisplayCutout.java
@@ -31,6 +31,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.util.PathParser;
import android.util.proto.ProtoOutputStream;
@@ -75,15 +76,19 @@ public final class DisplayCutout {
false /* copyArguments */);
+ private static final Pair<Path, DisplayCutout> NULL_PAIR = new Pair<>(null, null);
private static final Object CACHE_LOCK = new Object();
+
@GuardedBy("CACHE_LOCK")
private static String sCachedSpec;
@GuardedBy("CACHE_LOCK")
private static int sCachedDisplayWidth;
@GuardedBy("CACHE_LOCK")
+ private static int sCachedDisplayHeight;
+ @GuardedBy("CACHE_LOCK")
private static float sCachedDensity;
@GuardedBy("CACHE_LOCK")
- private static DisplayCutout sCachedCutout;
+ private static Pair<Path, DisplayCutout> sCachedCutout = NULL_PAIR;
private final Rect mSafeInsets;
private final Region mBounds;
@@ -347,7 +352,7 @@ public final class DisplayCutout {
}
/**
- * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+ * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout.
*
* @hide
*/
@@ -357,6 +362,16 @@ public final class DisplayCutout {
}
/**
+ * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+ *
+ * @hide
+ */
+ public static Path pathFromResources(Resources res, int displayWidth, int displayHeight) {
+ return pathAndDisplayCutoutFromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
+ displayWidth, displayHeight, res.getDisplayMetrics().density).first;
+ }
+
+ /**
* Creates an instance according to the supplied {@link android.util.PathParser.PathData} spec.
*
* @hide
@@ -364,11 +379,17 @@ public final class DisplayCutout {
@VisibleForTesting(visibility = PRIVATE)
public static DisplayCutout fromSpec(String spec, int displayWidth, int displayHeight,
float density) {
+ return pathAndDisplayCutoutFromSpec(spec, displayWidth, displayHeight, density).second;
+ }
+
+ private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec,
+ int displayWidth, int displayHeight, float density) {
if (TextUtils.isEmpty(spec)) {
- return null;
+ return NULL_PAIR;
}
synchronized (CACHE_LOCK) {
if (spec.equals(sCachedSpec) && sCachedDisplayWidth == displayWidth
+ && sCachedDisplayHeight == displayHeight
&& sCachedDensity == density) {
return sCachedCutout;
}
@@ -398,7 +419,7 @@ public final class DisplayCutout {
p = PathParser.createPathFromPathData(spec);
} catch (Throwable e) {
Log.wtf(TAG, "Could not inflate cutout: ", e);
- return null;
+ return NULL_PAIR;
}
final Matrix m = new Matrix();
@@ -414,7 +435,7 @@ public final class DisplayCutout {
bottomPath = PathParser.createPathFromPathData(bottomSpec);
} catch (Throwable e) {
Log.wtf(TAG, "Could not inflate bottom cutout: ", e);
- return null;
+ return NULL_PAIR;
}
// Keep top transform
m.postTranslate(0, displayHeight);
@@ -422,10 +443,11 @@ public final class DisplayCutout {
p.addPath(bottomPath);
}
- final DisplayCutout result = fromBounds(p);
+ final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p));
synchronized (CACHE_LOCK) {
sCachedSpec = spec;
sCachedDisplayWidth = displayWidth;
+ sCachedDisplayHeight = displayHeight;
sCachedDensity = density;
sCachedCutout = result;
}
diff --git a/android/view/HapticFeedbackConstants.java b/android/view/HapticFeedbackConstants.java
index b1479284..db01cea3 100644
--- a/android/view/HapticFeedbackConstants.java
+++ b/android/view/HapticFeedbackConstants.java
@@ -77,6 +77,55 @@ public class HapticFeedbackConstants {
public static final int TEXT_HANDLE_MOVE = 9;
/**
+ * The user unlocked the device
+ * @hide
+ */
+ public static final int ENTRY_BUMP = 10;
+
+ /**
+ * The user has moved the dragged object within a droppable area.
+ * @hide
+ */
+ public static final int DRAG_CROSSING = 11;
+
+ /**
+ * The user has started a gesture (e.g. on the soft keyboard).
+ * @hide
+ */
+ public static final int GESTURE_START = 12;
+
+ /**
+ * The user has finished a gesture (e.g. on the soft keyboard).
+ * @hide
+ */
+ public static final int GESTURE_END = 13;
+
+ /**
+ * The user's squeeze crossed the gesture's initiation threshold.
+ * @hide
+ */
+ public static final int EDGE_SQUEEZE = 14;
+
+ /**
+ * The user's squeeze crossed the gesture's release threshold.
+ * @hide
+ */
+ public static final int EDGE_RELEASE = 15;
+
+ /**
+ * A haptic effect to signal the confirmation or successful completion of a user
+ * interaction.
+ * @hide
+ */
+ public static final int CONFIRM = 16;
+
+ /**
+ * A haptic effect to signal the rejection or failure of a user interaction.
+ * @hide
+ */
+ public static final int REJECT = 17;
+
+ /**
* The phone has booted with safe mode enabled.
* This is a private constant. Feel free to renumber as desired.
* @hide
diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java
index d4610a56..5deee11b 100644
--- a/android/view/SurfaceControl.java
+++ b/android/view/SurfaceControl.java
@@ -87,6 +87,7 @@ public class SurfaceControl implements Parcelable {
private static native void nativeMergeTransaction(long transactionObj,
long otherTransactionObj);
private static native void nativeSetAnimationTransaction(long transactionObj);
+ private static native void nativeSetEarlyWakeup(long transactionObj);
private static native void nativeSetLayer(long transactionObj, long nativeObject, int zorder);
private static native void nativeSetRelativeLayer(long transactionObj, long nativeObject,
@@ -1642,6 +1643,19 @@ public class SurfaceControl implements Parcelable {
}
/**
+ * Indicate that SurfaceFlinger should wake up earlier than usual as a result of this
+ * transaction. This should be used when the caller thinks that the scene is complex enough
+ * that it's likely to hit GL composition, and thus, SurfaceFlinger needs to more time in
+ * order not to miss frame deadlines.
+ * <p>
+ * Corresponds to setting ISurfaceComposer::eEarlyWakeup
+ */
+ public Transaction setEarlyWakeup() {
+ nativeSetEarlyWakeup(mNativeObject);
+ return this;
+ }
+
+ /**
* Merge the other transaction into this transaction, clearing the
* other transaction as if it had been applied.
*/
diff --git a/android/view/SurfaceView.java b/android/view/SurfaceView.java
index ebb2af45..7e546476 100644
--- a/android/view/SurfaceView.java
+++ b/android/view/SurfaceView.java
@@ -16,115 +16,1237 @@
package android.view;
-import com.android.layoutlib.bridge.MockView;
+import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
+import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
+import static android.view.WindowManagerPolicyConstants.APPLICATION_PANEL_SUBLAYER;
import android.content.Context;
+import android.content.res.CompatibilityInfo.Translator;
+import android.content.res.Configuration;
import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.SystemClock;
import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.view.SurfaceCallbackHelper;
+
+import java.util.ArrayList;
+import java.util.concurrent.locks.ReentrantLock;
/**
- * Mock version of the SurfaceView.
- * Only non override public methods from the real SurfaceView have been added in there.
- * Methods that take an unknown class as parameter or as return object, have been removed for now.
+ * Provides a dedicated drawing surface embedded inside of a view hierarchy.
+ * You can control the format of this surface and, if you like, its size; the
+ * SurfaceView takes care of placing the surface at the correct location on the
+ * screen
+ *
+ * <p>The surface is Z ordered so that it is behind the window holding its
+ * SurfaceView; the SurfaceView punches a hole in its window to allow its
+ * surface to be displayed. The view hierarchy will take care of correctly
+ * compositing with the Surface any siblings of the SurfaceView that would
+ * normally appear on top of it. This can be used to place overlays such as
+ * buttons on top of the Surface, though note however that it can have an
+ * impact on performance since a full alpha-blended composite will be performed
+ * each time the Surface changes.
+ *
+ * <p> The transparent region that makes the surface visible is based on the
+ * layout positions in the view hierarchy. If the post-layout transform
+ * properties are used to draw a sibling view on top of the SurfaceView, the
+ * view may not be properly composited with the surface.
*
- * TODO: generate automatically.
+ * <p>Access to the underlying surface is provided via the SurfaceHolder interface,
+ * which can be retrieved by calling {@link #getHolder}.
*
+ * <p>The Surface will be created for you while the SurfaceView's window is
+ * visible; you should implement {@link SurfaceHolder.Callback#surfaceCreated}
+ * and {@link SurfaceHolder.Callback#surfaceDestroyed} to discover when the
+ * Surface is created and destroyed as the window is shown and hidden.
+ *
+ * <p>One of the purposes of this class is to provide a surface in which a
+ * secondary thread can render into the screen. If you are going to use it
+ * this way, you need to be aware of some threading semantics:
+ *
+ * <ul>
+ * <li> All SurfaceView and
+ * {@link SurfaceHolder.Callback SurfaceHolder.Callback} methods will be called
+ * from the thread running the SurfaceView's window (typically the main thread
+ * of the application). They thus need to correctly synchronize with any
+ * state that is also touched by the drawing thread.
+ * <li> You must ensure that the drawing thread only touches the underlying
+ * Surface while it is valid -- between
+ * {@link SurfaceHolder.Callback#surfaceCreated SurfaceHolder.Callback.surfaceCreated()}
+ * and
+ * {@link SurfaceHolder.Callback#surfaceDestroyed SurfaceHolder.Callback.surfaceDestroyed()}.
+ * </ul>
+ *
+ * <p class="note"><strong>Note:</strong> Starting in platform version
+ * {@link android.os.Build.VERSION_CODES#N}, SurfaceView's window position is
+ * updated synchronously with other View rendering. This means that translating
+ * and scaling a SurfaceView on screen will not cause rendering artifacts. Such
+ * artifacts may occur on previous versions of the platform when its window is
+ * positioned asynchronously.</p>
*/
-public class SurfaceView extends MockView {
+public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback {
+ private static final String TAG = "SurfaceView";
+ private static final boolean DEBUG = false;
+
+ final ArrayList<SurfaceHolder.Callback> mCallbacks
+ = new ArrayList<SurfaceHolder.Callback>();
+
+ final int[] mLocation = new int[2];
+
+ final ReentrantLock mSurfaceLock = new ReentrantLock();
+ final Surface mSurface = new Surface(); // Current surface in use
+ boolean mDrawingStopped = true;
+ // We use this to track if the application has produced a frame
+ // in to the Surface. Up until that point, we should be careful not to punch
+ // holes.
+ boolean mDrawFinished = false;
+
+ final Rect mScreenRect = new Rect();
+ SurfaceSession mSurfaceSession;
+
+ SurfaceControlWithBackground mSurfaceControl;
+ // In the case of format changes we switch out the surface in-place
+ // we need to preserve the old one until the new one has drawn.
+ SurfaceControl mDeferredDestroySurfaceControl;
+ final Rect mTmpRect = new Rect();
+ final Configuration mConfiguration = new Configuration();
+
+ int mSubLayer = APPLICATION_MEDIA_SUBLAYER;
+
+ boolean mIsCreating = false;
+ private volatile boolean mRtHandlingPositionUpdates = false;
+
+ private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener
+ = new ViewTreeObserver.OnScrollChangedListener() {
+ @Override
+ public void onScrollChanged() {
+ updateSurface();
+ }
+ };
+
+ private final ViewTreeObserver.OnPreDrawListener mDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ // reposition ourselves where the surface is
+ mHaveFrame = getWidth() > 0 && getHeight() > 0;
+ updateSurface();
+ return true;
+ }
+ };
+
+ boolean mRequestedVisible = false;
+ boolean mWindowVisibility = false;
+ boolean mLastWindowVisibility = false;
+ boolean mViewVisibility = false;
+ boolean mWindowStopped = false;
+
+ int mRequestedWidth = -1;
+ int mRequestedHeight = -1;
+ /* Set SurfaceView's format to 565 by default to maintain backward
+ * compatibility with applications assuming this format.
+ */
+ int mRequestedFormat = PixelFormat.RGB_565;
+
+ boolean mHaveFrame = false;
+ boolean mSurfaceCreated = false;
+ long mLastLockTime = 0;
+
+ boolean mVisible = false;
+ int mWindowSpaceLeft = -1;
+ int mWindowSpaceTop = -1;
+ int mSurfaceWidth = -1;
+ int mSurfaceHeight = -1;
+ int mFormat = -1;
+ final Rect mSurfaceFrame = new Rect();
+ int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
+ private Translator mTranslator;
+
+ private boolean mGlobalListenersAdded;
+ private boolean mAttachedToWindow;
+
+ private int mSurfaceFlags = SurfaceControl.HIDDEN;
+
+ private int mPendingReportDraws;
+
+ private SurfaceControl.Transaction mRtTransaction = new SurfaceControl.Transaction();
public SurfaceView(Context context) {
this(context, null);
}
public SurfaceView(Context context, AttributeSet attrs) {
- this(context, attrs , 0);
+ this(context, attrs, 0);
}
- public SurfaceView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
+ public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
}
public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
+ mRenderNode.requestPositionUpdates(this);
+
+ setWillNotDraw(true);
+ }
+
+ /**
+ * Return the SurfaceHolder providing access and control over this
+ * SurfaceView's underlying surface.
+ *
+ * @return SurfaceHolder The holder of the surface.
+ */
+ public SurfaceHolder getHolder() {
+ return mSurfaceHolder;
+ }
+
+ private void updateRequestedVisibility() {
+ mRequestedVisible = mViewVisibility && mWindowVisibility && !mWindowStopped;
+ }
+
+ /** @hide */
+ @Override
+ public void windowStopped(boolean stopped) {
+ mWindowStopped = stopped;
+ updateRequestedVisibility();
+ updateSurface();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ getViewRootImpl().addWindowStoppedCallback(this);
+ mWindowStopped = false;
+
+ mViewVisibility = getVisibility() == VISIBLE;
+ updateRequestedVisibility();
+
+ mAttachedToWindow = true;
+ mParent.requestTransparentRegion(SurfaceView.this);
+ if (!mGlobalListenersAdded) {
+ ViewTreeObserver observer = getViewTreeObserver();
+ observer.addOnScrollChangedListener(mScrollChangedListener);
+ observer.addOnPreDrawListener(mDrawListener);
+ mGlobalListenersAdded = true;
+ }
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mWindowVisibility = visibility == VISIBLE;
+ updateRequestedVisibility();
+ updateSurface();
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ mViewVisibility = visibility == VISIBLE;
+ boolean newRequestedVisible = mWindowVisibility && mViewVisibility && !mWindowStopped;
+ if (newRequestedVisible != mRequestedVisible) {
+ // our base class (View) invalidates the layout only when
+ // we go from/to the GONE state. However, SurfaceView needs
+ // to request a re-layout when the visibility changes at all.
+ // This is needed because the transparent region is computed
+ // as part of the layout phase, and it changes (obviously) when
+ // the visibility changes.
+ requestLayout();
+ }
+ mRequestedVisible = newRequestedVisible;
+ updateSurface();
+ }
+
+ private void performDrawFinished() {
+ if (mPendingReportDraws > 0) {
+ mDrawFinished = true;
+ if (mAttachedToWindow) {
+ notifyDrawFinished();
+ invalidate();
+ }
+ } else {
+ Log.e(TAG, System.identityHashCode(this) + "finished drawing"
+ + " but no pending report draw (extra call"
+ + " to draw completion runnable?)");
+ }
+ }
+
+ void notifyDrawFinished() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot != null) {
+ viewRoot.pendingDrawFinished();
+ }
+ mPendingReportDraws--;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ ViewRootImpl viewRoot = getViewRootImpl();
+ // It's possible to create a SurfaceView using the default constructor and never
+ // attach it to a view hierarchy, this is a common use case when dealing with
+ // OpenGL. A developer will probably create a new GLSurfaceView, and let it manage
+ // the lifecycle. Instead of attaching it to a view, he/she can just pass
+ // the SurfaceHolder forward, most live wallpapers do it.
+ if (viewRoot != null) {
+ viewRoot.removeWindowStoppedCallback(this);
+ }
+
+ mAttachedToWindow = false;
+ if (mGlobalListenersAdded) {
+ ViewTreeObserver observer = getViewTreeObserver();
+ observer.removeOnScrollChangedListener(mScrollChangedListener);
+ observer.removeOnPreDrawListener(mDrawListener);
+ mGlobalListenersAdded = false;
+ }
+
+ while (mPendingReportDraws > 0) {
+ notifyDrawFinished();
+ }
+
+ mRequestedVisible = false;
+
+ updateSurface();
+ if (mSurfaceControl != null) {
+ mSurfaceControl.destroy();
+ }
+ mSurfaceControl = null;
+
+ mHaveFrame = false;
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = mRequestedWidth >= 0
+ ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0)
+ : getDefaultSize(0, widthMeasureSpec);
+ int height = mRequestedHeight >= 0
+ ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0)
+ : getDefaultSize(0, heightMeasureSpec);
+ setMeasuredDimension(width, height);
}
+ /** @hide */
+ @Override
+ protected boolean setFrame(int left, int top, int right, int bottom) {
+ boolean result = super.setFrame(left, top, right, bottom);
+ updateSurface();
+ return result;
+ }
+
+ @Override
public boolean gatherTransparentRegion(Region region) {
- return false;
+ if (isAboveParent() || !mDrawFinished) {
+ return super.gatherTransparentRegion(region);
+ }
+
+ boolean opaque = true;
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
+ // this view draws, remove it from the transparent region
+ opaque = super.gatherTransparentRegion(region);
+ } else if (region != null) {
+ int w = getWidth();
+ int h = getHeight();
+ if (w>0 && h>0) {
+ getLocationInWindow(mLocation);
+ // otherwise, punch a hole in the whole hierarchy
+ int l = mLocation[0];
+ int t = mLocation[1];
+ region.op(l, t, l+w, t+h, Region.Op.UNION);
+ }
+ }
+ if (PixelFormat.formatHasAlpha(mRequestedFormat)) {
+ opaque = false;
+ }
+ return opaque;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mDrawFinished && !isAboveParent()) {
+ // draw() is not called when SKIP_DRAW is set
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
+ // punch a whole in the view-hierarchy below us
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+ }
+ super.draw(canvas);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (mDrawFinished && !isAboveParent()) {
+ // draw() is not called when SKIP_DRAW is set
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ // punch a whole in the view-hierarchy below us
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ }
+ }
+ super.dispatchDraw(canvas);
}
+ /**
+ * Control whether the surface view's surface is placed on top of another
+ * regular surface view in the window (but still behind the window itself).
+ * This is typically used to place overlays on top of an underlying media
+ * surface view.
+ *
+ * <p>Note that this must be set before the surface view's containing
+ * window is attached to the window manager.
+ *
+ * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
+ */
public void setZOrderMediaOverlay(boolean isMediaOverlay) {
+ mSubLayer = isMediaOverlay
+ ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER;
}
+ /**
+ * Control whether the surface view's surface is placed on top of its
+ * window. Normally it is placed behind the window, to allow it to
+ * (for the most part) appear to composite with the views in the
+ * hierarchy. By setting this, you cause it to be placed above the
+ * window. This means that none of the contents of the window this
+ * SurfaceView is in will be visible on top of its surface.
+ *
+ * <p>Note that this must be set before the surface view's containing
+ * window is attached to the window manager.
+ *
+ * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
+ */
public void setZOrderOnTop(boolean onTop) {
+ if (onTop) {
+ mSubLayer = APPLICATION_PANEL_SUBLAYER;
+ } else {
+ mSubLayer = APPLICATION_MEDIA_SUBLAYER;
+ }
}
+ /**
+ * Control whether the surface view's content should be treated as secure,
+ * preventing it from appearing in screenshots or from being viewed on
+ * non-secure displays.
+ *
+ * <p>Note that this must be set before the surface view's containing
+ * window is attached to the window manager.
+ *
+ * <p>See {@link android.view.Display#FLAG_SECURE} for details.
+ *
+ * @param isSecure True if the surface view is secure.
+ */
public void setSecure(boolean isSecure) {
+ if (isSecure) {
+ mSurfaceFlags |= SurfaceControl.SECURE;
+ } else {
+ mSurfaceFlags &= ~SurfaceControl.SECURE;
+ }
}
- public SurfaceHolder getHolder() {
- return mSurfaceHolder;
+ private void updateOpaqueFlag() {
+ if (!PixelFormat.formatHasAlpha(mRequestedFormat)) {
+ mSurfaceFlags |= SurfaceControl.OPAQUE;
+ } else {
+ mSurfaceFlags &= ~SurfaceControl.OPAQUE;
+ }
+ }
+
+ private Rect getParentSurfaceInsets() {
+ final ViewRootImpl root = getViewRootImpl();
+ if (root == null) {
+ return null;
+ } else {
+ return root.mWindowAttributes.surfaceInsets;
+ }
+ }
+
+ /** @hide */
+ protected void updateSurface() {
+ if (!mHaveFrame) {
+ return;
+ }
+ ViewRootImpl viewRoot = getViewRootImpl();
+ if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) {
+ return;
+ }
+
+ mTranslator = viewRoot.mTranslator;
+ if (mTranslator != null) {
+ mSurface.setCompatibilityTranslator(mTranslator);
+ }
+
+ int myWidth = mRequestedWidth;
+ if (myWidth <= 0) myWidth = getWidth();
+ int myHeight = mRequestedHeight;
+ if (myHeight <= 0) myHeight = getHeight();
+
+ final boolean formatChanged = mFormat != mRequestedFormat;
+ final boolean visibleChanged = mVisible != mRequestedVisible;
+ final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
+ && mRequestedVisible;
+ final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
+ final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility;
+ boolean redrawNeeded = false;
+
+ if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) {
+ getLocationInWindow(mLocation);
+
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ + "Changes: creating=" + creating
+ + " format=" + formatChanged + " size=" + sizeChanged
+ + " visible=" + visibleChanged
+ + " left=" + (mWindowSpaceLeft != mLocation[0])
+ + " top=" + (mWindowSpaceTop != mLocation[1]));
+
+ try {
+ final boolean visible = mVisible = mRequestedVisible;
+ mWindowSpaceLeft = mLocation[0];
+ mWindowSpaceTop = mLocation[1];
+ mSurfaceWidth = myWidth;
+ mSurfaceHeight = myHeight;
+ mFormat = mRequestedFormat;
+ mLastWindowVisibility = mWindowVisibility;
+
+ mScreenRect.left = mWindowSpaceLeft;
+ mScreenRect.top = mWindowSpaceTop;
+ mScreenRect.right = mWindowSpaceLeft + getWidth();
+ mScreenRect.bottom = mWindowSpaceTop + getHeight();
+ if (mTranslator != null) {
+ mTranslator.translateRectInAppWindowToScreen(mScreenRect);
+ }
+
+ final Rect surfaceInsets = getParentSurfaceInsets();
+ mScreenRect.offset(surfaceInsets.left, surfaceInsets.top);
+
+ if (creating) {
+ mSurfaceSession = new SurfaceSession(viewRoot.mSurface);
+ mDeferredDestroySurfaceControl = mSurfaceControl;
+
+ updateOpaqueFlag();
+ final String name = "SurfaceView - " + viewRoot.getTitle().toString();
+
+ mSurfaceControl = new SurfaceControlWithBackground(
+ name,
+ (mSurfaceFlags & SurfaceControl.OPAQUE) != 0,
+ new SurfaceControl.Builder(mSurfaceSession)
+ .setSize(mSurfaceWidth, mSurfaceHeight)
+ .setFormat(mFormat)
+ .setFlags(mSurfaceFlags));
+ } else if (mSurfaceControl == null) {
+ return;
+ }
+
+ boolean realSizeChanged = false;
+
+ mSurfaceLock.lock();
+ try {
+ mDrawingStopped = !visible;
+
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ + "Cur surface: " + mSurface);
+
+ SurfaceControl.openTransaction();
+ try {
+ mSurfaceControl.setLayer(mSubLayer);
+ if (mViewVisibility) {
+ mSurfaceControl.show();
+ } else {
+ mSurfaceControl.hide();
+ }
+
+ // While creating the surface, we will set it's initial
+ // geometry. Outside of that though, we should generally
+ // leave it to the RenderThread.
+ //
+ // There is one more case when the buffer size changes we aren't yet
+ // prepared to sync (as even following the transaction applying
+ // we still need to latch a buffer).
+ // b/28866173
+ if (sizeChanged || creating || !mRtHandlingPositionUpdates) {
+ mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top);
+ mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth,
+ 0.0f, 0.0f,
+ mScreenRect.height() / (float) mSurfaceHeight);
+ }
+ if (sizeChanged) {
+ mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight);
+ }
+ } finally {
+ SurfaceControl.closeTransaction();
+ }
+
+ if (sizeChanged || creating) {
+ redrawNeeded = true;
+ }
+
+ mSurfaceFrame.left = 0;
+ mSurfaceFrame.top = 0;
+ if (mTranslator == null) {
+ mSurfaceFrame.right = mSurfaceWidth;
+ mSurfaceFrame.bottom = mSurfaceHeight;
+ } else {
+ float appInvertedScale = mTranslator.applicationInvertedScale;
+ mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f);
+ mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f);
+ }
+
+ final int surfaceWidth = mSurfaceFrame.right;
+ final int surfaceHeight = mSurfaceFrame.bottom;
+ realSizeChanged = mLastSurfaceWidth != surfaceWidth
+ || mLastSurfaceHeight != surfaceHeight;
+ mLastSurfaceWidth = surfaceWidth;
+ mLastSurfaceHeight = surfaceHeight;
+ } finally {
+ mSurfaceLock.unlock();
+ }
+
+ try {
+ redrawNeeded |= visible && !mDrawFinished;
+
+ SurfaceHolder.Callback callbacks[] = null;
+
+ final boolean surfaceChanged = creating;
+ if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) {
+ mSurfaceCreated = false;
+ if (mSurface.isValid()) {
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ + "visibleChanged -- surfaceDestroyed");
+ callbacks = getSurfaceCallbacks();
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceDestroyed(mSurfaceHolder);
+ }
+ // Since Android N the same surface may be reused and given to us
+ // again by the system server at a later point. However
+ // as we didn't do this in previous releases, clients weren't
+ // necessarily required to clean up properly in
+ // surfaceDestroyed. This leads to problems for example when
+ // clients don't destroy their EGL context, and try
+ // and create a new one on the same surface following reuse.
+ // Since there is no valid use of the surface in-between
+ // surfaceDestroyed and surfaceCreated, we force a disconnect,
+ // so the next connect will always work if we end up reusing
+ // the surface.
+ if (mSurface.isValid()) {
+ mSurface.forceScopedDisconnect();
+ }
+ }
+ }
+
+ if (creating) {
+ mSurface.copyFrom(mSurfaceControl);
+ }
+
+ if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.O) {
+ // Some legacy applications use the underlying native {@link Surface} object
+ // as a key to whether anything has changed. In these cases, updates to the
+ // existing {@link Surface} will be ignored when the size changes.
+ // Therefore, we must explicitly recreate the {@link Surface} in these
+ // cases.
+ mSurface.createFrom(mSurfaceControl);
+ }
+
+ if (visible && mSurface.isValid()) {
+ if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {
+ mSurfaceCreated = true;
+ mIsCreating = true;
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ + "visibleChanged -- surfaceCreated");
+ if (callbacks == null) {
+ callbacks = getSurfaceCallbacks();
+ }
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceCreated(mSurfaceHolder);
+ }
+ }
+ if (creating || formatChanged || sizeChanged
+ || visibleChanged || realSizeChanged) {
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ + "surfaceChanged -- format=" + mFormat
+ + " w=" + myWidth + " h=" + myHeight);
+ if (callbacks == null) {
+ callbacks = getSurfaceCallbacks();
+ }
+ for (SurfaceHolder.Callback c : callbacks) {
+ c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight);
+ }
+ }
+ if (redrawNeeded) {
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ + "surfaceRedrawNeeded");
+ if (callbacks == null) {
+ callbacks = getSurfaceCallbacks();
+ }
+
+ mPendingReportDraws++;
+ viewRoot.drawPending();
+ SurfaceCallbackHelper sch =
+ new SurfaceCallbackHelper(this::onDrawFinished);
+ sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
+ }
+ }
+ } finally {
+ mIsCreating = false;
+ if (mSurfaceControl != null && !mSurfaceCreated) {
+ mSurface.release();
+ // If we are not in the stopped state, then the destruction of the Surface
+ // represents a visual change we need to display, and we should go ahead
+ // and destroy the SurfaceControl. However if we are in the stopped state,
+ // we can just leave the Surface around so it can be a part of animations,
+ // and we let the life-time be tied to the parent surface.
+ if (!mWindowStopped) {
+ mSurfaceControl.destroy();
+ mSurfaceControl = null;
+ }
+ }
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception configuring surface", ex);
+ }
+ if (DEBUG) Log.v(
+ TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top
+ + " w=" + mScreenRect.width() + " h=" + mScreenRect.height()
+ + ", frame=" + mSurfaceFrame);
+ } else {
+ // Calculate the window position in case RT loses the window
+ // and we need to fallback to a UI-thread driven position update
+ getLocationInSurface(mLocation);
+ final boolean positionChanged = mWindowSpaceLeft != mLocation[0]
+ || mWindowSpaceTop != mLocation[1];
+ final boolean layoutSizeChanged = getWidth() != mScreenRect.width()
+ || getHeight() != mScreenRect.height();
+ if (positionChanged || layoutSizeChanged) { // Only the position has changed
+ mWindowSpaceLeft = mLocation[0];
+ mWindowSpaceTop = mLocation[1];
+ // For our size changed check, we keep mScreenRect.width() and mScreenRect.height()
+ // in view local space.
+ mLocation[0] = getWidth();
+ mLocation[1] = getHeight();
+
+ mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop,
+ mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]);
+
+ if (mTranslator != null) {
+ mTranslator.translateRectInAppWindowToScreen(mScreenRect);
+ }
+
+ if (mSurfaceControl == null) {
+ return;
+ }
+
+ if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) {
+ try {
+ if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " +
+ "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
+ mScreenRect.left, mScreenRect.top,
+ mScreenRect.right, mScreenRect.bottom));
+ setParentSpaceRectangle(mScreenRect, -1);
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception configuring surface", ex);
+ }
+ }
+ }
+ }
+ }
+
+ private void onDrawFinished() {
+ if (DEBUG) {
+ Log.i(TAG, System.identityHashCode(this) + " "
+ + "finishedDrawing");
+ }
+
+ if (mDeferredDestroySurfaceControl != null) {
+ mDeferredDestroySurfaceControl.destroy();
+ mDeferredDestroySurfaceControl = null;
+ }
+
+ runOnUiThread(() -> {
+ performDrawFinished();
+ });
+ }
+
+ /**
+ * A place to over-ride for applying child-surface transactions.
+ * These can be synchronized with the viewroot surface using deferTransaction.
+ *
+ * Called from RenderWorker while UI thread is paused.
+ * @hide
+ */
+ protected void applyChildSurfaceTransaction_renderWorker(SurfaceControl.Transaction t,
+ Surface viewRootSurface, long nextViewRootFrameNumber) {
+ }
+
+ private void applySurfaceTransforms(SurfaceControl surface, Rect position, long frameNumber) {
+ if (frameNumber > 0) {
+ final ViewRootImpl viewRoot = getViewRootImpl();
+
+ mRtTransaction.deferTransactionUntilSurface(surface, viewRoot.mSurface,
+ frameNumber);
+ }
+
+ mRtTransaction.setPosition(surface, position.left, position.top);
+ mRtTransaction.setMatrix(surface,
+ position.width() / (float) mSurfaceWidth,
+ 0.0f, 0.0f,
+ position.height() / (float) mSurfaceHeight);
+ }
+
+ private void setParentSpaceRectangle(Rect position, long frameNumber) {
+ final ViewRootImpl viewRoot = getViewRootImpl();
+
+ applySurfaceTransforms(mSurfaceControl, position, frameNumber);
+ applySurfaceTransforms(mSurfaceControl.mBackgroundControl, position, frameNumber);
+
+ applyChildSurfaceTransaction_renderWorker(mRtTransaction, viewRoot.mSurface,
+ frameNumber);
+
+ mRtTransaction.apply();
+ }
+
+ private Rect mRTLastReportedPosition = new Rect();
+
+ /**
+ * Called by native by a Rendering Worker thread to update the window position
+ * @hide
+ */
+ public final void updateSurfacePosition_renderWorker(long frameNumber,
+ int left, int top, int right, int bottom) {
+ if (mSurfaceControl == null) {
+ return;
+ }
+
+ // TODO: This is teensy bit racey in that a brand new SurfaceView moving on
+ // its 2nd frame if RenderThread is running slowly could potentially see
+ // this as false, enter the branch, get pre-empted, then this comes along
+ // and reports a new position, then the UI thread resumes and reports
+ // its position. This could therefore be de-sync'd in that interval, but
+ // the synchronization would violate the rule that RT must never block
+ // on the UI thread which would open up potential deadlocks. The risk of
+ // a single-frame desync is therefore preferable for now.
+ mRtHandlingPositionUpdates = true;
+ if (mRTLastReportedPosition.left == left
+ && mRTLastReportedPosition.top == top
+ && mRTLastReportedPosition.right == right
+ && mRTLastReportedPosition.bottom == bottom) {
+ return;
+ }
+ try {
+ if (DEBUG) {
+ Log.d(TAG, String.format("%d updateSurfacePosition RenderWorker, frameNr = %d, " +
+ "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
+ frameNumber, left, top, right, bottom));
+ }
+ mRTLastReportedPosition.set(left, top, right, bottom);
+ setParentSpaceRectangle(mRTLastReportedPosition, frameNumber);
+ // Now overwrite mRTLastReportedPosition with our values
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception from repositionChild", ex);
+ }
+ }
+
+ /**
+ * Called by native on RenderThread to notify that the view is no longer in the
+ * draw tree. UI thread is blocked at this point.
+ * @hide
+ */
+ public final void surfacePositionLost_uiRtSync(long frameNumber) {
+ if (DEBUG) {
+ Log.d(TAG, String.format("%d windowPositionLost, frameNr = %d",
+ System.identityHashCode(this), frameNumber));
+ }
+ mRTLastReportedPosition.setEmpty();
+
+ if (mSurfaceControl == null) {
+ return;
+ }
+ if (mRtHandlingPositionUpdates) {
+ mRtHandlingPositionUpdates = false;
+ // This callback will happen while the UI thread is blocked, so we can
+ // safely access other member variables at this time.
+ // So do what the UI thread would have done if RT wasn't handling position
+ // updates.
+ if (!mScreenRect.isEmpty() && !mScreenRect.equals(mRTLastReportedPosition)) {
+ try {
+ if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition, " +
+ "postion = [%d, %d, %d, %d]", System.identityHashCode(this),
+ mScreenRect.left, mScreenRect.top,
+ mScreenRect.right, mScreenRect.bottom));
+ setParentSpaceRectangle(mScreenRect, frameNumber);
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception configuring surface", ex);
+ }
+ }
+ }
+ }
+
+ private SurfaceHolder.Callback[] getSurfaceCallbacks() {
+ SurfaceHolder.Callback callbacks[];
+ synchronized (mCallbacks) {
+ callbacks = new SurfaceHolder.Callback[mCallbacks.size()];
+ mCallbacks.toArray(callbacks);
+ }
+ return callbacks;
}
- private SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
+ private void runOnUiThread(Runnable runnable) {
+ Handler handler = getHandler();
+ if (handler != null && handler.getLooper() != Looper.myLooper()) {
+ handler.post(runnable);
+ } else {
+ runnable.run();
+ }
+ }
+
+ /**
+ * Check to see if the surface has fixed size dimensions or if the surface's
+ * dimensions are dimensions are dependent on its current layout.
+ *
+ * @return true if the surface has dimensions that are fixed in size
+ * @hide
+ */
+ public boolean isFixedSize() {
+ return (mRequestedWidth != -1 || mRequestedHeight != -1);
+ }
+
+ private boolean isAboveParent() {
+ return mSubLayer >= 0;
+ }
+
+ /**
+ * Set an opaque background color to use with this {@link SurfaceView} when it's being resized
+ * and size of the content hasn't updated yet. This color will fill the expanded area when the
+ * view becomes larger.
+ * @param bgColor An opaque color to fill the background. Alpha component will be ignored.
+ * @hide
+ */
+ public void setResizeBackgroundColor(int bgColor) {
+ mSurfaceControl.setBackgroundColor(bgColor);
+ }
+
+ private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
+ private static final String LOG_TAG = "SurfaceHolder";
@Override
public boolean isCreating() {
- return false;
+ return mIsCreating;
}
@Override
public void addCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ // This is a linear search, but in practice we'll
+ // have only a couple callbacks, so it doesn't matter.
+ if (mCallbacks.contains(callback) == false) {
+ mCallbacks.add(callback);
+ }
+ }
}
@Override
public void removeCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.remove(callback);
+ }
}
@Override
public void setFixedSize(int width, int height) {
+ if (mRequestedWidth != width || mRequestedHeight != height) {
+ mRequestedWidth = width;
+ mRequestedHeight = height;
+ requestLayout();
+ }
}
@Override
public void setSizeFromLayout() {
+ if (mRequestedWidth != -1 || mRequestedHeight != -1) {
+ mRequestedWidth = mRequestedHeight = -1;
+ requestLayout();
+ }
}
@Override
public void setFormat(int format) {
+ // for backward compatibility reason, OPAQUE always
+ // means 565 for SurfaceView
+ if (format == PixelFormat.OPAQUE)
+ format = PixelFormat.RGB_565;
+
+ mRequestedFormat = format;
+ if (mSurfaceControl != null) {
+ updateSurface();
+ }
}
+ /**
+ * @deprecated setType is now ignored.
+ */
@Override
- public void setType(int type) {
- }
+ @Deprecated
+ public void setType(int type) { }
@Override
public void setKeepScreenOn(boolean screenOn) {
+ runOnUiThread(() -> SurfaceView.this.setKeepScreenOn(screenOn));
}
+ /**
+ * Gets a {@link Canvas} for drawing into the SurfaceView's Surface
+ *
+ * After drawing into the provided {@link Canvas}, the caller must
+ * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
+ *
+ * The caller must redraw the entire surface.
+ * @return A canvas for drawing into the surface.
+ */
@Override
public Canvas lockCanvas() {
- return null;
+ return internalLockCanvas(null, false);
+ }
+
+ /**
+ * Gets a {@link Canvas} for drawing into the SurfaceView's Surface
+ *
+ * After drawing into the provided {@link Canvas}, the caller must
+ * invoke {@link #unlockCanvasAndPost} to post the new contents to the surface.
+ *
+ * @param inOutDirty A rectangle that represents the dirty region that the caller wants
+ * to redraw. This function may choose to expand the dirty rectangle if for example
+ * the surface has been resized or if the previous contents of the surface were
+ * not available. The caller must redraw the entire dirty region as represented
+ * by the contents of the inOutDirty rectangle upon return from this function.
+ * The caller may also pass <code>null</code> instead, in the case where the
+ * entire surface should be redrawn.
+ * @return A canvas for drawing into the surface.
+ */
+ @Override
+ public Canvas lockCanvas(Rect inOutDirty) {
+ return internalLockCanvas(inOutDirty, false);
}
@Override
- public Canvas lockCanvas(Rect dirty) {
+ public Canvas lockHardwareCanvas() {
+ return internalLockCanvas(null, true);
+ }
+
+ private Canvas internalLockCanvas(Rect dirty, boolean hardware) {
+ mSurfaceLock.lock();
+
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Locking canvas... stopped="
+ + mDrawingStopped + ", surfaceControl=" + mSurfaceControl);
+
+ Canvas c = null;
+ if (!mDrawingStopped && mSurfaceControl != null) {
+ try {
+ if (hardware) {
+ c = mSurface.lockHardwareCanvas();
+ } else {
+ c = mSurface.lockCanvas(dirty);
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Exception locking surface", e);
+ }
+ }
+
+ if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " " + "Returned canvas: " + c);
+ if (c != null) {
+ mLastLockTime = SystemClock.uptimeMillis();
+ return c;
+ }
+
+ // If the Surface is not ready to be drawn, then return null,
+ // but throttle calls to this function so it isn't called more
+ // than every 100ms.
+ long now = SystemClock.uptimeMillis();
+ long nextTime = mLastLockTime + 100;
+ if (nextTime > now) {
+ try {
+ Thread.sleep(nextTime-now);
+ } catch (InterruptedException e) {
+ }
+ now = SystemClock.uptimeMillis();
+ }
+ mLastLockTime = now;
+ mSurfaceLock.unlock();
+
return null;
}
+ /**
+ * Posts the new contents of the {@link Canvas} to the surface and
+ * releases the {@link Canvas}.
+ *
+ * @param canvas The canvas previously obtained from {@link #lockCanvas}.
+ */
@Override
public void unlockCanvasAndPost(Canvas canvas) {
+ mSurface.unlockCanvasAndPost(canvas);
+ mSurfaceLock.unlock();
}
@Override
public Surface getSurface() {
- return null;
+ return mSurface;
}
@Override
public Rect getSurfaceFrame() {
- return null;
+ return mSurfaceFrame;
}
};
-}
+ class SurfaceControlWithBackground extends SurfaceControl {
+ SurfaceControl mBackgroundControl;
+ private boolean mOpaque = true;
+ public boolean mVisible = false;
+
+ public SurfaceControlWithBackground(String name, boolean opaque, SurfaceControl.Builder b)
+ throws Exception {
+ super(b.setName(name).build());
+
+ mBackgroundControl = b.setName("Background for -" + name)
+ .setFormat(OPAQUE)
+ .setColorLayer(true)
+ .build();
+ mOpaque = opaque;
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ super.setAlpha(alpha);
+ mBackgroundControl.setAlpha(alpha);
+ }
+
+ @Override
+ public void setLayer(int zorder) {
+ super.setLayer(zorder);
+ // -3 is below all other child layers as SurfaceView never goes below -2
+ mBackgroundControl.setLayer(-3);
+ }
+
+ @Override
+ public void setPosition(float x, float y) {
+ super.setPosition(x, y);
+ mBackgroundControl.setPosition(x, y);
+ }
+
+ @Override
+ public void setSize(int w, int h) {
+ super.setSize(w, h);
+ mBackgroundControl.setSize(w, h);
+ }
+
+ @Override
+ public void setWindowCrop(Rect crop) {
+ super.setWindowCrop(crop);
+ mBackgroundControl.setWindowCrop(crop);
+ }
+
+ @Override
+ public void setFinalCrop(Rect crop) {
+ super.setFinalCrop(crop);
+ mBackgroundControl.setFinalCrop(crop);
+ }
+
+ @Override
+ public void setLayerStack(int layerStack) {
+ super.setLayerStack(layerStack);
+ mBackgroundControl.setLayerStack(layerStack);
+ }
+
+ @Override
+ public void setOpaque(boolean isOpaque) {
+ super.setOpaque(isOpaque);
+ mOpaque = isOpaque;
+ updateBackgroundVisibility();
+ }
+
+ @Override
+ public void setSecure(boolean isSecure) {
+ super.setSecure(isSecure);
+ }
+
+ @Override
+ public void setMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
+ super.setMatrix(dsdx, dtdx, dsdy, dtdy);
+ mBackgroundControl.setMatrix(dsdx, dtdx, dsdy, dtdy);
+ }
+
+ @Override
+ public void hide() {
+ super.hide();
+ mVisible = false;
+ updateBackgroundVisibility();
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ mVisible = true;
+ updateBackgroundVisibility();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ mBackgroundControl.destroy();
+ }
+
+ @Override
+ public void release() {
+ super.release();
+ mBackgroundControl.release();
+ }
+
+ @Override
+ public void setTransparentRegionHint(Region region) {
+ super.setTransparentRegionHint(region);
+ mBackgroundControl.setTransparentRegionHint(region);
+ }
+
+ @Override
+ public void deferTransactionUntil(IBinder handle, long frame) {
+ super.deferTransactionUntil(handle, frame);
+ mBackgroundControl.deferTransactionUntil(handle, frame);
+ }
+
+ @Override
+ public void deferTransactionUntil(Surface barrier, long frame) {
+ super.deferTransactionUntil(barrier, frame);
+ mBackgroundControl.deferTransactionUntil(barrier, frame);
+ }
+
+ /** Set the color to fill the background with. */
+ private void setBackgroundColor(int bgColor) {
+ final float[] colorComponents = new float[] { Color.red(bgColor) / 255.f,
+ Color.green(bgColor) / 255.f, Color.blue(bgColor) / 255.f };
+
+ SurfaceControl.openTransaction();
+ try {
+ mBackgroundControl.setColor(colorComponents);
+ } finally {
+ SurfaceControl.closeTransaction();
+ }
+ }
+
+ void updateBackgroundVisibility() {
+ if (mOpaque && mVisible) {
+ mBackgroundControl.show();
+ } else {
+ mBackgroundControl.hide();
+ }
+ }
+ }
+}
diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java
index 5eb7e9cb..e03f5faa 100644
--- a/android/view/ThreadedRenderer.java
+++ b/android/view/ThreadedRenderer.java
@@ -190,6 +190,10 @@ public final class ThreadedRenderer {
*/
public static final String DEBUG_FPS_DIVISOR = "debug.hwui.fps_divisor";
+ public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101;
+ public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102;
+ public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103;
+
static {
// Try to check OpenGL support early if possible.
isAvailable();
@@ -1140,6 +1144,16 @@ public final class ThreadedRenderer {
nHackySetRTAnimationsEnabled(divisor <= 1);
}
+ /**
+ * Changes the OpenGL context priority if IMG_context_priority extension is available. Must be
+ * called before any OpenGL context is created.
+ *
+ * @param priority The priority to use. Must be one of EGL_CONTEXT_PRIORITY_* values.
+ */
+ public static void setContextPriority(int priority) {
+ nSetContextPriority(priority);
+ }
+
/** Not actually public - internal use only. This doc to make lint happy */
public static native void disableVsync();
@@ -1213,4 +1227,5 @@ public final class ThreadedRenderer {
private static native void nHackySetRTAnimationsEnabled(boolean enabled);
private static native void nSetDebuggingEnabled(boolean enabled);
private static native void nSetIsolatedProcess(boolean enabled);
+ private static native void nSetContextPriority(int priority);
}
diff --git a/android/view/View.java b/android/view/View.java
index 97e11b15..71b60844 100644
--- a/android/view/View.java
+++ b/android/view/View.java
@@ -697,6 +697,7 @@ import java.util.function.Predicate;
* security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}.
* </p>
*
+ * @attr ref android.R.styleable#View_accessibilityHeading
* @attr ref android.R.styleable#View_alpha
* @attr ref android.R.styleable#View_background
* @attr ref android.R.styleable#View_clickable
@@ -2955,7 +2956,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* 1 PFLAG3_SCREEN_READER_FOCUSABLE
* 1 PFLAG3_AGGREGATED_VISIBLE
* 1 PFLAG3_AUTOFILLID_EXPLICITLY_SET
- * 1 available
+ * 1 PFLAG3_ACCESSIBILITY_HEADING
* |-------|-------|-------|-------|
*/
@@ -3252,6 +3253,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private static final int PFLAG3_AUTOFILLID_EXPLICITLY_SET = 0x40000000;
+ /**
+ * Indicates if the View is a heading for accessibility purposes
+ */
+ private static final int PFLAG3_ACCESSIBILITY_HEADING = 0x80000000;
+
/* End of masks for mPrivateFlags3 */
/**
@@ -5475,6 +5481,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
case R.styleable.View_outlineAmbientShadowColor:
setOutlineAmbientShadowColor(a.getColor(attr, Color.BLACK));
break;
+ case com.android.internal.R.styleable.View_accessibilityHeading:
+ setAccessibilityHeading(a.getBoolean(attr, false));
}
}
@@ -8795,6 +8803,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN);
populateAccessibilityNodeInfoDrawingOrderInParent(info);
info.setPaneTitle(mAccessibilityPaneTitle);
+ info.setHeading(isAccessibilityHeading());
}
/**
@@ -10398,7 +10407,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @param willNotCacheDrawing true if this view does not cache its
* drawing, false otherwise
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
*/
+ @Deprecated
public void setWillNotCacheDrawing(boolean willNotCacheDrawing) {
setFlags(willNotCacheDrawing ? WILL_NOT_CACHE_DRAWING : 0, WILL_NOT_CACHE_DRAWING);
}
@@ -10407,8 +10430,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* Returns whether or not this View can cache its drawing or not.
*
* @return true if this view does not cache its drawing, false otherwise
+ *
+ * @deprecated The view drawing cache was largely made obsolete with the introduction of
+ * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache
+ * layers are largely unnecessary and can easily result in a net loss in performance due to the
+ * cost of creating and updating the layer. In the rare cases where caching layers are useful,
+ * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware
+ * rendering. For software-rendered snapshots of a small part of the View hierarchy or
+ * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or
+ * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these
+ * software-rendered usages are discouraged and have compatibility issues with hardware-only
+ * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE}
+ * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback
+ * reports or unit testing the {@link PixelCopy} API is recommended.
*/
@ViewDebug.ExportedProperty(category = "drawing")
+ @Deprecated
public boolean willNotCacheDrawing() {
return (mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING;
}
@@ -10754,11 +10791,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* accessibility tools.
*/
public void setScreenReaderFocusable(boolean screenReaderFocusable) {
+ updatePflags3AndNotifyA11yIfChanged(PFLAG3_SCREEN_READER_FOCUSABLE, screenReaderFocusable);
+ }
+
+ /**
+ * Gets whether this view is a heading for accessibility purposes.
+ *
+ * @return {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#View_accessibilityHeading
+ */
+ public boolean isAccessibilityHeading() {
+ return (mPrivateFlags3 & PFLAG3_ACCESSIBILITY_HEADING) != 0;
+ }
+
+ /**
+ * Set if view is a heading for a section of content for accessibility purposes.
+ *
+ * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#View_accessibilityHeading
+ */
+ public void setAccessibilityHeading(boolean isHeading) {
+ updatePflags3AndNotifyA11yIfChanged(PFLAG3_ACCESSIBILITY_HEADING, isHeading);
+ }
+
+ private void updatePflags3AndNotifyA11yIfChanged(int mask, boolean newValue) {
int pflags3 = mPrivateFlags3;
- if (screenReaderFocusable) {
- pflags3 |= PFLAG3_SCREEN_READER_FOCUSABLE;
+ if (newValue) {
+ pflags3 |= mask;
} else {
- pflags3 &= ~PFLAG3_SCREEN_READER_FOCUSABLE;
+ pflags3 &= ~mask;
}
if (pflags3 != mPrivateFlags3) {
@@ -11763,6 +11826,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return null;
}
+ /** @hide */
+ View getSelfOrParentImportantForA11y() {
+ if (isImportantForAccessibility()) return this;
+ ViewParent parent = getParentForAccessibility();
+ if (parent instanceof View) return (View) parent;
+ return null;
+ }
+
/**
* Adds the children of this View relevant for accessibility to the given list
* as output. Since some Views are not important for accessibility the added
@@ -14978,10 +15049,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {
ensureTransformationInfo();
if (mTransformationInfo.mAlpha != alpha) {
- // Report visibility changes, which can affect children, to accessibility
- if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
- }
+ float oldAlpha = mTransformationInfo.mAlpha;
mTransformationInfo.mAlpha = alpha;
if (onSetAlpha((int) (alpha * 255))) {
mPrivateFlags |= PFLAG_ALPHA_SET;
@@ -14993,6 +15061,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(true, false);
mRenderNode.setAlpha(getFinalAlpha());
}
+ // Report visibility changes, which can affect children, to accessibility
+ if ((alpha == 0) ^ (oldAlpha == 0)) {
+ notifySubtreeAccessibilityStateChangedIfNeeded();
+ }
}
}
diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java
index 6002fe51..2ec42c0d 100644
--- a/android/view/ViewGroup.java
+++ b/android/view/ViewGroup.java
@@ -5692,6 +5692,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
&& isShown());
+ notifySubtreeAccessibilityStateChangedIfNeeded();
}
/**
diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java
index 433c90b3..730c3729 100644
--- a/android/view/ViewRootImpl.java
+++ b/android/view/ViewRootImpl.java
@@ -3719,7 +3719,7 @@ public final class ViewRootImpl implements ViewParent,
checkThread();
if (mView != null) {
if (!mView.hasFocus()) {
- if (sAlwaysAssignFocus || !isInTouchMode()) {
+ if (sAlwaysAssignFocus || !mAttachInfo.mInTouchMode) {
v.requestFocus();
}
} else {
@@ -6482,17 +6482,17 @@ public final class ViewRootImpl implements ViewParent,
params.type = mOrigWindowType;
}
}
+ }
- if (mSurface.isValid()) {
- params.frameNumber = mSurface.getNextFrameNumber();
- }
+ long frameNumber = -1;
+ if (mSurface.isValid()) {
+ frameNumber = mSurface.getNextFrameNumber();
}
- int relayoutResult = mWindowSession.relayout(
- mWindow, mSeq, params,
+ int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
- (int) (mView.getMeasuredHeight() * appScale + 0.5f),
- viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
+ (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
+ insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurface);
@@ -8305,6 +8305,12 @@ public final class ViewRootImpl implements ViewParent,
public View mSource;
public long mLastEventTimeMillis;
+ /**
+ * Override for {@link AccessibilityEvent#originStackTrace} to provide the stack trace
+ * of the original {@link #runOrPost} call instead of one for sending the delayed event
+ * from a looper.
+ */
+ public StackTraceElement[] mOrigin;
@Override
public void run() {
@@ -8322,6 +8328,7 @@ public final class ViewRootImpl implements ViewParent,
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
event.setContentChangeTypes(mChangeTypes);
+ if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin;
source.sendAccessibilityEventUnchecked(event);
} else {
mLastEventTimeMillis = 0;
@@ -8329,6 +8336,7 @@ public final class ViewRootImpl implements ViewParent,
// In any case reset to initial state.
source.resetSubtreeAccessibilityStateChanged();
mChangeTypes = 0;
+ if (AccessibilityEvent.DEBUG_ORIGIN) mOrigin = null;
}
public void runOrPost(View source, int changeType) {
@@ -8352,12 +8360,18 @@ public final class ViewRootImpl implements ViewParent,
// If there is no common predecessor, then mSource points to
// a removed view, hence in this case always prefer the source.
View predecessor = getCommonPredecessor(mSource, source);
+ if (predecessor != null) {
+ predecessor = predecessor.getSelfOrParentImportantForA11y();
+ }
mSource = (predecessor != null) ? predecessor : source;
mChangeTypes |= changeType;
return;
}
mSource = source;
mChangeTypes = changeType;
+ if (AccessibilityEvent.DEBUG_ORIGIN) {
+ mOrigin = Thread.currentThread().getStackTrace();
+ }
final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis;
final long minEventIntevalMillis =
ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java
index f6181d70..0f5c23f7 100644
--- a/android/view/WindowManager.java
+++ b/android/view/WindowManager.java
@@ -2438,13 +2438,6 @@ public interface WindowManager extends ViewManager {
public long hideTimeoutMilliseconds = -1;
/**
- * A frame number in which changes requested in this layout will be rendered.
- *
- * @hide
- */
- public long frameNumber = -1;
-
- /**
* The color mode requested by this window. The target display may
* not be able to honor the request. When the color mode is not set
* to {@link ActivityInfo#COLOR_MODE_DEFAULT}, it might override the
@@ -2617,7 +2610,6 @@ public interface WindowManager extends ViewManager {
TextUtils.writeToParcel(accessibilityTitle, out, parcelableFlags);
out.writeInt(mColorMode);
out.writeLong(hideTimeoutMilliseconds);
- out.writeLong(frameNumber);
}
public static final Parcelable.Creator<LayoutParams> CREATOR
@@ -2674,7 +2666,6 @@ public interface WindowManager extends ViewManager {
accessibilityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
mColorMode = in.readInt();
hideTimeoutMilliseconds = in.readLong();
- frameNumber = in.readLong();
}
@SuppressWarnings({"PointlessBitwiseExpression"})
@@ -2875,10 +2866,6 @@ public interface WindowManager extends ViewManager {
changes |= SURFACE_INSETS_CHANGED;
}
- // The frame number changing is only relevant in the context of other
- // changes, and so we don't need to track it with a flag.
- frameNumber = o.frameNumber;
-
if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) {
hasManualSurfaceInsets = o.hasManualSurfaceInsets;
changes |= SURFACE_INSETS_CHANGED;
diff --git a/android/view/WindowManagerGlobal.java b/android/view/WindowManagerGlobal.java
index cca66d6b..08c2d0b7 100644
--- a/android/view/WindowManagerGlobal.java
+++ b/android/view/WindowManagerGlobal.java
@@ -610,6 +610,10 @@ public final class WindowManagerGlobal {
ViewRootImpl root = mRoots.get(i);
// Client might remove the view by "stopped" event.
root.setWindowStopped(stopped);
+ // Recursively forward stopped state to View's attached
+ // to this Window rather than the root application token,
+ // e.g. PopupWindow's.
+ setStoppedState(root.mAttachInfo.mWindowToken, stopped);
}
}
}
diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java
index e0f74a7d..7946e9e2 100644
--- a/android/view/accessibility/AccessibilityEvent.java
+++ b/android/view/accessibility/AccessibilityEvent.java
@@ -201,6 +201,7 @@ import java.util.List;
* <em>Properties:</em></br>
* <ul>
* <li>{@link #getEventType()} - The type of the event.</li>
+ * <li>{@link #getContentChangeTypes()} - The type of state changes.</li>
* <li>{@link #getSource()} - The source info (for registered clients).</li>
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
@@ -388,6 +389,8 @@ import java.util.List;
*/
public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable {
private static final boolean DEBUG = false;
+ /** @hide */
+ public static final boolean DEBUG_ORIGIN = false;
/**
* Invalid selection/focus position.
@@ -748,7 +751,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
private static final int MAX_POOL_SIZE = 10;
private static final SynchronizedPool<AccessibilityEvent> sPool =
- new SynchronizedPool<AccessibilityEvent>(MAX_POOL_SIZE);
+ new SynchronizedPool<>(MAX_POOL_SIZE);
private @EventType int mEventType;
private CharSequence mPackageName;
@@ -758,6 +761,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
int mContentChangeTypes;
int mWindowChangeTypes;
+ /**
+ * The stack trace describing where this event originated from on the app side.
+ * Only populated if {@link #DEBUG_ORIGIN} is enabled
+ * Can be inspected(e.g. printed) from an
+ * {@link android.accessibilityservice.AccessibilityService} to trace where particular events
+ * are being dispatched from.
+ *
+ * @hide
+ */
+ public StackTraceElement[] originStackTrace = null;
+
private ArrayList<AccessibilityRecord> mRecords;
/*
@@ -780,6 +794,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
mWindowChangeTypes = event.mWindowChangeTypes;
mEventTime = event.mEventTime;
mPackageName = event.mPackageName;
+ if (DEBUG_ORIGIN) originStackTrace = event.originStackTrace;
}
/**
@@ -849,16 +864,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
}
/**
- * Gets the bit mask of change types signaled by an
- * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent
- * multiple change types.
+ * Gets the bit mask of change types signaled by a
+ * {@link #TYPE_WINDOW_CONTENT_CHANGED} event or {@link #TYPE_WINDOW_STATE_CHANGED}. A single
+ * event may represent multiple change types.
*
* @return The bit mask of change types. One or more of:
* <ul>
- * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
- * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
- * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT}
- * <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
+ * <li>{@link #CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION}
+ * <li>{@link #CONTENT_CHANGE_TYPE_SUBTREE}
+ * <li>{@link #CONTENT_CHANGE_TYPE_TEXT}
+ * <li>{@link #CONTENT_CHANGE_TYPE_PANE_TITLE}
+ * <li>{@link #CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
*/
@ContentChangeTypes
@@ -877,6 +893,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
}
case CONTENT_CHANGE_TYPE_SUBTREE: return "CONTENT_CHANGE_TYPE_SUBTREE";
case CONTENT_CHANGE_TYPE_TEXT: return "CONTENT_CHANGE_TYPE_TEXT";
+ case CONTENT_CHANGE_TYPE_PANE_TITLE: return "CONTENT_CHANGE_TYPE_PANE_TITLE";
case CONTENT_CHANGE_TYPE_UNDEFINED: return "CONTENT_CHANGE_TYPE_UNDEFINED";
default: return Integer.toHexString(type);
}
@@ -1104,7 +1121,9 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
*/
public static AccessibilityEvent obtain() {
AccessibilityEvent event = sPool.acquire();
- return (event != null) ? event : new AccessibilityEvent();
+ if (event == null) event = new AccessibilityEvent();
+ if (DEBUG_ORIGIN) event.originStackTrace = Thread.currentThread().getStackTrace();
+ return event;
}
/**
@@ -1142,6 +1161,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
record.recycle();
}
}
+ if (DEBUG_ORIGIN) originStackTrace = null;
}
/**
@@ -1164,7 +1184,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
// Read the records.
final int recordCount = parcel.readInt();
if (recordCount > 0) {
- mRecords = new ArrayList<AccessibilityRecord>(recordCount);
+ mRecords = new ArrayList<>(recordCount);
for (int i = 0; i < recordCount; i++) {
AccessibilityRecord record = AccessibilityRecord.obtain();
readAccessibilityRecordFromParcel(record, parcel);
@@ -1172,6 +1192,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
mRecords.add(record);
}
}
+
+ if (DEBUG_ORIGIN) {
+ originStackTrace = new StackTraceElement[parcel.readInt()];
+ for (int i = 0; i < originStackTrace.length; i++) {
+ originStackTrace[i] = new StackTraceElement(
+ parcel.readString(),
+ parcel.readString(),
+ parcel.readString(),
+ parcel.readInt());
+ }
+ }
}
/**
@@ -1227,6 +1258,17 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
AccessibilityRecord record = mRecords.get(i);
writeAccessibilityRecordToParcel(record, parcel, flags);
}
+
+ if (DEBUG_ORIGIN) {
+ if (originStackTrace == null) originStackTrace = Thread.currentThread().getStackTrace();
+ parcel.writeInt(originStackTrace.length);
+ for (StackTraceElement element : originStackTrace) {
+ parcel.writeString(element.getClassName());
+ parcel.writeString(element.getMethodName());
+ parcel.writeString(element.getFileName());
+ parcel.writeInt(element.getLineNumber());
+ }
+ }
}
/**
@@ -1285,7 +1327,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
}
if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) {
builder.append("; WindowChangeTypes: ").append(
- contentChangeTypesToString(mWindowChangeTypes));
+ windowChangeTypesToString(mWindowChangeTypes));
}
super.appendTo(builder);
if (DEBUG || DEBUG_CONCISE_TOSTRING) {
diff --git a/android/view/accessibility/AccessibilityInteractionClient.java b/android/view/accessibility/AccessibilityInteractionClient.java
index 72af203e..d60c4819 100644
--- a/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/android/view/accessibility/AccessibilityInteractionClient.java
@@ -326,12 +326,14 @@ public final class AccessibilityInteractionClient
accessibilityWindowId, accessibilityNodeId);
if (cachedInfo != null) {
if (DEBUG) {
- Log.i(LOG_TAG, "Node cache hit");
+ Log.i(LOG_TAG, "Node cache hit for "
+ + idToString(accessibilityWindowId, accessibilityNodeId));
}
return cachedInfo;
}
if (DEBUG) {
- Log.i(LOG_TAG, "Node cache miss");
+ Log.i(LOG_TAG, "Node cache miss for "
+ + idToString(accessibilityWindowId, accessibilityNodeId));
}
}
final int interactionId = mInteractionIdCounter.getAndIncrement();
@@ -368,6 +370,11 @@ public final class AccessibilityInteractionClient
return null;
}
+ private static String idToString(int accessibilityWindowId, long accessibilityNodeId) {
+ return accessibilityWindowId + "/"
+ + AccessibilityNodeInfo.idToString(accessibilityNodeId);
+ }
+
/**
* Finds an {@link AccessibilityNodeInfo} by View id. The search is performed in
* the window whose id is specified and starts from the node whose accessibility
diff --git a/android/view/accessibility/AccessibilityManager.java b/android/view/accessibility/AccessibilityManager.java
index 84b40641..cbb23f1a 100644
--- a/android/view/accessibility/AccessibilityManager.java
+++ b/android/view/accessibility/AccessibilityManager.java
@@ -16,48 +16,156 @@
package android.view.accessibility;
+import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_ENABLE_ACCESSIBILITY_VOLUME;
+
+import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SystemService;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.Binder;
import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
import android.view.IWindow;
import android.view.View;
import android.view.accessibility.AccessibilityEvent.EventType;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IntPair;
+
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
- * System level service that serves as an event dispatch for {@link AccessibilityEvent}s.
- * Such events are generated when something notable happens in the user interface,
+ * System level service that serves as an event dispatch for {@link AccessibilityEvent}s,
+ * and provides facilities for querying the accessibility state of the system.
+ * Accessibility events are generated when something notable happens in the user interface,
* for example an {@link android.app.Activity} starts, the focus or selection of a
* {@link android.view.View} changes etc. Parties interested in handling accessibility
* events implement and register an accessibility service which extends
- * {@code android.accessibilityservice.AccessibilityService}.
+ * {@link android.accessibilityservice.AccessibilityService}.
*
* @see AccessibilityEvent
- * @see android.content.Context#getSystemService
+ * @see AccessibilityNodeInfo
+ * @see android.accessibilityservice.AccessibilityService
+ * @see Context#getSystemService
+ * @see Context#ACCESSIBILITY_SERVICE
*/
-@SuppressWarnings("UnusedDeclaration")
+@SystemService(Context.ACCESSIBILITY_SERVICE)
public final class AccessibilityManager {
+ private static final boolean DEBUG = false;
+
+ private static final String LOG_TAG = "AccessibilityManager";
+
+ /** @hide */
+ public static final int STATE_FLAG_ACCESSIBILITY_ENABLED = 0x00000001;
+
+ /** @hide */
+ public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002;
+
+ /** @hide */
+ public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004;
+
+ /** @hide */
+ public static final int DALTONIZER_DISABLED = -1;
+
+ /** @hide */
+ public static final int DALTONIZER_SIMULATE_MONOCHROMACY = 0;
+
+ /** @hide */
+ public static final int DALTONIZER_CORRECT_DEUTERANOMALY = 12;
+
+ /** @hide */
+ public static final int AUTOCLICK_DELAY_DEFAULT = 600;
+
+ /**
+ * Activity action: Launch UI to manage which accessibility service or feature is assigned
+ * to the navigation bar Accessibility button.
+ * <p>
+ * Input: Nothing.
+ * </p>
+ * <p>
+ * Output: Nothing.
+ * </p>
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_CHOOSE_ACCESSIBILITY_BUTTON =
+ "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON";
+
+ static final Object sInstanceSync = new Object();
+
+ private static AccessibilityManager sInstance;
+
+ private final Object mLock = new Object();
+
+ private IAccessibilityManager mService;
+
+ final int mUserId;
+
+ final Handler mHandler;
+
+ final Handler.Callback mCallback;
+
+ boolean mIsEnabled;
+
+ int mRelevantEventTypes = AccessibilityEvent.TYPES_ALL_MASK;
+
+ boolean mIsTouchExplorationEnabled;
- private static AccessibilityManager sInstance = new AccessibilityManager(null, null, 0);
+ boolean mIsHighTextContrastEnabled;
+ AccessibilityPolicy mAccessibilityPolicy;
+
+ private final ArrayMap<AccessibilityStateChangeListener, Handler>
+ mAccessibilityStateChangeListeners = new ArrayMap<>();
+
+ private final ArrayMap<TouchExplorationStateChangeListener, Handler>
+ mTouchExplorationStateChangeListeners = new ArrayMap<>();
+
+ private final ArrayMap<HighTextContrastChangeListener, Handler>
+ mHighTextContrastStateChangeListeners = new ArrayMap<>();
+
+ private final ArrayMap<AccessibilityServicesStateChangeListener, Handler>
+ mServicesStateChangeListeners = new ArrayMap<>();
/**
- * Listener for the accessibility state.
+ * Map from a view's accessibility id to the list of request preparers set for that view
+ */
+ private SparseArray<List<AccessibilityRequestPreparer>> mRequestPreparerLists;
+
+ /**
+ * Listener for the system accessibility state. To listen for changes to the
+ * accessibility state on the device, implement this interface and register
+ * it with the system by calling {@link #addAccessibilityStateChangeListener}.
*/
public interface AccessibilityStateChangeListener {
/**
- * Called back on change in the accessibility state.
+ * Called when the accessibility enabled state changes.
*
* @param enabled Whether accessibility is enabled.
*/
- public void onAccessibilityStateChanged(boolean enabled);
+ void onAccessibilityStateChanged(boolean enabled);
}
/**
@@ -73,7 +181,24 @@ public final class AccessibilityManager {
*
* @param enabled Whether touch exploration is enabled.
*/
- public void onTouchExplorationStateChanged(boolean enabled);
+ void onTouchExplorationStateChanged(boolean enabled);
+ }
+
+ /**
+ * Listener for changes to the state of accessibility services. Changes include services being
+ * enabled or disabled, or changes to the {@link AccessibilityServiceInfo} of a running service.
+ * {@see #addAccessibilityServicesStateChangeListener}.
+ *
+ * @hide
+ */
+ public interface AccessibilityServicesStateChangeListener {
+
+ /**
+ * Called when the state of accessibility services changes.
+ *
+ * @param manager The manager that is calling back
+ */
+ void onAccessibilityServicesStateChanged(AccessibilityManager manager);
}
/**
@@ -81,6 +206,8 @@ public final class AccessibilityManager {
* the high text contrast state on the device, implement this interface and
* register it with the system by calling
* {@link #addHighTextContrastStateChangeListener}.
+ *
+ * @hide
*/
public interface HighTextContrastChangeListener {
@@ -89,7 +216,7 @@ public final class AccessibilityManager {
*
* @param enabled Whether high text contrast is enabled.
*/
- public void onHighTextContrastStateChanged(boolean enabled);
+ void onHighTextContrastStateChanged(boolean enabled);
}
/**
@@ -148,21 +275,67 @@ public final class AccessibilityManager {
private final IAccessibilityManagerClient.Stub mClient =
new IAccessibilityManagerClient.Stub() {
- public void setState(int state) {
- }
+ @Override
+ public void setState(int state) {
+ // We do not want to change this immediately as the application may
+ // have already checked that accessibility is on and fired an event,
+ // that is now propagating up the view tree, Hence, if accessibility
+ // is now off an exception will be thrown. We want to have the exception
+ // enforcement to guard against apps that fire unnecessary accessibility
+ // events when accessibility is off.
+ mHandler.obtainMessage(MyCallback.MSG_SET_STATE, state, 0).sendToTarget();
+ }
- public void notifyServicesStateChanged() {
+ @Override
+ public void notifyServicesStateChanged() {
+ final ArrayMap<AccessibilityServicesStateChangeListener, Handler> listeners;
+ synchronized (mLock) {
+ if (mServicesStateChangeListeners.isEmpty()) {
+ return;
}
+ listeners = new ArrayMap<>(mServicesStateChangeListeners);
+ }
- public void setRelevantEventTypes(int eventTypes) {
- }
- };
+ int numListeners = listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ final AccessibilityServicesStateChangeListener listener =
+ mServicesStateChangeListeners.keyAt(i);
+ mServicesStateChangeListeners.valueAt(i).post(() -> listener
+ .onAccessibilityServicesStateChanged(AccessibilityManager.this));
+ }
+ }
+
+ @Override
+ public void setRelevantEventTypes(int eventTypes) {
+ mRelevantEventTypes = eventTypes;
+ }
+ };
/**
* Get an AccessibilityManager instance (create one if necessary).
*
+ * @param context Context in which this manager operates.
+ *
+ * @hide
*/
public static AccessibilityManager getInstance(Context context) {
+ synchronized (sInstanceSync) {
+ if (sInstance == null) {
+ final int userId;
+ if (Binder.getCallingUid() == Process.SYSTEM_UID
+ || context.checkCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS)
+ == PackageManager.PERMISSION_GRANTED
+ || context.checkCallingOrSelfPermission(
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ == PackageManager.PERMISSION_GRANTED) {
+ userId = UserHandle.USER_CURRENT;
+ } else {
+ userId = context.getUserId();
+ }
+ sInstance = new AccessibilityManager(context, null, userId);
+ }
+ }
return sInstance;
}
@@ -170,21 +343,65 @@ public final class AccessibilityManager {
* Create an instance.
*
* @param context A {@link Context}.
+ * @param service An interface to the backing service.
+ * @param userId User id under which to run.
+ *
+ * @hide
*/
public AccessibilityManager(Context context, IAccessibilityManager service, int userId) {
+ // Constructor can't be chained because we can't create an instance of an inner class
+ // before calling another constructor.
+ mCallback = new MyCallback();
+ mHandler = new Handler(context.getMainLooper(), mCallback);
+ mUserId = userId;
+ synchronized (mLock) {
+ tryConnectToServiceLocked(service);
+ }
+ }
+
+ /**
+ * Create an instance.
+ *
+ * @param handler The handler to use
+ * @param service An interface to the backing service.
+ * @param userId User id under which to run.
+ *
+ * @hide
+ */
+ public AccessibilityManager(Handler handler, IAccessibilityManager service, int userId) {
+ mCallback = new MyCallback();
+ mHandler = handler;
+ mUserId = userId;
+ synchronized (mLock) {
+ tryConnectToServiceLocked(service);
+ }
}
+ /**
+ * @hide
+ */
public IAccessibilityManagerClient getClient() {
return mClient;
}
/**
- * Returns if the {@link AccessibilityManager} is enabled.
+ * @hide
+ */
+ @VisibleForTesting
+ public Handler.Callback getCallback() {
+ return mCallback;
+ }
+
+ /**
+ * Returns if the accessibility in the system is enabled.
*
- * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+ * @return True if accessibility is enabled, false otherwise.
*/
public boolean isEnabled() {
- return false;
+ synchronized (mLock) {
+ return mIsEnabled || (mAccessibilityPolicy != null
+ && mAccessibilityPolicy.isEnabled(mIsEnabled));
+ }
}
/**
@@ -193,7 +410,13 @@ public final class AccessibilityManager {
* @return True if touch exploration is enabled, false otherwise.
*/
public boolean isTouchExplorationEnabled() {
- return true;
+ synchronized (mLock) {
+ IAccessibilityManager service = getServiceLocked();
+ if (service == null) {
+ return false;
+ }
+ return mIsTouchExplorationEnabled;
+ }
}
/**
@@ -203,47 +426,188 @@ public final class AccessibilityManager {
* doing its own rendering and does not rely on the platform rendering pipeline.
* </p>
*
+ * @return True if high text contrast is enabled, false otherwise.
+ *
+ * @hide
*/
public boolean isHighTextContrastEnabled() {
- return false;
+ synchronized (mLock) {
+ IAccessibilityManager service = getServiceLocked();
+ if (service == null) {
+ return false;
+ }
+ return mIsHighTextContrastEnabled;
+ }
}
/**
* Sends an {@link AccessibilityEvent}.
- */
- public void sendAccessibilityEvent(AccessibilityEvent event) {
- }
-
- /**
- * Returns whether there are observers registered for this event type. If
- * this method returns false you shuold not generate events of this type
- * to conserve resources.
*
- * @param type The event type.
- * @return Whether the event is being observed.
+ * @param event The event to send.
+ *
+ * @throws IllegalStateException if accessibility is not enabled.
+ *
+ * <strong>Note:</strong> The preferred mechanism for sending custom accessibility
+ * events is through calling
+ * {@link android.view.ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)}
+ * instead of this method to allow predecessors to augment/filter events sent by
+ * their descendants.
*/
- public boolean isObservedEventType(@AccessibilityEvent.EventType int type) {
- return false;
+ public void sendAccessibilityEvent(AccessibilityEvent event) {
+ final IAccessibilityManager service;
+ final int userId;
+ final AccessibilityEvent dispatchedEvent;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ event.setEventTime(SystemClock.uptimeMillis());
+ if (mAccessibilityPolicy != null) {
+ dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event,
+ mIsEnabled, mRelevantEventTypes);
+ if (dispatchedEvent == null) {
+ return;
+ }
+ } else {
+ dispatchedEvent = event;
+ }
+ if (!isEnabled()) {
+ Looper myLooper = Looper.myLooper();
+ if (myLooper == Looper.getMainLooper()) {
+ throw new IllegalStateException(
+ "Accessibility off. Did you forget to check that?");
+ } else {
+ // If we're not running on the thread with the main looper, it's possible for
+ // the state of accessibility to change between checking isEnabled and
+ // calling this method. So just log the error rather than throwing the
+ // exception.
+ Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled");
+ return;
+ }
+ }
+ if ((dispatchedEvent.getEventType() & mRelevantEventTypes) == 0) {
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Not dispatching irrelevant event: " + dispatchedEvent
+ + " that is not among "
+ + AccessibilityEvent.eventTypeToString(mRelevantEventTypes));
+ }
+ return;
+ }
+ userId = mUserId;
+ }
+ try {
+ // it is possible that this manager is in the same process as the service but
+ // client using it is called through Binder from another process. Example: MMS
+ // app adds a SMS notification and the NotificationManagerService calls this method
+ long identityToken = Binder.clearCallingIdentity();
+ try {
+ service.sendAccessibilityEvent(dispatchedEvent, userId);
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ if (DEBUG) {
+ Log.i(LOG_TAG, dispatchedEvent + " sent");
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re);
+ } finally {
+ if (event != dispatchedEvent) {
+ event.recycle();
+ }
+ dispatchedEvent.recycle();
+ }
}
/**
- * Requests interruption of the accessibility feedback from all accessibility services.
+ * Requests feedback interruption from all accessibility services.
*/
public void interrupt() {
+ final IAccessibilityManager service;
+ final int userId;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ if (!isEnabled()) {
+ Looper myLooper = Looper.myLooper();
+ if (myLooper == Looper.getMainLooper()) {
+ throw new IllegalStateException(
+ "Accessibility off. Did you forget to check that?");
+ } else {
+ // If we're not running on the thread with the main looper, it's possible for
+ // the state of accessibility to change between checking isEnabled and
+ // calling this method. So just log the error rather than throwing the
+ // exception.
+ Log.e(LOG_TAG, "Interrupt called with accessibility disabled");
+ return;
+ }
+ }
+ userId = mUserId;
+ }
+ try {
+ service.interrupt(userId);
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Requested interrupt from all services");
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re);
+ }
}
/**
* Returns the {@link ServiceInfo}s of the installed accessibility services.
*
* @return An unmodifiable list with {@link ServiceInfo}s.
+ *
+ * @deprecated Use {@link #getInstalledAccessibilityServiceList()}
*/
@Deprecated
public List<ServiceInfo> getAccessibilityServiceList() {
- return Collections.emptyList();
+ List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList();
+ List<ServiceInfo> services = new ArrayList<>();
+ final int infoCount = infos.size();
+ for (int i = 0; i < infoCount; i++) {
+ AccessibilityServiceInfo info = infos.get(i);
+ services.add(info.getResolveInfo().serviceInfo);
+ }
+ return Collections.unmodifiableList(services);
}
+ /**
+ * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services.
+ *
+ * @return An unmodifiable list with {@link AccessibilityServiceInfo}s.
+ */
public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() {
- return Collections.emptyList();
+ final IAccessibilityManager service;
+ final int userId;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return Collections.emptyList();
+ }
+ userId = mUserId;
+ }
+
+ List<AccessibilityServiceInfo> services = null;
+ try {
+ services = service.getInstalledAccessibilityServiceList(userId);
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
+ }
+ if (mAccessibilityPolicy != null) {
+ services = mAccessibilityPolicy.getInstalledAccessibilityServiceList(services);
+ }
+ if (services != null) {
+ return Collections.unmodifiableList(services);
+ } else {
+ return Collections.emptyList();
+ }
}
/**
@@ -258,21 +622,52 @@ public final class AccessibilityManager {
* @see AccessibilityServiceInfo#FEEDBACK_HAPTIC
* @see AccessibilityServiceInfo#FEEDBACK_SPOKEN
* @see AccessibilityServiceInfo#FEEDBACK_VISUAL
+ * @see AccessibilityServiceInfo#FEEDBACK_BRAILLE
*/
public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(
int feedbackTypeFlags) {
- return Collections.emptyList();
+ final IAccessibilityManager service;
+ final int userId;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return Collections.emptyList();
+ }
+ userId = mUserId;
+ }
+
+ List<AccessibilityServiceInfo> services = null;
+ try {
+ services = service.getEnabledAccessibilityServiceList(feedbackTypeFlags, userId);
+ if (DEBUG) {
+ Log.i(LOG_TAG, "Installed AccessibilityServices " + services);
+ }
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re);
+ }
+ if (mAccessibilityPolicy != null) {
+ services = mAccessibilityPolicy.getEnabledAccessibilityServiceList(
+ feedbackTypeFlags, services);
+ }
+ if (services != null) {
+ return Collections.unmodifiableList(services);
+ } else {
+ return Collections.emptyList();
+ }
}
/**
* Registers an {@link AccessibilityStateChangeListener} for changes in
- * the global accessibility state of the system.
+ * the global accessibility state of the system. Equivalent to calling
+ * {@link #addAccessibilityStateChangeListener(AccessibilityStateChangeListener, Handler)}
+ * with a null handler.
*
* @param listener The listener.
- * @return True if successfully registered.
+ * @return Always returns {@code true}.
*/
public boolean addAccessibilityStateChangeListener(
- AccessibilityStateChangeListener listener) {
+ @NonNull AccessibilityStateChangeListener listener) {
+ addAccessibilityStateChangeListener(listener, null);
return true;
}
@@ -286,22 +681,40 @@ public final class AccessibilityManager {
* for a callback on the process's main handler.
*/
public void addAccessibilityStateChangeListener(
- @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {}
+ @NonNull AccessibilityStateChangeListener listener, @Nullable Handler handler) {
+ synchronized (mLock) {
+ mAccessibilityStateChangeListeners
+ .put(listener, (handler == null) ? mHandler : handler);
+ }
+ }
+ /**
+ * Unregisters an {@link AccessibilityStateChangeListener}.
+ *
+ * @param listener The listener.
+ * @return True if the listener was previously registered.
+ */
public boolean removeAccessibilityStateChangeListener(
- AccessibilityStateChangeListener listener) {
- return true;
+ @NonNull AccessibilityStateChangeListener listener) {
+ synchronized (mLock) {
+ int index = mAccessibilityStateChangeListeners.indexOfKey(listener);
+ mAccessibilityStateChangeListeners.remove(listener);
+ return (index >= 0);
+ }
}
/**
* Registers a {@link TouchExplorationStateChangeListener} for changes in
- * the global touch exploration state of the system.
+ * the global touch exploration state of the system. Equivalent to calling
+ * {@link #addTouchExplorationStateChangeListener(TouchExplorationStateChangeListener, Handler)}
+ * with a null handler.
*
* @param listener The listener.
- * @return True if successfully registered.
+ * @return Always returns {@code true}.
*/
public boolean addTouchExplorationStateChangeListener(
@NonNull TouchExplorationStateChangeListener listener) {
+ addTouchExplorationStateChangeListener(listener, null);
return true;
}
@@ -315,17 +728,104 @@ public final class AccessibilityManager {
* for a callback on the process's main handler.
*/
public void addTouchExplorationStateChangeListener(
- @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {}
+ @NonNull TouchExplorationStateChangeListener listener, @Nullable Handler handler) {
+ synchronized (mLock) {
+ mTouchExplorationStateChangeListeners
+ .put(listener, (handler == null) ? mHandler : handler);
+ }
+ }
/**
* Unregisters a {@link TouchExplorationStateChangeListener}.
*
* @param listener The listener.
- * @return True if successfully unregistered.
+ * @return True if listener was previously registered.
*/
public boolean removeTouchExplorationStateChangeListener(
@NonNull TouchExplorationStateChangeListener listener) {
- return true;
+ synchronized (mLock) {
+ int index = mTouchExplorationStateChangeListeners.indexOfKey(listener);
+ mTouchExplorationStateChangeListeners.remove(listener);
+ return (index >= 0);
+ }
+ }
+
+ /**
+ * Registers a {@link AccessibilityServicesStateChangeListener}.
+ *
+ * @param listener The listener.
+ * @param handler The handler on which the listener should be called back, or {@code null}
+ * for a callback on the process's main handler.
+ * @hide
+ */
+ public void addAccessibilityServicesStateChangeListener(
+ @NonNull AccessibilityServicesStateChangeListener listener, @Nullable Handler handler) {
+ synchronized (mLock) {
+ mServicesStateChangeListeners
+ .put(listener, (handler == null) ? mHandler : handler);
+ }
+ }
+
+ /**
+ * Unregisters a {@link AccessibilityServicesStateChangeListener}.
+ *
+ * @param listener The listener.
+ *
+ * @hide
+ */
+ public void removeAccessibilityServicesStateChangeListener(
+ @NonNull AccessibilityServicesStateChangeListener listener) {
+ synchronized (mLock) {
+ mServicesStateChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Registers a {@link AccessibilityRequestPreparer}.
+ */
+ public void addAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
+ if (mRequestPreparerLists == null) {
+ mRequestPreparerLists = new SparseArray<>(1);
+ }
+ int id = preparer.getView().getAccessibilityViewId();
+ List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(id);
+ if (requestPreparerList == null) {
+ requestPreparerList = new ArrayList<>(1);
+ mRequestPreparerLists.put(id, requestPreparerList);
+ }
+ requestPreparerList.add(preparer);
+ }
+
+ /**
+ * Unregisters a {@link AccessibilityRequestPreparer}.
+ */
+ public void removeAccessibilityRequestPreparer(AccessibilityRequestPreparer preparer) {
+ if (mRequestPreparerLists == null) {
+ return;
+ }
+ int viewId = preparer.getView().getAccessibilityViewId();
+ List<AccessibilityRequestPreparer> requestPreparerList = mRequestPreparerLists.get(viewId);
+ if (requestPreparerList != null) {
+ requestPreparerList.remove(preparer);
+ if (requestPreparerList.isEmpty()) {
+ mRequestPreparerLists.remove(viewId);
+ }
+ }
+ }
+
+ /**
+ * Get the preparers that are registered for an accessibility ID
+ *
+ * @param id The ID of interest
+ * @return The list of preparers, or {@code null} if there are none.
+ *
+ * @hide
+ */
+ public List<AccessibilityRequestPreparer> getRequestPreparersForAccessibilityId(int id) {
+ if (mRequestPreparerLists == null) {
+ return null;
+ }
+ return mRequestPreparerLists.get(id);
}
/**
@@ -337,7 +837,12 @@ public final class AccessibilityManager {
* @hide
*/
public void addHighTextContrastStateChangeListener(
- @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {}
+ @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {
+ synchronized (mLock) {
+ mHighTextContrastStateChangeListeners
+ .put(listener, (handler == null) ? mHandler : handler);
+ }
+ }
/**
* Unregisters a {@link HighTextContrastChangeListener}.
@@ -347,7 +852,64 @@ public final class AccessibilityManager {
* @hide
*/
public void removeHighTextContrastStateChangeListener(
- @NonNull HighTextContrastChangeListener listener) {}
+ @NonNull HighTextContrastChangeListener listener) {
+ synchronized (mLock) {
+ mHighTextContrastStateChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sets the {@link AccessibilityPolicy} controlling this manager.
+ *
+ * @param policy The policy.
+ *
+ * @hide
+ */
+ public void setAccessibilityPolicy(@Nullable AccessibilityPolicy policy) {
+ synchronized (mLock) {
+ mAccessibilityPolicy = policy;
+ }
+ }
+
+ /**
+ * Check if the accessibility volume stream is active.
+ *
+ * @return True if accessibility volume is active (i.e. some service has requested it). False
+ * otherwise.
+ * @hide
+ */
+ public boolean isAccessibilityVolumeStreamActive() {
+ List<AccessibilityServiceInfo> serviceInfos =
+ getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+ for (int i = 0; i < serviceInfos.size(); i++) {
+ if ((serviceInfos.get(i).flags & FLAG_ENABLE_ACCESSIBILITY_VOLUME) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Report a fingerprint gesture to accessibility. Only available for the system process.
+ *
+ * @param keyCode The key code of the gesture
+ * @return {@code true} if accessibility consumes the event. {@code false} if not.
+ * @hide
+ */
+ public boolean sendFingerprintGesture(int keyCode) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return false;
+ }
+ }
+ try {
+ return service.sendFingerprintGesture(keyCode);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
/**
* Sets the current state and notifies listeners, if necessary.
@@ -355,14 +917,312 @@ public final class AccessibilityManager {
* @param stateFlags The state flags.
*/
private void setStateLocked(int stateFlags) {
+ final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;
+ final boolean touchExplorationEnabled =
+ (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
+ final boolean highTextContrastEnabled =
+ (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
+
+ final boolean wasEnabled = isEnabled();
+ final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
+ final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
+
+ // Ensure listeners get current state from isZzzEnabled() calls.
+ mIsEnabled = enabled;
+ mIsTouchExplorationEnabled = touchExplorationEnabled;
+ mIsHighTextContrastEnabled = highTextContrastEnabled;
+
+ if (wasEnabled != isEnabled()) {
+ notifyAccessibilityStateChanged();
+ }
+
+ if (wasTouchExplorationEnabled != touchExplorationEnabled) {
+ notifyTouchExplorationStateChanged();
+ }
+
+ if (wasHighTextContrastEnabled != highTextContrastEnabled) {
+ notifyHighTextContrastStateChanged();
+ }
+ }
+
+ /**
+ * Find an installed service with the specified {@link ComponentName}.
+ *
+ * @param componentName The name to match to the service.
+ *
+ * @return The info corresponding to the installed service, or {@code null} if no such service
+ * is installed.
+ * @hide
+ */
+ public AccessibilityServiceInfo getInstalledServiceInfoWithComponentName(
+ ComponentName componentName) {
+ final List<AccessibilityServiceInfo> installedServiceInfos =
+ getInstalledAccessibilityServiceList();
+ if ((installedServiceInfos == null) || (componentName == null)) {
+ return null;
+ }
+ for (int i = 0; i < installedServiceInfos.size(); i++) {
+ if (componentName.equals(installedServiceInfos.get(i).getComponentName())) {
+ return installedServiceInfos.get(i);
+ }
+ }
+ return null;
}
+ /**
+ * Adds an accessibility interaction connection interface for a given window.
+ * @param windowToken The window token to which a connection is added.
+ * @param connection The connection.
+ *
+ * @hide
+ */
public int addAccessibilityInteractionConnection(IWindow windowToken,
- IAccessibilityInteractionConnection connection) {
+ String packageName, IAccessibilityInteractionConnection connection) {
+ final IAccessibilityManager service;
+ final int userId;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return View.NO_ID;
+ }
+ userId = mUserId;
+ }
+ try {
+ return service.addAccessibilityInteractionConnection(windowToken, connection,
+ packageName, userId);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re);
+ }
return View.NO_ID;
}
+ /**
+ * Removed an accessibility interaction connection interface for a given window.
+ * @param windowToken The window token to which a connection is removed.
+ *
+ * @hide
+ */
public void removeAccessibilityInteractionConnection(IWindow windowToken) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.removeAccessibilityInteractionConnection(windowToken);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re);
+ }
}
+ /**
+ * Perform the accessibility shortcut if the caller has permission.
+ *
+ * @hide
+ */
+ public void performAccessibilityShortcut() {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.performAccessibilityShortcut();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error performing accessibility shortcut. ", re);
+ }
+ }
+
+ /**
+ * Notifies that the accessibility button in the system's navigation area has been clicked
+ *
+ * @hide
+ */
+ public void notifyAccessibilityButtonClicked() {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.notifyAccessibilityButtonClicked();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while dispatching accessibility button click", re);
+ }
+ }
+
+ /**
+ * Notifies that the visibility of the accessibility button in the system's navigation area
+ * has changed.
+ *
+ * @param shown {@code true} if the accessibility button is visible within the system
+ * navigation area, {@code false} otherwise
+ * @hide
+ */
+ public void notifyAccessibilityButtonVisibilityChanged(boolean shown) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.notifyAccessibilityButtonVisibilityChanged(shown);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error while dispatching accessibility button visibility change", re);
+ }
+ }
+
+ /**
+ * Set an IAccessibilityInteractionConnection to replace the actions of a picture-in-picture
+ * window. Intended for use by the System UI only.
+ *
+ * @param connection The connection to handle the actions. Set to {@code null} to avoid
+ * affecting the actions.
+ *
+ * @hide
+ */
+ public void setPictureInPictureActionReplacingConnection(
+ @Nullable IAccessibilityInteractionConnection connection) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return;
+ }
+ }
+ try {
+ service.setPictureInPictureActionReplacingConnection(connection);
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error setting picture in picture action replacement", re);
+ }
+ }
+
+ private IAccessibilityManager getServiceLocked() {
+ if (mService == null) {
+ tryConnectToServiceLocked(null);
+ }
+ return mService;
+ }
+
+ private void tryConnectToServiceLocked(IAccessibilityManager service) {
+ if (service == null) {
+ IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
+ if (iBinder == null) {
+ return;
+ }
+ service = IAccessibilityManager.Stub.asInterface(iBinder);
+ }
+
+ try {
+ final long userStateAndRelevantEvents = service.addClient(mClient, mUserId);
+ setStateLocked(IntPair.first(userStateAndRelevantEvents));
+ mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents);
+ mService = service;
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "AccessibilityManagerService is dead", re);
+ }
+ }
+
+ /**
+ * Notifies the registered {@link AccessibilityStateChangeListener}s.
+ */
+ private void notifyAccessibilityStateChanged() {
+ final boolean isEnabled;
+ final ArrayMap<AccessibilityStateChangeListener, Handler> listeners;
+ synchronized (mLock) {
+ if (mAccessibilityStateChangeListeners.isEmpty()) {
+ return;
+ }
+ isEnabled = isEnabled();
+ listeners = new ArrayMap<>(mAccessibilityStateChangeListeners);
+ }
+
+ final int numListeners = listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ final AccessibilityStateChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).post(() ->
+ listener.onAccessibilityStateChanged(isEnabled));
+ }
+ }
+
+ /**
+ * Notifies the registered {@link TouchExplorationStateChangeListener}s.
+ */
+ private void notifyTouchExplorationStateChanged() {
+ final boolean isTouchExplorationEnabled;
+ final ArrayMap<TouchExplorationStateChangeListener, Handler> listeners;
+ synchronized (mLock) {
+ if (mTouchExplorationStateChangeListeners.isEmpty()) {
+ return;
+ }
+ isTouchExplorationEnabled = mIsTouchExplorationEnabled;
+ listeners = new ArrayMap<>(mTouchExplorationStateChangeListeners);
+ }
+
+ final int numListeners = listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ final TouchExplorationStateChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).post(() ->
+ listener.onTouchExplorationStateChanged(isTouchExplorationEnabled));
+ }
+ }
+
+ /**
+ * Notifies the registered {@link HighTextContrastChangeListener}s.
+ */
+ private void notifyHighTextContrastStateChanged() {
+ final boolean isHighTextContrastEnabled;
+ final ArrayMap<HighTextContrastChangeListener, Handler> listeners;
+ synchronized (mLock) {
+ if (mHighTextContrastStateChangeListeners.isEmpty()) {
+ return;
+ }
+ isHighTextContrastEnabled = mIsHighTextContrastEnabled;
+ listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners);
+ }
+
+ final int numListeners = listeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ final HighTextContrastChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).post(() ->
+ listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
+ }
+ }
+
+ /**
+ * Determines if the accessibility button within the system navigation area is supported.
+ *
+ * @return {@code true} if the accessibility button is supported on this device,
+ * {@code false} otherwise
+ */
+ public static boolean isAccessibilityButtonSupported() {
+ final Resources res = Resources.getSystem();
+ return res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
+ }
+
+ private final class MyCallback implements Handler.Callback {
+ public static final int MSG_SET_STATE = 1;
+
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SET_STATE: {
+ // See comment at mClient
+ final int state = message.arg1;
+ synchronized (mLock) {
+ setStateLocked(state);
+ }
+ } break;
+ }
+ return true;
+ }
+ }
}
diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java
index 4c437dd4..03f1c124 100644
--- a/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/android/view/accessibility/AccessibilityNodeInfo.java
@@ -3874,6 +3874,24 @@ public class AccessibilityNodeInfo implements Parcelable {
| FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null);
}
+ /** @hide */
+ public static String idToString(long accessibilityId) {
+ int accessibilityViewId = getAccessibilityViewId(accessibilityId);
+ int virtualDescendantId = getVirtualDescendantId(accessibilityId);
+ return virtualDescendantId == AccessibilityNodeProvider.HOST_VIEW_ID
+ ? idItemToString(accessibilityViewId)
+ : idItemToString(accessibilityViewId) + ":" + idItemToString(virtualDescendantId);
+ }
+
+ private static String idItemToString(int item) {
+ switch (item) {
+ case ROOT_ITEM_ID: return "ROOT";
+ case UNDEFINED_ITEM_ID: return "UNDEFINED";
+ case AccessibilityNodeProvider.HOST_VIEW_ID: return "HOST";
+ default: return "" + item;
+ }
+ }
+
/**
* A class defining an action that can be performed on an {@link AccessibilityNodeInfo}.
* Each action has a unique id that is mandatory and optional data.
diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java
index 1da998d0..a6495d15 100644
--- a/android/view/autofill/AutofillPopupWindow.java
+++ b/android/view/autofill/AutofillPopupWindow.java
@@ -79,6 +79,11 @@ public class AutofillPopupWindow extends PopupWindow {
public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) {
mWindowPresenter = new WindowPresenter(presenter);
+ // We want to show the window as system controlled one so it covers app windows, but it has
+ // to be an application type (so it's contained inside the application area).
+ // Hence, we set it to the application type with the highest z-order, which currently
+ // is TYPE_APPLICATION_ABOVE_SUB_PANEL.
+ setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
setTouchModal(false);
setOutsideTouchable(true);
setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
diff --git a/android/view/inputmethod/BaseInputConnection.java b/android/view/inputmethod/BaseInputConnection.java
index 5f7a0f78..090e19f9 100644
--- a/android/view/inputmethod/BaseInputConnection.java
+++ b/android/view/inputmethod/BaseInputConnection.java
@@ -522,7 +522,7 @@ public class BaseInputConnection implements InputConnection {
b = tmp;
}
- if (a == b) return null;
+ if (a == b || a < 0) return null;
if ((flags&GET_TEXT_WITH_STYLES) != 0) {
return content.subSequence(a, b);
diff --git a/android/view/textclassifier/GenerateLinksLogger.java b/android/view/textclassifier/GenerateLinksLogger.java
index 73cf43b8..067513f1 100644
--- a/android/view/textclassifier/GenerateLinksLogger.java
+++ b/android/view/textclassifier/GenerateLinksLogger.java
@@ -19,13 +19,13 @@ package android.view.textclassifier;
import android.annotation.Nullable;
import android.metrics.LogMaker;
import android.util.ArrayMap;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.Preconditions;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
@@ -39,6 +39,7 @@ import java.util.UUID;
public final class GenerateLinksLogger {
private static final String LOG_TAG = "GenerateLinksLogger";
+ private static final boolean DEBUG_LOG_ENABLED = false;
private static final String ZERO = "0";
private final MetricsLogger mMetricsLogger;
@@ -127,7 +128,7 @@ public final class GenerateLinksLogger {
}
private static void debugLog(LogMaker log) {
- if (!Logger.DEBUG_LOG_ENABLED) return;
+ if (!DEBUG_LOG_ENABLED) return;
final String callId = Objects.toString(
log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), "");
@@ -142,8 +143,9 @@ public final class GenerateLinksLogger {
final int latencyMs = Integer.parseInt(
Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO));
- Log.d(LOG_TAG, String.format("%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
- numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
+ Log.d(LOG_TAG,
+ String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
+ numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
}
/** Helper class for storing per-entity type statistics. */
diff --git a/android/view/textclassifier/Logger.java b/android/view/textclassifier/Logger.java
deleted file mode 100644
index f03906a0..00000000
--- a/android/view/textclassifier/Logger.java
+++ /dev/null
@@ -1,397 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.view.textclassifier;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-
-import com.android.internal.util.Preconditions;
-
-import java.text.BreakIterator;
-import java.util.Locale;
-import java.util.Objects;
-
-/**
- * A helper for logging TextClassifier related events.
- * @hide
- */
-public abstract class Logger {
-
- private static final String LOG_TAG = "Logger";
- /* package */ static final boolean DEBUG_LOG_ENABLED = true;
-
- private @SelectionEvent.InvocationMethod int mInvocationMethod;
- private SelectionEvent mPrevEvent;
- private SelectionEvent mSmartEvent;
- private SelectionEvent mStartEvent;
-
- /**
- * Logger that does not log anything.
- * @hide
- */
- public static final Logger DISABLED = new Logger() {
- @Override
- public void writeEvent(SelectionEvent event) {}
- };
-
- @Nullable
- private final Config mConfig;
-
- public Logger(Config config) {
- mConfig = Preconditions.checkNotNull(config);
- }
-
- private Logger() {
- mConfig = null;
- }
-
- /**
- * Writes the selection event to a log.
- */
- public abstract void writeEvent(@NonNull SelectionEvent event);
-
- /**
- * Returns true if the resultId matches that of a smart selection event (i.e.
- * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or
- * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}).
- * Returns false otherwise.
- */
- public boolean isSmartSelection(@NonNull String resultId) {
- return false;
- }
-
- /**
- * Returns a token iterator for tokenizing text for logging purposes.
- */
- public BreakIterator getTokenIterator(@NonNull Locale locale) {
- return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale));
- }
-
- /**
- * Logs a "selection started" event.
- *
- * @param invocationMethod the way the selection was triggered
- * @param start the token index of the selected token
- */
- public final void logSelectionStartedEvent(
- @SelectionEvent.InvocationMethod int invocationMethod, int start) {
- if (mConfig == null) {
- return;
- }
-
- mInvocationMethod = invocationMethod;
- logEvent(new SelectionEvent(
- start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
- TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig));
- }
-
- /**
- * Logs a "selection modified" event.
- * Use when the user modifies the selection.
- *
- * @param start the start token (inclusive) index of the selection
- * @param end the end token (exclusive) index of the selection
- */
- public final void logSelectionModifiedEvent(int start, int end) {
- Preconditions.checkArgument(end >= start, "end cannot be less than start");
-
- if (mConfig == null) {
- return;
- }
-
- logEvent(new SelectionEvent(
- start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
- TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig));
- }
-
- /**
- * Logs a "selection modified" event.
- * Use when the user modifies the selection and the selection's entity type is known.
- *
- * @param start the start token (inclusive) index of the selection
- * @param end the end token (exclusive) index of the selection
- * @param classification the TextClassification object returned by the TextClassifier that
- * classified the selected text
- */
- public final void logSelectionModifiedEvent(
- int start, int end, @NonNull TextClassification classification) {
- Preconditions.checkArgument(end >= start, "end cannot be less than start");
- Preconditions.checkNotNull(classification);
-
- if (mConfig == null) {
- return;
- }
-
- final String entityType = classification.getEntityCount() > 0
- ? classification.getEntity(0)
- : TextClassifier.TYPE_UNKNOWN;
- logEvent(new SelectionEvent(
- start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
- entityType, mInvocationMethod, classification.getId(), mConfig));
- }
-
- /**
- * Logs a "selection modified" event.
- * Use when a TextClassifier modifies the selection.
- *
- * @param start the start token (inclusive) index of the selection
- * @param end the end token (exclusive) index of the selection
- * @param selection the TextSelection object returned by the TextClassifier for the
- * specified selection
- */
- public final void logSelectionModifiedEvent(
- int start, int end, @NonNull TextSelection selection) {
- Preconditions.checkArgument(end >= start, "end cannot be less than start");
- Preconditions.checkNotNull(selection);
-
- if (mConfig == null) {
- return;
- }
-
- final int eventType;
- if (isSmartSelection(selection.getId())) {
- eventType = end - start > 1
- ? SelectionEvent.EVENT_SMART_SELECTION_MULTI
- : SelectionEvent.EVENT_SMART_SELECTION_SINGLE;
-
- } else {
- eventType = SelectionEvent.EVENT_AUTO_SELECTION;
- }
- final String entityType = selection.getEntityCount() > 0
- ? selection.getEntity(0)
- : TextClassifier.TYPE_UNKNOWN;
- logEvent(new SelectionEvent(start, end, eventType, entityType, mInvocationMethod,
- selection.getId(), mConfig));
- }
-
- /**
- * Logs an event specifying an action taken on a selection.
- * Use when the user clicks on an action to act on the selected text.
- *
- * @param start the start token (inclusive) index of the selection
- * @param end the end token (exclusive) index of the selection
- * @param actionType the action that was performed on the selection
- */
- public final void logSelectionActionEvent(
- int start, int end, @SelectionEvent.ActionType int actionType) {
- Preconditions.checkArgument(end >= start, "end cannot be less than start");
- checkActionType(actionType);
-
- if (mConfig == null) {
- return;
- }
-
- logEvent(new SelectionEvent(
- start, end, actionType, TextClassifier.TYPE_UNKNOWN, mInvocationMethod,
- null, mConfig));
- }
-
- /**
- * Logs an event specifying an action taken on a selection.
- * Use when the user clicks on an action to act on the selected text and the selection's
- * entity type is known.
- *
- * @param start the start token (inclusive) index of the selection
- * @param end the end token (exclusive) index of the selection
- * @param actionType the action that was performed on the selection
- * @param classification the TextClassification object returned by the TextClassifier that
- * classified the selected text
- *
- * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
- */
- public final void logSelectionActionEvent(
- int start, int end, @SelectionEvent.ActionType int actionType,
- @NonNull TextClassification classification) {
- Preconditions.checkArgument(end >= start, "end cannot be less than start");
- Preconditions.checkNotNull(classification);
- checkActionType(actionType);
-
- if (mConfig == null) {
- return;
- }
-
- final String entityType = classification.getEntityCount() > 0
- ? classification.getEntity(0)
- : TextClassifier.TYPE_UNKNOWN;
- logEvent(new SelectionEvent(start, end, actionType, entityType, mInvocationMethod,
- classification.getId(), mConfig));
- }
-
- private void logEvent(@NonNull SelectionEvent event) {
- Preconditions.checkNotNull(event);
-
- if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED
- && mStartEvent == null) {
- if (DEBUG_LOG_ENABLED) {
- Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
- }
- return;
- }
-
- final long now = System.currentTimeMillis();
- switch (event.getEventType()) {
- case SelectionEvent.EVENT_SELECTION_STARTED:
- Preconditions.checkArgument(event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
- event.setSessionId(startNewSession());
- mStartEvent = event;
- break;
- case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through
- case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
- mSmartEvent = event;
- break;
- case SelectionEvent.EVENT_SELECTION_MODIFIED: // fall through
- case SelectionEvent.EVENT_AUTO_SELECTION:
- if (mPrevEvent != null
- && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart()
- && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) {
- // Selection did not change. Ignore event.
- return;
- }
- break;
- default:
- // do nothing.
- }
-
- event.setEventTime(now);
- if (mStartEvent != null) {
- event.setSessionId(mStartEvent.getSessionId())
- .setDurationSinceSessionStart(now - mStartEvent.getEventTime())
- .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
- .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
- }
- if (mSmartEvent != null) {
- event.setResultId(mSmartEvent.getResultId())
- .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
- .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
- }
- if (mPrevEvent != null) {
- event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime())
- .setEventIndex(mPrevEvent.getEventIndex() + 1);
- }
- writeEvent(event);
- mPrevEvent = event;
-
- if (event.isTerminal()) {
- endSession();
- }
- }
-
- private TextClassificationSessionId startNewSession() {
- endSession();
- return new TextClassificationSessionId();
- }
-
- private void endSession() {
- mPrevEvent = null;
- mSmartEvent = null;
- mStartEvent = null;
- }
-
- /**
- * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
- */
- private static void checkActionType(@SelectionEvent.EventType int eventType)
- throws IllegalArgumentException {
- switch (eventType) {
- case SelectionEvent.ACTION_OVERTYPE: // fall through
- case SelectionEvent.ACTION_COPY: // fall through
- case SelectionEvent.ACTION_PASTE: // fall through
- case SelectionEvent.ACTION_CUT: // fall through
- case SelectionEvent.ACTION_SHARE: // fall through
- case SelectionEvent.ACTION_SMART_SHARE: // fall through
- case SelectionEvent.ACTION_DRAG: // fall through
- case SelectionEvent.ACTION_ABANDON: // fall through
- case SelectionEvent.ACTION_SELECT_ALL: // fall through
- case SelectionEvent.ACTION_RESET: // fall through
- return;
- default:
- throw new IllegalArgumentException(
- String.format(Locale.US, "%d is not an eventType", eventType));
- }
- }
-
-
- /**
- * A Logger config.
- */
- public static final class Config {
-
- private final String mPackageName;
- private final String mWidgetType;
- @Nullable private final String mWidgetVersion;
-
- /**
- * @param context Context of the widget the logger logs for
- * @param widgetType a name for the widget being logged for. e.g.
- * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}
- * @param widgetVersion a string version info for the widget the logger logs for
- */
- public Config(
- @NonNull Context context,
- @TextClassifier.WidgetType String widgetType,
- @Nullable String widgetVersion) {
- mPackageName = Preconditions.checkNotNull(context).getPackageName();
- mWidgetType = widgetType;
- mWidgetVersion = widgetVersion;
- }
-
- /**
- * Returns the package name of the application the logger logs for.
- */
- public String getPackageName() {
- return mPackageName;
- }
-
- /**
- * Returns the name for the widget being logged for. e.g.
- * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}.
- */
- public String getWidgetType() {
- return mWidgetType;
- }
-
- /**
- * Returns string version info for the logger. This is specific to the text classifier.
- */
- @Nullable
- public String getWidgetVersion() {
- return mWidgetVersion;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mPackageName, mWidgetType, mWidgetVersion);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == this) {
- return true;
- }
-
- if (!(obj instanceof Config)) {
- return false;
- }
-
- final Config other = (Config) obj;
- return Objects.equals(mPackageName, other.mPackageName)
- && Objects.equals(mWidgetType, other.mWidgetType)
- && Objects.equals(mWidgetVersion, other.mWidgetType);
- }
- }
-}
diff --git a/android/view/textclassifier/SelectionEvent.java b/android/view/textclassifier/SelectionEvent.java
index 1e978ccf..b0735969 100644
--- a/android/view/textclassifier/SelectionEvent.java
+++ b/android/view/textclassifier/SelectionEvent.java
@@ -150,20 +150,6 @@ public final class SelectionEvent implements Parcelable {
mInvocationMethod = invocationMethod;
}
- SelectionEvent(
- int start, int end,
- @EventType int eventType, @EntityType String entityType,
- @InvocationMethod int invocationMethod, @Nullable String resultId,
- Logger.Config config) {
- this(start, end, eventType, entityType, invocationMethod, resultId);
- Preconditions.checkNotNull(config);
- setTextClassificationSessionContext(
- new TextClassificationContext.Builder(
- config.getPackageName(), config.getWidgetType())
- .setWidgetVersion(config.getWidgetVersion())
- .build());
- }
-
private SelectionEvent(Parcel in) {
mAbsoluteStart = in.readInt();
mAbsoluteEnd = in.readInt();
@@ -362,6 +348,7 @@ public final class SelectionEvent implements Parcelable {
case SelectionEvent.ACTION_ABANDON: // fall through
case SelectionEvent.ACTION_SELECT_ALL: // fall through
case SelectionEvent.ACTION_RESET: // fall through
+ case SelectionEvent.ACTION_OTHER: // fall through
return;
default:
throw new IllegalArgumentException(
@@ -667,4 +654,4 @@ public final class SelectionEvent implements Parcelable {
return new SelectionEvent[size];
}
};
-} \ No newline at end of file
+}
diff --git a/android/view/textclassifier/DefaultLogger.java b/android/view/textclassifier/SelectionSessionLogger.java
index 203ca560..f2fb63eb 100644
--- a/android/view/textclassifier/DefaultLogger.java
+++ b/android/view/textclassifier/SelectionSessionLogger.java
@@ -17,28 +17,29 @@
package android.view.textclassifier;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.metrics.LogMaker;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.Preconditions;
+import java.text.BreakIterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.StringJoiner;
/**
- * Default Logger.
- * Used internally by TextClassifierImpl.
+ * A helper for logging selection session events.
* @hide
*/
-public final class DefaultLogger extends Logger {
+public final class SelectionSessionLogger {
- private static final String LOG_TAG = "DefaultLogger";
+ private static final String LOG_TAG = "SelectionSessionLogger";
+ private static final boolean DEBUG_LOG_ENABLED = false;
static final String CLASSIFIER_ID = "androidtc";
private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
@@ -59,23 +60,16 @@ public final class DefaultLogger extends Logger {
private final MetricsLogger mMetricsLogger;
- public DefaultLogger(@NonNull Config config) {
- super(config);
+ public SelectionSessionLogger() {
mMetricsLogger = new MetricsLogger();
}
@VisibleForTesting
- public DefaultLogger(@NonNull Config config, @NonNull MetricsLogger metricsLogger) {
- super(config);
+ public SelectionSessionLogger(@NonNull MetricsLogger metricsLogger) {
mMetricsLogger = Preconditions.checkNotNull(metricsLogger);
}
- @Override
- public boolean isSmartSelection(@NonNull String signature) {
- return CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
- }
-
- @Override
+ /** Emits a selection event to the logs. */
public void writeEvent(@NonNull SelectionEvent event) {
Preconditions.checkNotNull(event);
final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
@@ -93,7 +87,7 @@ public final class DefaultLogger extends Logger {
.addTaggedData(SMART_END, event.getSmartEnd())
.addTaggedData(EVENT_START, event.getStart())
.addTaggedData(EVENT_END, event.getEnd())
- .addTaggedData(SESSION_ID, event.getSessionId());
+ .addTaggedData(SESSION_ID, event.getSessionId().flattenToString());
mMetricsLogger.write(log);
debugLog(log);
}
@@ -225,9 +219,17 @@ public final class DefaultLogger extends Logger {
final int eventEnd = Integer.parseInt(
Objects.toString(log.getTaggedData(EVENT_END), ZERO));
- Log.d(LOG_TAG, String.format("%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
- index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd, widget,
- model));
+ Log.d(LOG_TAG,
+ String.format(Locale.US, "%2d: %s/%s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
+ index, type, subType, entity, eventStart, eventEnd, smartStart, smartEnd,
+ widget, model));
+ }
+
+ /**
+ * Returns a token iterator for tokenizing text for logging purposes.
+ */
+ public static BreakIterator getTokenIterator(@NonNull Locale locale) {
+ return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale));
}
/**
@@ -260,8 +262,10 @@ public final class DefaultLogger extends Logger {
return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash);
}
- static String getClassifierId(String signature) {
- Preconditions.checkNotNull(signature);
+ static String getClassifierId(@Nullable String signature) {
+ if (signature == null) {
+ return "";
+ }
final int end = signature.indexOf("|");
if (end >= 0) {
return signature.substring(0, end);
@@ -269,8 +273,10 @@ public final class DefaultLogger extends Logger {
return "";
}
- static String getModelName(String signature) {
- Preconditions.checkNotNull(signature);
+ static String getModelName(@Nullable String signature) {
+ if (signature == null) {
+ return "";
+ }
final int start = signature.indexOf("|") + 1;
final int end = signature.indexOf("|", start);
if (start >= 1 && end >= start) {
@@ -279,8 +285,10 @@ public final class DefaultLogger extends Logger {
return "";
}
- static int getHash(String signature) {
- Preconditions.checkNotNull(signature);
+ static int getHash(@Nullable String signature) {
+ if (signature == null) {
+ return 0;
+ }
final int index1 = signature.indexOf("|");
final int index2 = signature.indexOf("|", index1);
if (index2 > 0) {
diff --git a/android/view/textclassifier/SystemTextClassifier.java b/android/view/textclassifier/SystemTextClassifier.java
index 45fd6bfb..490c3890 100644
--- a/android/view/textclassifier/SystemTextClassifier.java
+++ b/android/view/textclassifier/SystemTextClassifier.java
@@ -28,7 +28,6 @@ import android.service.textclassifier.ITextClassifierService;
import android.service.textclassifier.ITextLinksCallback;
import android.service.textclassifier.ITextSelectionCallback;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.internal.util.Preconditions;
@@ -49,13 +48,6 @@ public final class SystemTextClassifier implements TextClassifier {
private final TextClassificationConstants mSettings;
private final TextClassifier mFallback;
private final String mPackageName;
-
- private final Object mLoggerLock = new Object();
- @GuardedBy("mLoggerLock")
- private Logger.Config mLoggerConfig;
- @GuardedBy("mLoggerLock")
- private Logger mLogger;
- @GuardedBy("mLoggerLock")
private TextClassificationSessionId mSessionId;
public SystemTextClassifier(Context context, TextClassificationConstants settings)
@@ -147,27 +139,6 @@ public final class SystemTextClassifier implements TextClassifier {
}
@Override
- public Logger getLogger(@NonNull Logger.Config config) {
- Preconditions.checkNotNull(config);
- synchronized (mLoggerLock) {
- if (mLogger == null || !config.equals(mLoggerConfig)) {
- mLoggerConfig = config;
- mLogger = new Logger(config) {
- @Override
- public void writeEvent(SelectionEvent event) {
- try {
- mManagerService.onSelectionEvent(mSessionId, event);
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "Error reporting selection event.", e);
- }
- }
- };
- }
- }
- return mLogger;
- }
-
- @Override
public void destroy() {
try {
if (mSessionId != null) {
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
index 37a5d9a1..96016b44 100644
--- a/android/view/textclassifier/TextClassification.java
+++ b/android/view/textclassifier/TextClassification.java
@@ -375,13 +375,13 @@ public final class TextClassification implements Parcelable {
*/
public static final class Builder {
- @NonNull private String mText;
@NonNull private List<RemoteAction> mActions = new ArrayList<>();
@NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
- @Nullable Drawable mLegacyIcon;
- @Nullable String mLegacyLabel;
- @Nullable Intent mLegacyIntent;
- @Nullable OnClickListener mLegacyOnClickListener;
+ @Nullable private String mText;
+ @Nullable private Drawable mLegacyIcon;
+ @Nullable private String mLegacyLabel;
+ @Nullable private Intent mLegacyIntent;
+ @Nullable private OnClickListener mLegacyOnClickListener;
@Nullable private String mId;
/**
@@ -721,4 +721,67 @@ public final class TextClassification implements Parcelable {
mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
mId = in.readString();
}
+
+ // TODO: Remove once apps can build against the latest sdk.
+ /**
+ * Optional input parameters for generating TextClassification.
+ * @hide
+ */
+ public static final class Options {
+
+ @Nullable private final TextClassificationSessionId mSessionId;
+ @Nullable private final Request mRequest;
+ @Nullable private LocaleList mDefaultLocales;
+ @Nullable private ZonedDateTime mReferenceTime;
+
+ public Options() {
+ this(null, null);
+ }
+
+ private Options(
+ @Nullable TextClassificationSessionId sessionId, @Nullable Request request) {
+ mSessionId = sessionId;
+ mRequest = request;
+ }
+
+ /** Helper to create Options from a Request. */
+ public static Options from(TextClassificationSessionId sessionId, Request request) {
+ final Options options = new Options(sessionId, request);
+ options.setDefaultLocales(request.getDefaultLocales());
+ options.setReferenceTime(request.getReferenceTime());
+ return options;
+ }
+
+ /** @param defaultLocales ordered list of locale preferences. */
+ public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /** @param referenceTime refrence time used for interpreting relatives dates */
+ public Options setReferenceTime(@Nullable ZonedDateTime referenceTime) {
+ mReferenceTime = referenceTime;
+ return this;
+ }
+
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ @Nullable
+ public ZonedDateTime getReferenceTime() {
+ return mReferenceTime;
+ }
+
+ @Nullable
+ public Request getRequest() {
+ return mRequest;
+ }
+
+ @Nullable
+ public TextClassificationSessionId getSessionId() {
+ return mSessionId;
+ }
+ }
}
diff --git a/android/view/textclassifier/TextClassificationSession.java b/android/view/textclassifier/TextClassificationSession.java
index e8e300a9..4c641985 100644
--- a/android/view/textclassifier/TextClassificationSession.java
+++ b/android/view/textclassifier/TextClassificationSession.java
@@ -17,7 +17,6 @@
package android.view.textclassifier;
import android.annotation.WorkerThread;
-import android.view.textclassifier.DefaultLogger.SignatureParser;
import android.view.textclassifier.SelectionEvent.InvocationMethod;
import com.android.internal.util.Preconditions;
@@ -222,7 +221,8 @@ final class TextClassificationSession implements TextClassifier {
}
private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
- return DefaultLogger.CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
+ return SelectionSessionLogger.CLASSIFIER_ID.equals(
+ SelectionSessionLogger.SignatureParser.getClassifierId(signature));
}
}
}
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
index 54261be3..da47bcb1 100644
--- a/android/view/textclassifier/TextClassifier.java
+++ b/android/view/textclassifier/TextClassifier.java
@@ -41,8 +41,9 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
/**
* Interface for providing text classification related features.
@@ -208,6 +209,26 @@ public interface TextClassifier {
return suggestSelection(request);
}
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ default TextSelection suggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options) {
+ if (options == null) {
+ return suggestSelection(new TextSelection.Request.Builder(
+ text, selectionStartIndex, selectionEndIndex).build());
+ } else if (options.getRequest() != null) {
+ return suggestSelection(options.getRequest());
+ } else {
+ return suggestSelection(
+ new TextSelection.Request.Builder(text, selectionStartIndex, selectionEndIndex)
+ .setDefaultLocales(options.getDefaultLocales())
+ .build());
+ }
+ }
+
/**
* Classifies the specified text and returns a {@link TextClassification} object that can be
* used to generate a widget for handling the classified text.
@@ -267,6 +288,26 @@ public interface TextClassifier {
return classifyText(request);
}
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ default TextClassification classifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable TextClassification.Options options) {
+ if (options == null) {
+ return classifyText(
+ new TextClassification.Request.Builder(text, startIndex, endIndex).build());
+ } else if (options.getRequest() != null) {
+ return classifyText(options.getRequest());
+ } else {
+ return classifyText(new TextClassification.Request.Builder(text, startIndex, endIndex)
+ .setDefaultLocales(options.getDefaultLocales())
+ .setReferenceTime(options.getReferenceTime())
+ .build());
+ }
+ }
+
/**
* Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
* links information.
@@ -288,6 +329,22 @@ public interface TextClassifier {
return new TextLinks.Builder(request.getText().toString()).build();
}
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ default TextLinks generateLinks(
+ @NonNull CharSequence text, @Nullable TextLinks.Options options) {
+ if (options == null) {
+ return generateLinks(new TextLinks.Request.Builder(text).build());
+ } else if (options.getRequest() != null) {
+ return generateLinks(options.getRequest());
+ } else {
+ return generateLinks(new TextLinks.Request.Builder(text)
+ .setDefaultLocales(options.getDefaultLocales())
+ .setEntityConfig(options.getEntityConfig())
+ .build());
+ }
+ }
+
/**
* Returns the maximal length of text that can be processed by generateLinks.
*
@@ -302,18 +359,6 @@ public interface TextClassifier {
}
/**
- * Returns a helper for logging TextClassifier related events.
- *
- * @param config logger configuration
- * @hide
- */
- @WorkerThread
- default Logger getLogger(@NonNull Logger.Config config) {
- Preconditions.checkNotNull(config);
- return Logger.DISABLED;
- }
-
- /**
* Reports a selection event.
*
* <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
@@ -377,6 +422,12 @@ public interface TextClassifier {
/* includedEntityTypes */null, /* excludedEntityTypes */ null);
}
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ public static EntityConfig create(@Nullable Collection<String> hints) {
+ return createWithHints(hints);
+ }
+
/**
* Creates an EntityConfig.
*
@@ -406,6 +457,12 @@ public interface TextClassifier {
/* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null);
}
+ // TODO: Remove once apps can build against the latest sdk.
+ /** @hide */
+ public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) {
+ return createWithExplicitEntityList(entityTypes);
+ }
+
/**
* Returns a list of the final set of entities to find.
*
@@ -413,21 +470,15 @@ public interface TextClassifier {
*
* This method is intended for use by TextClassifier implementations.
*/
- public List<String> resolveEntityListModifications(@NonNull Collection<String> entities) {
- final ArrayList<String> finalList = new ArrayList<>();
+ public Collection<String> resolveEntityListModifications(
+ @NonNull Collection<String> entities) {
+ final Set<String> finalSet = new HashSet();
if (mUseHints) {
- for (String entity : entities) {
- if (!mExcludedEntityTypes.contains(entity)) {
- finalList.add(entity);
- }
- }
- }
- for (String entity : mIncludedEntityTypes) {
- if (!mExcludedEntityTypes.contains(entity) && !finalList.contains(entity)) {
- finalList.add(entity);
- }
+ finalSet.addAll(entities);
}
- return finalList;
+ finalSet.addAll(mIncludedEntityTypes);
+ finalSet.removeAll(mExcludedEntityTypes);
+ return finalSet;
}
/**
@@ -508,7 +559,7 @@ public interface TextClassifier {
final String string = request.getText().toString();
final TextLinks.Builder links = new TextLinks.Builder(string);
- final List<String> entities = request.getEntityConfig()
+ final Collection<String> entities = request.getEntityConfig()
.resolveEntityListModifications(Collections.emptyList());
if (entities.contains(TextClassifier.TYPE_URL)) {
addLinks(links, string, TextClassifier.TYPE_URL);
diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java
index 7e3748ae..22133558 100644
--- a/android/view/textclassifier/TextClassifierImpl.java
+++ b/android/view/textclassifier/TextClassifierImpl.java
@@ -94,11 +94,7 @@ public final class TextClassifierImpl implements TextClassifier {
private final Object mLoggerLock = new Object();
@GuardedBy("mLoggerLock") // Do not access outside this lock.
- private Logger.Config mLoggerConfig;
- @GuardedBy("mLoggerLock") // Do not access outside this lock.
- private Logger mLogger;
- @GuardedBy("mLoggerLock") // Do not access outside this lock.
- private Logger mLogger2; // This is the new logger. Will replace mLogger.
+ private SelectionSessionLogger mSessionLogger;
private final TextClassificationConstants mSettings;
@@ -283,28 +279,14 @@ public final class TextClassifierImpl implements TextClassifier {
}
}
- /** @inheritDoc */
- @Override
- public Logger getLogger(@NonNull Logger.Config config) {
- Preconditions.checkNotNull(config);
- synchronized (mLoggerLock) {
- if (mLogger == null || !config.equals(mLoggerConfig)) {
- mLoggerConfig = config;
- mLogger = new DefaultLogger(config);
- }
- }
- return mLogger;
- }
-
@Override
public void onSelectionEvent(SelectionEvent event) {
Preconditions.checkNotNull(event);
synchronized (mLoggerLock) {
- if (mLogger2 == null) {
- mLogger2 = new DefaultLogger(
- new Logger.Config(mContext, WIDGET_TYPE_UNKNOWN, null));
+ if (mSessionLogger == null) {
+ mSessionLogger = new SelectionSessionLogger();
}
- mLogger2.writeEvent(event);
+ mSessionLogger.writeEvent(event);
}
}
@@ -331,7 +313,7 @@ public final class TextClassifierImpl implements TextClassifier {
private String createId(String text, int start, int end) {
synchronized (mLock) {
- return DefaultLogger.createId(text, start, end, mContext, mModel.getVersion(),
+ return SelectionSessionLogger.createId(text, start, end, mContext, mModel.getVersion(),
mModel.getSupportedLocales());
}
}
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
index 17c7b13c..851b2c9b 100644
--- a/android/view/textclassifier/TextLinks.java
+++ b/android/view/textclassifier/TextLinks.java
@@ -28,6 +28,8 @@ import android.text.Spannable;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
+import android.text.util.Linkify;
+import android.text.util.Linkify.LinkifyMask;
import android.view.View;
import android.view.textclassifier.TextClassifier.EntityType;
import android.widget.TextView;
@@ -337,7 +339,7 @@ public final class TextLinks implements Parcelable {
/**
* @return The config representing the set of entities to look for
- * @see #setEntityConfig(TextClassifier.EntityConfig)
+ * @see Builder#setEntityConfig(TextClassifier.EntityConfig)
*/
@Nullable
public TextClassifier.EntityConfig getEntityConfig() {
@@ -607,4 +609,124 @@ public final class TextLinks implements Parcelable {
return new TextLinks(mFullText, mLinks);
}
}
+
+ // TODO: Remove once apps can build against the latest sdk.
+ /**
+ * Optional input parameters for generating TextLinks.
+ * @hide
+ */
+ public static final class Options {
+
+ @Nullable private final TextClassificationSessionId mSessionId;
+ @Nullable private final Request mRequest;
+ @Nullable private LocaleList mDefaultLocales;
+ @Nullable private TextClassifier.EntityConfig mEntityConfig;
+ private boolean mLegacyFallback;
+
+ private @ApplyStrategy int mApplyStrategy;
+ private Function<TextLink, TextLinkSpan> mSpanFactory;
+
+ private String mCallingPackageName;
+
+ public Options() {
+ this(null, null);
+ }
+
+ private Options(
+ @Nullable TextClassificationSessionId sessionId, @Nullable Request request) {
+ mSessionId = sessionId;
+ mRequest = request;
+ }
+
+ /** Helper to create Options from a Request. */
+ public static Options from(TextClassificationSessionId sessionId, Request request) {
+ final Options options = new Options(sessionId, request);
+ options.setDefaultLocales(request.getDefaultLocales());
+ options.setEntityConfig(request.getEntityConfig());
+ return options;
+ }
+
+ /** Returns a new options object based on the specified link mask. */
+ public static Options fromLinkMask(@LinkifyMask int mask) {
+ final List<String> entitiesToFind = new ArrayList<>();
+
+ if ((mask & Linkify.WEB_URLS) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_URL);
+ }
+ if ((mask & Linkify.EMAIL_ADDRESSES) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_EMAIL);
+ }
+ if ((mask & Linkify.PHONE_NUMBERS) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_PHONE);
+ }
+ if ((mask & Linkify.MAP_ADDRESSES) != 0) {
+ entitiesToFind.add(TextClassifier.TYPE_ADDRESS);
+ }
+
+ return new Options().setEntityConfig(
+ TextClassifier.EntityConfig.createWithEntityList(entitiesToFind));
+ }
+
+ /** @param defaultLocales ordered list of locale preferences. */
+ public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ /** @param entityConfig definition of which entity types to look for. */
+ public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) {
+ mEntityConfig = entityConfig;
+ return this;
+ }
+
+ /** @param applyStrategy strategy to use when resolving conflicts. */
+ public Options setApplyStrategy(@ApplyStrategy int applyStrategy) {
+ checkValidApplyStrategy(applyStrategy);
+ mApplyStrategy = applyStrategy;
+ return this;
+ }
+
+ /** @param spanFactory factory for converting TextLink to TextLinkSpan. */
+ public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) {
+ mSpanFactory = spanFactory;
+ return this;
+ }
+
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ @Nullable
+ public TextClassifier.EntityConfig getEntityConfig() {
+ return mEntityConfig;
+ }
+
+ @ApplyStrategy
+ public int getApplyStrategy() {
+ return mApplyStrategy;
+ }
+
+ @Nullable
+ public Function<TextLink, TextLinkSpan> getSpanFactory() {
+ return mSpanFactory;
+ }
+
+ @Nullable
+ public Request getRequest() {
+ return mRequest;
+ }
+
+ @Nullable
+ public TextClassificationSessionId getSessionId() {
+ return mSessionId;
+ }
+
+ private static void checkValidApplyStrategy(int applyStrategy) {
+ if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) {
+ throw new IllegalArgumentException(
+ "Invalid apply strategy. See TextLinks.ApplyStrategy for options.");
+ }
+ }
+ }
}
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
index 939e7176..17687c9e 100644
--- a/android/view/textclassifier/TextSelection.java
+++ b/android/view/textclassifier/TextSelection.java
@@ -375,4 +375,56 @@ public final class TextSelection implements Parcelable {
mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
mId = in.readString();
}
+
+
+ // TODO: Remove once apps can build against the latest sdk.
+ /**
+ * Optional input parameters for generating TextSelection.
+ * @hide
+ */
+ public static final class Options {
+
+ @Nullable private final TextClassificationSessionId mSessionId;
+ @Nullable private final Request mRequest;
+ @Nullable private LocaleList mDefaultLocales;
+ private boolean mDarkLaunchAllowed;
+
+ public Options() {
+ this(null, null);
+ }
+
+ private Options(
+ @Nullable TextClassificationSessionId sessionId, @Nullable Request request) {
+ mSessionId = sessionId;
+ mRequest = request;
+ }
+
+ /** Helper to create Options from a Request. */
+ public static Options from(TextClassificationSessionId sessionId, Request request) {
+ final Options options = new Options(sessionId, request);
+ options.setDefaultLocales(request.getDefaultLocales());
+ return options;
+ }
+
+ /** @param defaultLocales ordered list of locale preferences. */
+ public Options setDefaultLocales(@Nullable LocaleList defaultLocales) {
+ mDefaultLocales = defaultLocales;
+ return this;
+ }
+
+ @Nullable
+ public LocaleList getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ @Nullable
+ public Request getRequest() {
+ return mRequest;
+ }
+
+ @Nullable
+ public TextClassificationSessionId getSessionId() {
+ return mSessionId;
+ }
+ }
}
diff --git a/android/view/textservice/TextServicesManager.java b/android/view/textservice/TextServicesManager.java
index 8e1f2183..21ec42b1 100644
--- a/android/view/textservice/TextServicesManager.java
+++ b/android/view/textservice/TextServicesManager.java
@@ -1,58 +1,219 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
*/
package android.view.textservice;
+import android.annotation.SystemService;
+import android.content.Context;
import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
+import com.android.internal.textservice.ITextServicesManager;
+
import java.util.Locale;
/**
- * A stub class of TextServicesManager for Layout-Lib.
+ * System API to the overall text services, which arbitrates interaction between applications
+ * and text services.
+ *
+ * The user can change the current text services in Settings. And also applications can specify
+ * the target text services.
+ *
+ * <h3>Architecture Overview</h3>
+ *
+ * <p>There are three primary parties involved in the text services
+ * framework (TSF) architecture:</p>
+ *
+ * <ul>
+ * <li> The <strong>text services manager</strong> as expressed by this class
+ * is the central point of the system that manages interaction between all
+ * other parts. It is expressed as the client-side API here which exists
+ * in each application context and communicates with a global system service
+ * that manages the interaction across all processes.
+ * <li> A <strong>text service</strong> implements a particular
+ * interaction model allowing the client application to retrieve information of text.
+ * The system binds to the current text service that is in use, causing it to be created and run.
+ * <li> Multiple <strong>client applications</strong> arbitrate with the text service
+ * manager for connections to text services.
+ * </ul>
+ *
+ * <h3>Text services sessions</h3>
+ * <ul>
+ * <li>The <strong>spell checker session</strong> is one of the text services.
+ * {@link android.view.textservice.SpellCheckerSession}</li>
+ * </ul>
+ *
*/
+@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)
public final class TextServicesManager {
- private static final TextServicesManager sInstance = new TextServicesManager();
- private static final SpellCheckerInfo[] EMPTY_SPELL_CHECKER_INFO = new SpellCheckerInfo[0];
+ private static final String TAG = TextServicesManager.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ /**
+ * A compile time switch to control per-profile spell checker, which is not yet ready.
+ * @hide
+ */
+ public static final boolean DISABLE_PER_PROFILE_SPELL_CHECKER = true;
+
+ private static TextServicesManager sInstance;
+
+ private final ITextServicesManager mService;
+
+ private TextServicesManager() throws ServiceNotFoundException {
+ mService = ITextServicesManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE));
+ }
/**
* Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
* @hide
*/
public static TextServicesManager getInstance() {
- return sInstance;
+ synchronized (TextServicesManager.class) {
+ if (sInstance == null) {
+ try {
+ sInstance = new TextServicesManager();
+ } catch (ServiceNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return sInstance;
+ }
}
+ /**
+ * Returns the language component of a given locale string.
+ */
+ private static String parseLanguageFromLocaleString(String locale) {
+ final int idx = locale.indexOf('_');
+ if (idx < 0) {
+ return locale;
+ } else {
+ return locale.substring(0, idx);
+ }
+ }
+
+ /**
+ * Get a spell checker session for the specified spell checker
+ * @param locale the locale for the spell checker. If {@code locale} is null and
+ * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be
+ * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true,
+ * the locale specified in Settings will be returned only when it is same as {@code locale}.
+ * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is
+ * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
+ * selected.
+ * @param listener a spell checker session lister for getting results from a spell checker.
+ * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled
+ * languages in settings will be returned.
+ * @return the spell checker session of the spell checker
+ */
public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
- return null;
+ if (listener == null) {
+ throw new NullPointerException();
+ }
+ if (!referToSpellCheckerLanguageSettings && locale == null) {
+ throw new IllegalArgumentException("Locale should not be null if you don't refer"
+ + " settings.");
+ }
+
+ if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) {
+ return null;
+ }
+
+ final SpellCheckerInfo sci;
+ try {
+ sci = mService.getCurrentSpellChecker(null);
+ } catch (RemoteException e) {
+ return null;
+ }
+ if (sci == null) {
+ return null;
+ }
+ SpellCheckerSubtype subtypeInUse = null;
+ if (referToSpellCheckerLanguageSettings) {
+ subtypeInUse = getCurrentSpellCheckerSubtype(true);
+ if (subtypeInUse == null) {
+ return null;
+ }
+ if (locale != null) {
+ final String subtypeLocale = subtypeInUse.getLocale();
+ final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
+ if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) {
+ return null;
+ }
+ }
+ } else {
+ final String localeStr = locale.toString();
+ for (int i = 0; i < sci.getSubtypeCount(); ++i) {
+ final SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
+ final String tempSubtypeLocale = subtype.getLocale();
+ final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale);
+ if (tempSubtypeLocale.equals(localeStr)) {
+ subtypeInUse = subtype;
+ break;
+ } else if (tempSubtypeLanguage.length() >= 2 &&
+ locale.getLanguage().equals(tempSubtypeLanguage)) {
+ subtypeInUse = subtype;
+ }
+ }
+ }
+ if (subtypeInUse == null) {
+ return null;
+ }
+ final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener);
+ try {
+ mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
+ session.getTextServicesSessionListener(),
+ session.getSpellCheckerSessionListener(), bundle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return session;
}
/**
* @hide
*/
public SpellCheckerInfo[] getEnabledSpellCheckers() {
- return EMPTY_SPELL_CHECKER_INFO;
+ try {
+ final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers();
+ if (DBG) {
+ Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null"));
+ }
+ return retval;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
* @hide
*/
public SpellCheckerInfo getCurrentSpellChecker() {
- return null;
+ try {
+ // Passing null as a locale for ICS
+ return mService.getCurrentSpellChecker(null);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
@@ -60,13 +221,22 @@ public final class TextServicesManager {
*/
public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
boolean allowImplicitlySelectedSubtype) {
- return null;
+ try {
+ // Passing null as a locale until we support multiple enabled spell checker subtypes.
+ return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
* @hide
*/
public boolean isSpellCheckerEnabled() {
- return false;
+ try {
+ return mService.isSpellCheckerEnabled();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
}
diff --git a/android/webkit/FindAddress.java b/android/webkit/FindAddress.java
index 31b24273..9183227b 100644
--- a/android/webkit/FindAddress.java
+++ b/android/webkit/FindAddress.java
@@ -429,20 +429,21 @@ class FindAddress {
// At this point we've matched a state; try to match a zip code after it.
Matcher zipMatcher = sWordRe.matcher(content);
- if (zipMatcher.find(stateMatch.end())
- && isValidZipCode(zipMatcher.group(0), stateMatch)) {
- return zipMatcher.end();
+ if (zipMatcher.find(stateMatch.end())) {
+ if (isValidZipCode(zipMatcher.group(0), stateMatch)) {
+ return zipMatcher.end();
+ }
+ } else {
+ // The content ends with a state but no zip
+ // code. This is a legal match according to the
+ // documentation. N.B. This is equivalent to the
+ // original c++ implementation, which only allowed
+ // the zip code to be optional at the end of the
+ // string, which presumably is a bug. We tried
+ // relaxing this to work in other places but it
+ // caused too many false positives.
+ nonZipMatch = stateMatch.end();
}
- // The content ends with a state but no zip
- // code. This is a legal match according to the
- // documentation. N.B. This differs from the
- // original c++ implementation, which only allowed
- // the zip code to be optional at the end of the
- // string, which presumably is a bug. Now we
- // prefer to find a match with a zip code, but
- // remember non-zip matches and return them if
- // necessary.
- nonZipMatch = stateMatch.end();
}
}
}
diff --git a/android/webkit/TracingConfig.java b/android/webkit/TracingConfig.java
index d95ca61d..20801684 100644
--- a/android/webkit/TracingConfig.java
+++ b/android/webkit/TracingConfig.java
@@ -54,37 +54,37 @@ public class TracingConfig {
/**
* Predefined set of categories typically useful for analyzing WebViews.
- * Typically includes android_webview and Java.
+ * Typically includes "android_webview" and "Java" categories.
*/
public static final int CATEGORIES_ANDROID_WEBVIEW = 1 << 1;
/**
* Predefined set of categories typically useful for web developers.
- * Typically includes blink, compositor, renderer.scheduler and v8 categories.
+ * Typically includes "blink", "compositor", "renderer.scheduler" and "v8" categories.
*/
public static final int CATEGORIES_WEB_DEVELOPER = 1 << 2;
/**
* Predefined set of categories for analyzing input latency issues.
- * Typically includes input, renderer.scheduler categories.
+ * Typically includes "input", "renderer.scheduler" categories.
*/
public static final int CATEGORIES_INPUT_LATENCY = 1 << 3;
/**
* Predefined set of categories for analyzing rendering issues.
- * Typically includes blink, compositor and gpu categories.
+ * Typically includes "blink", "compositor" and "gpu" categories.
*/
public static final int CATEGORIES_RENDERING = 1 << 4;
/**
* Predefined set of categories for analyzing javascript and rendering issues.
- * Typically includes blink, compositor, gpu, renderer.scheduler and v8 categories.
+ * Typically includes "blink", "compositor", "gpu", "renderer.scheduler" and "v8" categories.
*/
public static final int CATEGORIES_JAVASCRIPT_AND_RENDERING = 1 << 5;
/**
* Predefined set of categories for studying difficult rendering performance problems.
- * Typically includes blink, compositor, gpu, renderer.scheduler, v8 and
+ * Typically includes "blink", "compositor", "gpu", "renderer.scheduler", "v8" and
* some other compositor categories which are disabled by default.
*/
public static final int CATEGORIES_FRAME_VIEWER = 1 << 6;
@@ -123,7 +123,9 @@ public class TracingConfig {
}
/**
- * Returns a bitmask of the predefined categories values of this configuration.
+ * Returns a bitmask of the predefined category sets of this configuration.
+ *
+ * @return Bitmask of predefined category sets.
*/
@PredefinedCategories
public int getPredefinedCategories() {
@@ -133,7 +135,7 @@ public class TracingConfig {
/**
* Returns the list of included custom category patterns for this configuration.
*
- * @return empty list if no custom category patterns are specified.
+ * @return Empty list if no custom category patterns are specified.
*/
@NonNull
public List<String> getCustomIncludedCategories() {
@@ -142,6 +144,8 @@ public class TracingConfig {
/**
* Returns the tracing mode of this configuration.
+ *
+ * @return The tracing mode of this configuration.
*/
@TracingMode
public int getTracingMode() {
@@ -150,28 +154,37 @@ public class TracingConfig {
/**
* Builder used to create {@link TracingConfig} objects.
- *
+ * <p>
* Examples:
- * new TracingConfig.Builder().build()
- * -- creates a configuration with default options: {@link #CATEGORIES_NONE},
- * {@link #RECORD_UNTIL_FULL}.
- * new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build()
- * -- records trace events from the "web developer" predefined category sets.
- * new TracingConfig.Builder().addCategories(CATEGORIES_RENDERING,
- * CATEGORIES_INPUT_LATENCY).build()
- * -- records trace events from the "rendering" and "input latency" predefined
- * category sets.
- * new TracingConfig.Builder().addCategories("browser").build()
- * -- records only the trace events from the "browser" category.
- * new TracingConfig.Builder().addCategories("blink*","renderer*").build()
- * -- records only the trace events matching the "blink*" and "renderer*" patterns
- * (e.g. "blink.animations", "renderer_host" and "renderer.scheduler" categories).
- * new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER)
+ * <pre class="prettyprint">
+ * // Create a configuration with default options: {@link #CATEGORIES_NONE},
+ * // {@link #RECORD_CONTINUOUSLY}.
+ * <code>new TracingConfig.Builder().build()</code>
+ *
+ * // Record trace events from the "web developer" predefined category sets.
+ * // Uses a ring buffer (the default {@link #RECORD_CONTINUOUSLY} mode) for
+ * // internal storage during tracing.
+ * <code>new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER).build()</code>
+ *
+ * // Record trace events from the "rendering" and "input latency" predefined
+ * // category sets.
+ * <code>new TracingConfig.Builder().addCategories(CATEGORIES_RENDERING,
+ * CATEGORIES_INPUT_LATENCY).build()</code>
+ *
+ * // Record only the trace events from the "browser" category.
+ * <code>new TracingConfig.Builder().addCategories("browser").build()</code>
+ *
+ * // Record only the trace events matching the "blink*" and "renderer*" patterns
+ * // (e.g. "blink.animations", "renderer_host" and "renderer.scheduler" categories).
+ * <code>new TracingConfig.Builder().addCategories("blink*","renderer*").build()</code>
+ *
+ * // Record events from the "web developer" predefined category set and events from
+ * // the "disabled-by-default-v8.gc" category to understand where garbage collection
+ * // is being triggered. Uses a limited size buffer for internal storage during tracing.
+ * <code>new TracingConfig.Builder().addCategories(CATEGORIES_WEB_DEVELOPER)
* .addCategories("disabled-by-default-v8.gc")
- * .setTracingMode(RECORD_CONTINUOUSLY).build()
- * -- records events from the "web developer" predefined category set and events from
- * the "disabled-by-default-v8.gc" category to understand where garbage collection
- * is being triggered. Uses a ring buffer for internal storage during tracing.
+ * .setTracingMode(RECORD_UNTIL_FULL).build()</code>
+ * </pre>
*/
public static class Builder {
private @PredefinedCategories int mPredefinedCategories = CATEGORIES_NONE;
@@ -185,6 +198,8 @@ public class TracingConfig {
/**
* Build {@link TracingConfig} using the current settings.
+ *
+ * @return The {@link TracingConfig} with the current settings.
*/
public TracingConfig build() {
return new TracingConfig(mPredefinedCategories, mCustomIncludedCategories,
@@ -192,16 +207,15 @@ public class TracingConfig {
}
/**
- * Adds categories from a predefined set of categories to be included in the trace output.
+ * Adds predefined sets of categories to be included in the trace output.
+ *
+ * A predefined category set can be one of {@link #CATEGORIES_NONE},
+ * {@link #CATEGORIES_ALL}, {@link #CATEGORIES_ANDROID_WEBVIEW},
+ * {@link #CATEGORIES_WEB_DEVELOPER}, {@link #CATEGORIES_INPUT_LATENCY},
+ * {@link #CATEGORIES_RENDERING}, {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or
+ * {@link #CATEGORIES_FRAME_VIEWER}.
*
- * @param predefinedCategories list or bitmask of predefined category sets to use:
- * {@link #CATEGORIES_NONE}, {@link #CATEGORIES_ALL},
- * {@link #CATEGORIES_ANDROID_WEBVIEW},
- * {@link #CATEGORIES_WEB_DEVELOPER},
- * {@link #CATEGORIES_INPUT_LATENCY},
- * {@link #CATEGORIES_RENDERING},
- * {@link #CATEGORIES_JAVASCRIPT_AND_RENDERING} or
- * {@link #CATEGORIES_FRAME_VIEWER}.
+ * @param predefinedCategories A list or bitmask of predefined category sets.
* @return The builder to facilitate chaining.
*/
public Builder addCategories(@PredefinedCategories int... predefinedCategories) {
@@ -215,11 +229,11 @@ public class TracingConfig {
* Adds custom categories to be included in trace output.
*
* Note that the categories are defined by the currently-in-use version of WebView. They
- * live in chromium code and are not part of the Android API. See
+ * live in chromium code and are not part of the Android API.
* See <a href="https://www.chromium.org/developers/how-tos/trace-event-profiling-tool">
* chromium documentation on tracing</a> for more details.
*
- * @param categories a list of category patterns. A category pattern can contain wilcards,
+ * @param categories A list of category patterns. A category pattern can contain wildcards,
* e.g. "blink*" or full category name e.g. "renderer.scheduler".
* @return The builder to facilitate chaining.
*/
@@ -235,7 +249,7 @@ public class TracingConfig {
*
* Same as {@link #addCategories(String...)} but allows to pass a Collection as a parameter.
*
- * @param categories a list of category patters.
+ * @param categories A list of category patterns.
* @return The builder to facilitate chaining.
*/
public Builder addCategories(Collection<String> categories) {
@@ -245,8 +259,9 @@ public class TracingConfig {
/**
* Sets the tracing mode for this configuration.
+ * When tracingMode is not set explicitly, the default is {@link #RECORD_CONTINUOUSLY}.
*
- * @param tracingMode tracing mode to use, one of {@link #RECORD_UNTIL_FULL} or
+ * @param tracingMode The tracing mode to use, one of {@link #RECORD_UNTIL_FULL} or
* {@link #RECORD_CONTINUOUSLY}.
* @return The builder to facilitate chaining.
*/
diff --git a/android/webkit/TracingController.java b/android/webkit/TracingController.java
index 50068f5a..05c0304e 100644
--- a/android/webkit/TracingController.java
+++ b/android/webkit/TracingController.java
@@ -35,9 +35,9 @@ import java.util.concurrent.Executor;
* Example usage:
* <pre class="prettyprint">
* TracingController tracingController = TracingController.getInstance();
- * tracingController.start(new TraceConfig.Builder()
+ * tracingController.start(new TracingConfig.Builder()
* .addCategories(CATEGORIES_WEB_DEVELOPER).build());
- * [..]
+ * ...
* tracingController.stop(new FileOutputStream("trace.json"),
* Executors.newSingleThreadExecutor());
* </pre></p>
@@ -49,7 +49,7 @@ public abstract class TracingController {
* only one TracingController instance for all WebView instances,
* however this restriction may be relaxed in a future Android release.
*
- * @return the default TracingController instance
+ * @return The default TracingController instance.
*/
@NonNull
public static TracingController getInstance() {
@@ -65,8 +65,10 @@ public abstract class TracingController {
* using an internal buffer and flushed to the outputStream when
* {@link #stop(OutputStream, Executor)} is called.
*
- * @param tracingConfig configuration options to use for tracing
- * @throws IllegalStateException if the system is already tracing.
+ * @param tracingConfig Configuration options to use for tracing.
+ * @throws IllegalStateException If the system is already tracing.
+ * @throws IllegalArgumentException If the configuration is invalid (e.g.
+ * invalid category pattern or invalid tracing mode).
*/
public abstract void start(@NonNull TracingConfig tracingConfig);
@@ -77,17 +79,22 @@ public abstract class TracingController {
* in chunks by invoking {@link java.io.OutputStream#write(byte[])}. On completion
* the {@link java.io.OutputStream#close()} method is called.
*
- * @param outputStream the output steam the tracing data will be sent to. If null
+ * @param outputStream The output stream the tracing data will be sent to. If null
* the tracing data will be discarded.
- * @param executor the {@link java.util.concurrent.Executor} on which the
- * outputStream #write and #close methods will be invoked.
- * @return false if the system was not tracing at the time of the call, true
- * otherwise.
+ * @param executor The {@link java.util.concurrent.Executor} on which the
+ * outputStream {@link java.io.OutputStream#write(byte[])} and
+ * {@link java.io.OutputStream#close()} methods will be invoked.
+ * @return False if the WebView framework was not tracing at the time of the call,
+ * true otherwise.
*/
public abstract boolean stop(@Nullable OutputStream outputStream,
@NonNull @CallbackExecutor Executor executor);
- /** True if the system is tracing */
+ /**
+ * Returns whether the WebView framework is tracing.
+ *
+ * @return True if tracing is enabled.
+ */
public abstract boolean isTracing();
}
diff --git a/android/webkit/WebView.java b/android/webkit/WebView.java
index 202f2046..10748acd 100644
--- a/android/webkit/WebView.java
+++ b/android/webkit/WebView.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,223 +16,3136 @@
package android.webkit;
-import com.android.layoutlib.bridge.MockView;
-
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.Widget;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.res.Configuration;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
import android.graphics.Picture;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.net.http.SslCertificate;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.print.PrintDocumentAdapter;
+import android.security.KeyChain;
import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.DragEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewStructure;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.textclassifier.TextClassifier;
+import android.widget.AbsoluteLayout;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
/**
- * Mock version of the WebView.
- * Only non override public methods from the real WebView have been added in there.
- * Methods that take an unknown class as parameter or as return object, have been removed for now.
- *
- * TODO: generate automatically.
+ * <p>A View that displays web pages. This class is the basis upon which you
+ * can roll your own web browser or simply display some online content within your Activity.
+ * It uses the WebKit rendering engine to display
+ * web pages and includes methods to navigate forward and backward
+ * through a history, zoom in and out, perform text searches and more.
+ *
+ * <p>Note that, in order for your Activity to access the Internet and load web pages
+ * in a WebView, you must add the {@code INTERNET} permissions to your
+ * Android Manifest file:
+ *
+ * <pre>
+ * {@code <uses-permission android:name="android.permission.INTERNET" />}
+ * </pre>
+ *
+ * <p>This must be a child of the <a
+ * href="{@docRoot}guide/topics/manifest/manifest-element.html">{@code <manifest>}</a>
+ * element.
+ *
+ * <p>For more information, read
+ * <a href="{@docRoot}guide/webapps/webview.html">Building Web Apps in WebView</a>.
+ *
+ * <h3>Basic usage</h3>
+ *
+ * <p>By default, a WebView provides no browser-like widgets, does not
+ * enable JavaScript and web page errors are ignored. If your goal is only
+ * to display some HTML as a part of your UI, this is probably fine;
+ * the user won't need to interact with the web page beyond reading
+ * it, and the web page won't need to interact with the user. If you
+ * actually want a full-blown web browser, then you probably want to
+ * invoke the Browser application with a URL Intent rather than show it
+ * with a WebView. For example:
+ * <pre>
+ * Uri uri = Uri.parse("https://www.example.com");
+ * Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ * startActivity(intent);
+ * </pre>
+ * <p>See {@link android.content.Intent} for more information.
+ *
+ * <p>To provide a WebView in your own Activity, include a {@code <WebView>} in your layout,
+ * or set the entire Activity window as a WebView during {@link
+ * android.app.Activity#onCreate(Bundle) onCreate()}:
+ *
+ * <pre class="prettyprint">
+ * WebView webview = new WebView(this);
+ * setContentView(webview);
+ * </pre>
+ *
+ * <p>Then load the desired web page:
+ *
+ * <pre>
+ * // Simplest usage: note that an exception will NOT be thrown
+ * // if there is an error loading this page (see below).
+ * webview.loadUrl("https://example.com/");
+ *
+ * // OR, you can also load from an HTML string:
+ * String summary = "&lt;html>&lt;body>You scored &lt;b>192&lt;/b> points.&lt;/body>&lt;/html>";
+ * webview.loadData(summary, "text/html", null);
+ * // ... although note that there are restrictions on what this HTML can do.
+ * // See {@link #loadData(String,String,String)} and {@link
+ * #loadDataWithBaseURL(String,String,String,String,String)} for more info.
+ * // Also see {@link #loadData(String,String,String)} for information on encoding special
+ * // characters.
+ * </pre>
+ *
+ * <p>A WebView has several customization points where you can add your
+ * own behavior. These are:
+ *
+ * <ul>
+ * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass.
+ * This class is called when something that might impact a
+ * browser UI happens, for instance, progress updates and
+ * JavaScript alerts are sent here (see <a
+ * href="{@docRoot}guide/developing/debug-tasks.html#DebuggingWebPages">Debugging Tasks</a>).
+ * </li>
+ * <li>Creating and setting a {@link android.webkit.WebViewClient} subclass.
+ * It will be called when things happen that impact the
+ * rendering of the content, eg, errors or form submissions. You
+ * can also intercept URL loading here (via {@link
+ * android.webkit.WebViewClient#shouldOverrideUrlLoading(WebView,String)
+ * shouldOverrideUrlLoading()}).</li>
+ * <li>Modifying the {@link android.webkit.WebSettings}, such as
+ * enabling JavaScript with {@link android.webkit.WebSettings#setJavaScriptEnabled(boolean)
+ * setJavaScriptEnabled()}. </li>
+ * <li>Injecting Java objects into the WebView using the
+ * {@link android.webkit.WebView#addJavascriptInterface} method. This
+ * method allows you to inject Java objects into a page's JavaScript
+ * context, so that they can be accessed by JavaScript in the page.</li>
+ * </ul>
+ *
+ * <p>Here's a more complicated example, showing error handling,
+ * settings, and progress notification:
+ *
+ * <pre class="prettyprint">
+ * // Let's display the progress in the activity title bar, like the
+ * // browser app does.
+ * getWindow().requestFeature(Window.FEATURE_PROGRESS);
+ *
+ * webview.getSettings().setJavaScriptEnabled(true);
+ *
+ * final Activity activity = this;
+ * webview.setWebChromeClient(new WebChromeClient() {
+ * public void onProgressChanged(WebView view, int progress) {
+ * // Activities and WebViews measure progress with different scales.
+ * // The progress meter will automatically disappear when we reach 100%
+ * activity.setProgress(progress * 1000);
+ * }
+ * });
+ * webview.setWebViewClient(new WebViewClient() {
+ * public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ * Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show();
+ * }
+ * });
+ *
+ * webview.loadUrl("https://developer.android.com/");
+ * </pre>
+ *
+ * <h3>Zoom</h3>
+ *
+ * <p>To enable the built-in zoom, set
+ * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)}
+ * (introduced in API level {@link android.os.Build.VERSION_CODES#CUPCAKE}).
+ *
+ * <p class="note"><b>Note:</b> Using zoom if either the height or width is set to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} may lead to undefined behavior
+ * and should be avoided.
+ *
+ * <h3>Cookie and window management</h3>
+ *
+ * <p>For obvious security reasons, your application has its own
+ * cache, cookie store etc.&mdash;it does not share the Browser
+ * application's data.
+ *
+ * <p>By default, requests by the HTML to open new windows are
+ * ignored. This is {@code true} whether they be opened by JavaScript or by
+ * the target attribute on a link. You can customize your
+ * {@link WebChromeClient} to provide your own behavior for opening multiple windows,
+ * and render them in whatever manner you want.
+ *
+ * <p>The standard behavior for an Activity is to be destroyed and
+ * recreated when the device orientation or any other configuration changes. This will cause
+ * the WebView to reload the current page. If you don't want that, you
+ * can set your Activity to handle the {@code orientation} and {@code keyboardHidden}
+ * changes, and then just leave the WebView alone. It'll automatically
+ * re-orient itself as appropriate. Read <a
+ * href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a> for
+ * more information about how to handle configuration changes during runtime.
+ *
+ *
+ * <h3>Building web pages to support different screen densities</h3>
+ *
+ * <p>The screen density of a device is based on the screen resolution. A screen with low density
+ * has fewer available pixels per inch, where a screen with high density
+ * has more &mdash; sometimes significantly more &mdash; pixels per inch. The density of a
+ * screen is important because, other things being equal, a UI element (such as a button) whose
+ * height and width are defined in terms of screen pixels will appear larger on the lower density
+ * screen and smaller on the higher density screen.
+ * For simplicity, Android collapses all actual screen densities into three generalized densities:
+ * high, medium, and low.
+ * <p>By default, WebView scales a web page so that it is drawn at a size that matches the default
+ * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen
+ * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels
+ * are bigger).
+ * Starting with API level {@link android.os.Build.VERSION_CODES#ECLAIR}, WebView supports DOM, CSS,
+ * and meta tag features to help you (as a web developer) target screens with different screen
+ * densities.
+ * <p>Here's a summary of the features you can use to handle different screen densities:
+ * <ul>
+ * <li>The {@code window.devicePixelRatio} DOM property. The value of this property specifies the
+ * default scaling factor used for the current device. For example, if the value of {@code
+ * window.devicePixelRatio} is "1.0", then the device is considered a medium density (mdpi) device
+ * and default scaling is not applied to the web page; if the value is "1.5", then the device is
+ * considered a high density device (hdpi) and the page content is scaled 1.5x; if the
+ * value is "0.75", then the device is considered a low density device (ldpi) and the content is
+ * scaled 0.75x.</li>
+ * <li>The {@code -webkit-device-pixel-ratio} CSS media query. Use this to specify the screen
+ * densities for which this style sheet is to be used. The corresponding value should be either
+ * "0.75", "1", or "1.5", to indicate that the styles are for devices with low density, medium
+ * density, or high density screens, respectively. For example:
+ * <pre>
+ * &lt;link rel="stylesheet" media="screen and (-webkit-device-pixel-ratio:1.5)" href="hdpi.css" /&gt;</pre>
+ * <p>The {@code hdpi.css} stylesheet is only used for devices with a screen pixel ratio of 1.5,
+ * which is the high density pixel ratio.
+ * </li>
+ * </ul>
+ *
+ * <h3>HTML5 Video support</h3>
+ *
+ * <p>In order to support inline HTML5 video in your application you need to have hardware
+ * acceleration turned on.
+ *
+ * <h3>Full screen support</h3>
+ *
+ * <p>In order to support full screen &mdash; for video or other HTML content &mdash; you need to set a
+ * {@link android.webkit.WebChromeClient} and implement both
+ * {@link WebChromeClient#onShowCustomView(View, WebChromeClient.CustomViewCallback)}
+ * and {@link WebChromeClient#onHideCustomView()}. If the implementation of either of these two methods is
+ * missing then the web contents will not be allowed to enter full screen. Optionally you can implement
+ * {@link WebChromeClient#getVideoLoadingProgressView()} to customize the View displayed whilst a video
+ * is loading.
+ *
+ * <h3>HTML5 Geolocation API support</h3>
+ *
+ * <p>For applications targeting Android N and later releases
+ * (API level > {@link android.os.Build.VERSION_CODES#M}) the geolocation api is only supported on
+ * secure origins such as https. For such applications requests to geolocation api on non-secure
+ * origins are automatically denied without invoking the corresponding
+ * {@link WebChromeClient#onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback)}
+ * method.
+ *
+ * <h3>Layout size</h3>
+ * <p>
+ * It is recommended to set the WebView layout height to a fixed value or to
+ * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} instead of using
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
+ * When using {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+ * for the height none of the WebView's parents should use a
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} layout height since that could result in
+ * incorrect sizing of the views.
+ *
+ * <p>Setting the WebView's height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+ * enables the following behaviors:
+ * <ul>
+ * <li>The HTML body layout height is set to a fixed value. This means that elements with a height
+ * relative to the HTML body may not be sized correctly. </li>
+ * <li>For applications targeting {@link android.os.Build.VERSION_CODES#KITKAT} and earlier SDKs the
+ * HTML viewport meta tag will be ignored in order to preserve backwards compatibility. </li>
+ * </ul>
+ *
+ * <p>
+ * Using a layout width of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} is not
+ * supported. If such a width is used the WebView will attempt to use the width of the parent
+ * instead.
+ *
+ * <h3>Metrics</h3>
+ *
+ * <p>
+ * WebView may upload anonymous diagnostic data to Google when the user has consented. This data
+ * helps Google improve WebView. Data is collected on a per-app basis for each app which has
+ * instantiated a WebView. An individual app can opt out of this feature by putting the following
+ * tag in its manifest's {@code <application>} element:
+ * <pre>
+ * &lt;manifest&gt;
+ * &lt;application&gt;
+ * ...
+ * &lt;meta-data android:name=&quot;android.webkit.WebView.MetricsOptOut&quot;
+ * android:value=&quot;true&quot; /&gt;
+ * &lt;/application&gt;
+ * &lt;/manifest&gt;
+ * </pre>
+ * <p>
+ * Data will only be uploaded for a given app if the user has consented AND the app has not opted
+ * out.
+ *
+ * <h3>Safe Browsing</h3>
+ *
+ * <p>
+ * With Safe Browsing, WebView will block malicious URLs and present a warning UI to the user to
+ * allow them to navigate back safely or proceed to the malicious page.
+ * <p>
+ * Safe Browsing is enabled by default on devices which support it. If your app needs to disable
+ * Safe Browsing for all WebViews, it can do so in the manifest's {@code <application>} element:
+ * <p>
+ * <pre>
+ * &lt;manifest&gt;
+ * &lt;application&gt;
+ * ...
+ * &lt;meta-data android:name=&quot;android.webkit.WebView.EnableSafeBrowsing&quot;
+ * android:value=&quot;false&quot; /&gt;
+ * &lt;/application&gt;
+ * &lt;/manifest&gt;
+ * </pre>
+ *
+ * <p>
+ * Otherwise, see {@link WebSettings#setSafeBrowsingEnabled}.
*
*/
-public class WebView extends MockView {
+// Implementation notes.
+// The WebView is a thin API class that delegates its public API to a backend WebViewProvider
+// class instance. WebView extends {@link AbsoluteLayout} for backward compatibility reasons.
+// Methods are delegated to the provider implementation: all public API methods introduced in this
+// file are fully delegated, whereas public and protected methods from the View base classes are
+// only delegated where a specific need exists for them to do so.
+@Widget
+public class WebView extends AbsoluteLayout
+ implements ViewTreeObserver.OnGlobalFocusChangeListener,
+ ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler {
+
+ private static final String LOGTAG = "WebView";
+
+ // Throwing an exception for incorrect thread usage if the
+ // build target is JB MR2 or newer. Defaults to false, and is
+ // set in the WebView constructor.
+ private static volatile boolean sEnforceThreadChecking = false;
+
+ /**
+ * Transportation object for returning WebView across thread boundaries.
+ */
+ public class WebViewTransport {
+ private WebView mWebview;
+
+ /**
+ * Sets the WebView to the transportation object.
+ *
+ * @param webview the WebView to transport
+ */
+ public synchronized void setWebView(WebView webview) {
+ mWebview = webview;
+ }
+
+ /**
+ * Gets the WebView object.
+ *
+ * @return the transported WebView object
+ */
+ public synchronized WebView getWebView() {
+ return mWebview;
+ }
+ }
+
+ /**
+ * URI scheme for telephone number.
+ */
+ public static final String SCHEME_TEL = "tel:";
+ /**
+ * URI scheme for email address.
+ */
+ public static final String SCHEME_MAILTO = "mailto:";
+ /**
+ * URI scheme for map address.
+ */
+ public static final String SCHEME_GEO = "geo:0,0?q=";
/**
- * Construct a new WebView with a Context object.
- * @param context A Context object used to access application assets.
+ * Interface to listen for find results.
+ */
+ public interface FindListener {
+ /**
+ * Notifies the listener about progress made by a find operation.
+ *
+ * @param activeMatchOrdinal the zero-based ordinal of the currently selected match
+ * @param numberOfMatches how many matches have been found
+ * @param isDoneCounting whether the find operation has actually completed. The listener
+ * may be notified multiple times while the
+ * operation is underway, and the numberOfMatches
+ * value should not be considered final unless
+ * isDoneCounting is {@code true}.
+ */
+ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
+ boolean isDoneCounting);
+ }
+
+ /**
+ * Callback interface supplied to {@link #postVisualStateCallback} for receiving
+ * notifications about the visual state.
+ */
+ public static abstract class VisualStateCallback {
+ /**
+ * Invoked when the visual state is ready to be drawn in the next {@link #onDraw}.
+ *
+ * @param requestId The identifier passed to {@link #postVisualStateCallback} when this
+ * callback was posted.
+ */
+ public abstract void onComplete(long requestId);
+ }
+
+ /**
+ * Interface to listen for new pictures as they change.
+ *
+ * @deprecated This interface is now obsolete.
+ */
+ @Deprecated
+ public interface PictureListener {
+ /**
+ * Used to provide notification that the WebView's picture has changed.
+ * See {@link WebView#capturePicture} for details of the picture.
+ *
+ * @param view the WebView that owns the picture
+ * @param picture the new picture. Applications targeting
+ * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} or above
+ * will always receive a {@code null} Picture.
+ * @deprecated Deprecated due to internal changes.
+ */
+ @Deprecated
+ void onNewPicture(WebView view, @Nullable Picture picture);
+ }
+
+ public static class HitTestResult {
+ /**
+ * Default HitTestResult, where the target is unknown.
+ */
+ public static final int UNKNOWN_TYPE = 0;
+ /**
+ * @deprecated This type is no longer used.
+ */
+ @Deprecated
+ public static final int ANCHOR_TYPE = 1;
+ /**
+ * HitTestResult for hitting a phone number.
+ */
+ public static final int PHONE_TYPE = 2;
+ /**
+ * HitTestResult for hitting a map address.
+ */
+ public static final int GEO_TYPE = 3;
+ /**
+ * HitTestResult for hitting an email address.
+ */
+ public static final int EMAIL_TYPE = 4;
+ /**
+ * HitTestResult for hitting an HTML::img tag.
+ */
+ public static final int IMAGE_TYPE = 5;
+ /**
+ * @deprecated This type is no longer used.
+ */
+ @Deprecated
+ public static final int IMAGE_ANCHOR_TYPE = 6;
+ /**
+ * HitTestResult for hitting a HTML::a tag with src=http.
+ */
+ public static final int SRC_ANCHOR_TYPE = 7;
+ /**
+ * HitTestResult for hitting a HTML::a tag with src=http + HTML::img.
+ */
+ public static final int SRC_IMAGE_ANCHOR_TYPE = 8;
+ /**
+ * HitTestResult for hitting an edit text area.
+ */
+ public static final int EDIT_TEXT_TYPE = 9;
+
+ private int mType;
+ private String mExtra;
+
+ /**
+ * @hide Only for use by WebViewProvider implementations
+ */
+ @SystemApi
+ public HitTestResult() {
+ mType = UNKNOWN_TYPE;
+ }
+
+ /**
+ * @hide Only for use by WebViewProvider implementations
+ */
+ @SystemApi
+ public void setType(int type) {
+ mType = type;
+ }
+
+ /**
+ * @hide Only for use by WebViewProvider implementations
+ */
+ @SystemApi
+ public void setExtra(String extra) {
+ mExtra = extra;
+ }
+
+ /**
+ * Gets the type of the hit test result. See the XXX_TYPE constants
+ * defined in this class.
+ *
+ * @return the type of the hit test result
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Gets additional type-dependant information about the result. See
+ * {@link WebView#getHitTestResult()} for details. May either be {@code null}
+ * or contain extra information about this result.
+ *
+ * @return additional type-dependant information about the result
+ */
+ @Nullable
+ public String getExtra() {
+ return mExtra;
+ }
+ }
+
+ /**
+ * Constructs a new WebView with an Activity Context object.
+ *
+ * <p class="note"><b>Note:</b> WebView should always be instantiated with an Activity Context.
+ * If instantiated with an Application Context, WebView will be unable to provide several
+ * features, such as JavaScript dialogs and autofill.
+ *
+ * @param context an Activity Context to access application assets
*/
public WebView(Context context) {
this(context, null);
}
/**
- * Construct a new WebView with layout parameters.
- * @param context A Context object used to access application assets.
- * @param attrs An AttributeSet passed to our parent.
+ * Constructs a new WebView with layout parameters.
+ *
+ * @param context an Activity Context to access application assets
+ * @param attrs an AttributeSet passed to our parent
*/
public WebView(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.webViewStyle);
}
/**
- * Construct a new WebView with layout parameters and a default style.
- * @param context A Context object used to access application assets.
- * @param attrs An AttributeSet passed to our parent.
- * @param defStyle The default style resource ID.
+ * Constructs a new WebView with layout parameters and a default style.
+ *
+ * @param context an Activity Context to access application assets
+ * @param attrs an AttributeSet passed to our parent
+ * @param defStyleAttr an attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ */
+ public WebView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ /**
+ * Constructs a new WebView with layout parameters and a default style.
+ *
+ * @param context an Activity Context to access application assets
+ * @param attrs an AttributeSet passed to our parent
+ * @param defStyleAttr an attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param defStyleRes a resource identifier of a style resource that
+ * supplies default values for the view, used only if
+ * defStyleAttr is 0 or can not be found in the theme. Can be 0
+ * to not look for defaults.
*/
- public WebView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
+ public WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ this(context, attrs, defStyleAttr, defStyleRes, null, false);
}
-
- // START FAKE PUBLIC METHODS
-
+
+ /**
+ * Constructs a new WebView with layout parameters and a default style.
+ *
+ * @param context an Activity Context to access application assets
+ * @param attrs an AttributeSet passed to our parent
+ * @param defStyleAttr an attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param privateBrowsing whether this WebView will be initialized in
+ * private mode
+ *
+ * @deprecated Private browsing is no longer supported directly via
+ * WebView and will be removed in a future release. Prefer using
+ * {@link WebSettings}, {@link WebViewDatabase}, {@link CookieManager}
+ * and {@link WebStorage} for fine-grained control of privacy data.
+ */
+ @Deprecated
+ public WebView(Context context, AttributeSet attrs, int defStyleAttr,
+ boolean privateBrowsing) {
+ this(context, attrs, defStyleAttr, 0, null, privateBrowsing);
+ }
+
+ /**
+ * Constructs a new WebView with layout parameters, a default style and a set
+ * of custom JavaScript interfaces to be added to this WebView at initialization
+ * time. This guarantees that these interfaces will be available when the JS
+ * context is initialized.
+ *
+ * @param context an Activity Context to access application assets
+ * @param attrs an AttributeSet passed to our parent
+ * @param defStyleAttr an attribute in the current theme that contains a
+ * reference to a style resource that supplies default values for
+ * the view. Can be 0 to not look for defaults.
+ * @param javaScriptInterfaces a Map of interface names, as keys, and
+ * object implementing those interfaces, as
+ * values
+ * @param privateBrowsing whether this WebView will be initialized in
+ * private mode
+ * @hide This is used internally by dumprendertree, as it requires the JavaScript interfaces to
+ * be added synchronously, before a subsequent loadUrl call takes effect.
+ */
+ protected WebView(Context context, AttributeSet attrs, int defStyleAttr,
+ Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
+ this(context, attrs, defStyleAttr, 0, javaScriptInterfaces, privateBrowsing);
+ }
+
+ /**
+ * @hide
+ */
+ @SuppressWarnings("deprecation") // for super() call into deprecated base class constructor.
+ protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
+ Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ // WebView is important by default, unless app developer overrode attribute.
+ if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+ setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+ }
+
+ if (context == null) {
+ throw new IllegalArgumentException("Invalid context argument");
+ }
+ if (mWebViewThread == null) {
+ throw new RuntimeException(
+ "WebView cannot be initialized on a thread that has no Looper.");
+ }
+ sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >=
+ Build.VERSION_CODES.JELLY_BEAN_MR2;
+ checkThread();
+
+ ensureProviderCreated();
+ mProvider.init(javaScriptInterfaces, privateBrowsing);
+ // Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed.
+ CookieSyncManager.setGetInstanceIsAllowed();
+ }
+
+ /**
+ * Specifies whether the horizontal scrollbar has overlay style.
+ *
+ * @deprecated This method has no effect.
+ * @param overlay {@code true} if horizontal scrollbar should have overlay style
+ */
+ @Deprecated
public void setHorizontalScrollbarOverlay(boolean overlay) {
}
+ /**
+ * Specifies whether the vertical scrollbar has overlay style.
+ *
+ * @deprecated This method has no effect.
+ * @param overlay {@code true} if vertical scrollbar should have overlay style
+ */
+ @Deprecated
public void setVerticalScrollbarOverlay(boolean overlay) {
}
+ /**
+ * Gets whether horizontal scrollbar has overlay style.
+ *
+ * @deprecated This method is now obsolete.
+ * @return {@code true}
+ */
+ @Deprecated
public boolean overlayHorizontalScrollbar() {
- return false;
+ // The old implementation defaulted to true, so return true for consistency
+ return true;
}
+ /**
+ * Gets whether vertical scrollbar has overlay style.
+ *
+ * @deprecated This method is now obsolete.
+ * @return {@code false}
+ */
+ @Deprecated
public boolean overlayVerticalScrollbar() {
+ // The old implementation defaulted to false, so return false for consistency
return false;
}
+ /**
+ * Gets the visible height (in pixels) of the embedded title bar (if any).
+ *
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
+ public int getVisibleTitleHeight() {
+ checkThread();
+ return mProvider.getVisibleTitleHeight();
+ }
+
+ /**
+ * Gets the SSL certificate for the main top-level page or {@code null} if there is
+ * no certificate (the site is not secure).
+ *
+ * @return the SSL certificate for the main top-level page
+ */
+ @Nullable
+ public SslCertificate getCertificate() {
+ checkThread();
+ return mProvider.getCertificate();
+ }
+
+ /**
+ * Sets the SSL certificate for the main top-level page.
+ *
+ * @deprecated Calling this function has no useful effect, and will be
+ * ignored in future releases.
+ */
+ @Deprecated
+ public void setCertificate(SslCertificate certificate) {
+ checkThread();
+ mProvider.setCertificate(certificate);
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods called by activity
+ //-------------------------------------------------------------------------
+
+ /**
+ * Sets a username and password pair for the specified host. This data is
+ * used by the WebView to autocomplete username and password fields in web
+ * forms. Note that this is unrelated to the credentials used for HTTP
+ * authentication.
+ *
+ * @param host the host that required the credentials
+ * @param username the username for the given host
+ * @param password the password for the given host
+ * @see WebViewDatabase#clearUsernamePassword
+ * @see WebViewDatabase#hasUsernamePassword
+ * @deprecated Saving passwords in WebView will not be supported in future versions.
+ */
+ @Deprecated
public void savePassword(String host, String username, String password) {
+ checkThread();
+ mProvider.savePassword(host, username, password);
}
+ /**
+ * Stores HTTP authentication credentials for a given host and realm to the {@link WebViewDatabase}
+ * instance.
+ *
+ * @param host the host to which the credentials apply
+ * @param realm the realm to which the credentials apply
+ * @param username the username
+ * @param password the password
+ * @deprecated Use {@link WebViewDatabase#setHttpAuthUsernamePassword} instead
+ */
+ @Deprecated
public void setHttpAuthUsernamePassword(String host, String realm,
String username, String password) {
+ checkThread();
+ mProvider.setHttpAuthUsernamePassword(host, realm, username, password);
}
+ /**
+ * Retrieves HTTP authentication credentials for a given host and realm from the {@link
+ * WebViewDatabase} instance.
+ * @param host the host to which the credentials apply
+ * @param realm the realm to which the credentials apply
+ * @return the credentials as a String array, if found. The first element
+ * is the username and the second element is the password. {@code null} if
+ * no credentials are found.
+ * @deprecated Use {@link WebViewDatabase#getHttpAuthUsernamePassword} instead
+ */
+ @Deprecated
+ @Nullable
public String[] getHttpAuthUsernamePassword(String host, String realm) {
- return null;
+ checkThread();
+ return mProvider.getHttpAuthUsernamePassword(host, realm);
}
+ /**
+ * Destroys the internal state of this WebView. This method should be called
+ * after this WebView has been removed from the view system. No other
+ * methods may be called on this WebView after destroy.
+ */
public void destroy() {
+ checkThread();
+ mProvider.destroy();
}
+ /**
+ * Enables platform notifications of data state and proxy changes.
+ * Notifications are enabled by default.
+ *
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
public static void enablePlatformNotifications() {
+ // noop
}
+ /**
+ * Disables platform notifications of data state and proxy changes.
+ * Notifications are enabled by default.
+ *
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
public static void disablePlatformNotifications() {
+ // noop
+ }
+
+ /**
+ * Used only by internal tests to free up memory.
+ *
+ * @hide
+ */
+ public static void freeMemoryForTests() {
+ getFactory().getStatics().freeMemoryForTests();
+ }
+
+ /**
+ * Informs WebView of the network state. This is used to set
+ * the JavaScript property window.navigator.isOnline and
+ * generates the online/offline event as specified in HTML5, sec. 5.7.7
+ *
+ * @param networkUp a boolean indicating if network is available
+ */
+ public void setNetworkAvailable(boolean networkUp) {
+ checkThread();
+ mProvider.setNetworkAvailable(networkUp);
+ }
+
+ /**
+ * Saves the state of this WebView used in
+ * {@link android.app.Activity#onSaveInstanceState}. Please note that this
+ * method no longer stores the display data for this WebView. The previous
+ * behavior could potentially leak files if {@link #restoreState} was never
+ * called.
+ *
+ * @param outState the Bundle to store this WebView's state
+ * @return the same copy of the back/forward list used to save the state, {@code null} if the
+ * method fails.
+ */
+ @Nullable
+ public WebBackForwardList saveState(Bundle outState) {
+ checkThread();
+ return mProvider.saveState(outState);
+ }
+
+ /**
+ * Saves the current display data to the Bundle given. Used in conjunction
+ * with {@link #saveState}.
+ * @param b a Bundle to store the display data
+ * @param dest the file to store the serialized picture data. Will be
+ * overwritten with this WebView's picture data.
+ * @return {@code true} if the picture was successfully saved
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
+ public boolean savePicture(Bundle b, final File dest) {
+ checkThread();
+ return mProvider.savePicture(b, dest);
+ }
+
+ /**
+ * Restores the display data that was saved in {@link #savePicture}. Used in
+ * conjunction with {@link #restoreState}. Note that this will not work if
+ * this WebView is hardware accelerated.
+ *
+ * @param b a Bundle containing the saved display data
+ * @param src the file where the picture data was stored
+ * @return {@code true} if the picture was successfully restored
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
+ public boolean restorePicture(Bundle b, File src) {
+ checkThread();
+ return mProvider.restorePicture(b, src);
+ }
+
+ /**
+ * Restores the state of this WebView from the given Bundle. This method is
+ * intended for use in {@link android.app.Activity#onRestoreInstanceState}
+ * and should be called to restore the state of this WebView. If
+ * it is called after this WebView has had a chance to build state (load
+ * pages, create a back/forward list, etc.) there may be undesirable
+ * side-effects. Please note that this method no longer restores the
+ * display data for this WebView.
+ *
+ * @param inState the incoming Bundle of state
+ * @return the restored back/forward list or {@code null} if restoreState failed
+ */
+ @Nullable
+ public WebBackForwardList restoreState(Bundle inState) {
+ checkThread();
+ return mProvider.restoreState(inState);
+ }
+
+ /**
+ * Loads the given URL with the specified additional HTTP headers.
+ * <p>
+ * Also see compatibility note on {@link #evaluateJavascript}.
+ *
+ * @param url the URL of the resource to load
+ * @param additionalHttpHeaders the additional headers to be used in the
+ * HTTP request for this URL, specified as a map from name to
+ * value. Note that if this map contains any of the headers
+ * that are set by default by this WebView, such as those
+ * controlling caching, accept types or the User-Agent, their
+ * values may be overridden by this WebView's defaults.
+ */
+ public void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
+ checkThread();
+ mProvider.loadUrl(url, additionalHttpHeaders);
}
+ /**
+ * Loads the given URL.
+ * <p>
+ * Also see compatibility note on {@link #evaluateJavascript}.
+ *
+ * @param url the URL of the resource to load
+ */
public void loadUrl(String url) {
+ checkThread();
+ mProvider.loadUrl(url);
+ }
+
+ /**
+ * Loads the URL with postData using "POST" method into this WebView. If url
+ * is not a network URL, it will be loaded with {@link #loadUrl(String)}
+ * instead, ignoring the postData param.
+ *
+ * @param url the URL of the resource to load
+ * @param postData the data will be passed to "POST" request, which must be
+ * be "application/x-www-form-urlencoded" encoded.
+ */
+ public void postUrl(String url, byte[] postData) {
+ checkThread();
+ if (URLUtil.isNetworkUrl(url)) {
+ mProvider.postUrl(url, postData);
+ } else {
+ mProvider.loadUrl(url);
+ }
}
- public void loadData(String data, String mimeType, String encoding) {
+ /**
+ * Loads the given data into this WebView using a 'data' scheme URL.
+ * <p>
+ * Note that JavaScript's same origin policy means that script running in a
+ * page loaded using this method will be unable to access content loaded
+ * using any scheme other than 'data', including 'http(s)'. To avoid this
+ * restriction, use {@link
+ * #loadDataWithBaseURL(String,String,String,String,String)
+ * loadDataWithBaseURL()} with an appropriate base URL.
+ * <p>
+ * The {@code encoding} parameter specifies whether the data is base64 or URL
+ * encoded. If the data is base64 encoded, the value of the encoding
+ * parameter must be 'base64'. HTML can be encoded with {@link
+ * android.util.Base64#encodeToString(byte[],int)} like so:
+ * <pre>
+ * String unencodedHtml =
+ * "&lt;html&gt;&lt;body&gt;'%28' is the code for '('&lt;/body&gt;&lt;/html&gt;";
+ * String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(), Base64.NO_PADDING);
+ * webView.loadData(encodedHtml, "text/html", "base64");
+ * </pre>
+ * <p>
+ * For all other values of {@code encoding} (including {@code null}) it is assumed that the
+ * data uses ASCII encoding for octets inside the range of safe URL characters and use the
+ * standard %xx hex encoding of URLs for octets outside that range. See <a
+ * href="https://tools.ietf.org/html/rfc3986#section-2.2">RFC 3986</a> for more information.
+ * <p>
+ * The {@code mimeType} parameter specifies the format of the data.
+ * If WebView can't handle the specified MIME type, it will download the data.
+ * If {@code null}, defaults to 'text/html'.
+ * <p>
+ * The 'data' scheme URL formed by this method uses the default US-ASCII
+ * charset. If you need need to set a different charset, you should form a
+ * 'data' scheme URL which explicitly specifies a charset parameter in the
+ * mediatype portion of the URL and call {@link #loadUrl(String)} instead.
+ * Note that the charset obtained from the mediatype portion of a data URL
+ * always overrides that specified in the HTML or XML document itself.
+ *
+ * @param data a String of data in the given encoding
+ * @param mimeType the MIME type of the data, e.g. 'text/html'.
+ * @param encoding the encoding of the data
+ */
+ public void loadData(String data, @Nullable String mimeType, @Nullable String encoding) {
+ checkThread();
+ mProvider.loadData(data, mimeType, encoding);
}
- public void loadDataWithBaseURL(String baseUrl, String data,
- String mimeType, String encoding, String failUrl) {
+ /**
+ * Loads the given data into this WebView, using baseUrl as the base URL for
+ * the content. The base URL is used both to resolve relative URLs and when
+ * applying JavaScript's same origin policy. The historyUrl is used for the
+ * history entry.
+ * <p>
+ * The {@code mimeType} parameter specifies the format of the data.
+ * If WebView can't handle the specified MIME type, it will download the data.
+ * If {@code null}, defaults to 'text/html'.
+ * <p>
+ * Note that content specified in this way can access local device files
+ * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than
+ * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'.
+ * <p>
+ * If the base URL uses the data scheme, this method is equivalent to
+ * calling {@link #loadData(String,String,String) loadData()} and the
+ * historyUrl is ignored, and the data will be treated as part of a data: URL.
+ * If the base URL uses any other scheme, then the data will be loaded into
+ * the WebView as a plain string (i.e. not part of a data URL) and any URL-encoded
+ * entities in the string will not be decoded.
+ * <p>
+ * Note that the baseUrl is sent in the 'Referer' HTTP header when
+ * requesting subresources (images, etc.) of the page loaded using this method.
+ *
+ * @param baseUrl the URL to use as the page's base URL. If {@code null} defaults to
+ * 'about:blank'.
+ * @param data a String of data in the given encoding
+ * @param mimeType the MIME type of the data, e.g. 'text/html'.
+ * @param encoding the encoding of the data
+ * @param historyUrl the URL to use as the history entry. If {@code null} defaults
+ * to 'about:blank'. If non-null, this must be a valid URL.
+ */
+ public void loadDataWithBaseURL(@Nullable String baseUrl, String data,
+ @Nullable String mimeType, @Nullable String encoding, @Nullable String historyUrl) {
+ checkThread();
+ mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
}
+ /**
+ * Asynchronously evaluates JavaScript in the context of the currently displayed page.
+ * If non-null, |resultCallback| will be invoked with any result returned from that
+ * execution. This method must be called on the UI thread and the callback will
+ * be made on the UI thread.
+ * <p>
+ * Compatibility note. Applications targeting {@link android.os.Build.VERSION_CODES#N} or
+ * later, JavaScript state from an empty WebView is no longer persisted across navigations like
+ * {@link #loadUrl(String)}. For example, global variables and functions defined before calling
+ * {@link #loadUrl(String)} will not exist in the loaded page. Applications should use
+ * {@link #addJavascriptInterface} instead to persist JavaScript objects across navigations.
+ *
+ * @param script the JavaScript to execute.
+ * @param resultCallback A callback to be invoked when the script execution
+ * completes with the result of the execution (if any).
+ * May be {@code null} if no notification of the result is required.
+ */
+ public void evaluateJavascript(String script, @Nullable ValueCallback<String> resultCallback) {
+ checkThread();
+ mProvider.evaluateJavaScript(script, resultCallback);
+ }
+
+ /**
+ * Saves the current view as a web archive.
+ *
+ * @param filename the filename where the archive should be placed
+ */
+ public void saveWebArchive(String filename) {
+ checkThread();
+ mProvider.saveWebArchive(filename);
+ }
+
+ /**
+ * Saves the current view as a web archive.
+ *
+ * @param basename the filename where the archive should be placed
+ * @param autoname if {@code false}, takes basename to be a file. If {@code true}, basename
+ * is assumed to be a directory in which a filename will be
+ * chosen according to the URL of the current page.
+ * @param callback called after the web archive has been saved. The
+ * parameter for onReceiveValue will either be the filename
+ * under which the file was saved, or {@code null} if saving the
+ * file failed.
+ */
+ public void saveWebArchive(String basename, boolean autoname, @Nullable ValueCallback<String>
+ callback) {
+ checkThread();
+ mProvider.saveWebArchive(basename, autoname, callback);
+ }
+
+ /**
+ * Stops the current load.
+ */
public void stopLoading() {
+ checkThread();
+ mProvider.stopLoading();
}
+ /**
+ * Reloads the current URL.
+ */
public void reload() {
+ checkThread();
+ mProvider.reload();
}
+ /**
+ * Gets whether this WebView has a back history item.
+ *
+ * @return {@code true} if this WebView has a back history item
+ */
public boolean canGoBack() {
- return false;
+ checkThread();
+ return mProvider.canGoBack();
}
+ /**
+ * Goes back in the history of this WebView.
+ */
public void goBack() {
+ checkThread();
+ mProvider.goBack();
}
+ /**
+ * Gets whether this WebView has a forward history item.
+ *
+ * @return {@code true} if this WebView has a forward history item
+ */
public boolean canGoForward() {
- return false;
+ checkThread();
+ return mProvider.canGoForward();
}
+ /**
+ * Goes forward in the history of this WebView.
+ */
public void goForward() {
+ checkThread();
+ mProvider.goForward();
}
+ /**
+ * Gets whether the page can go back or forward the given
+ * number of steps.
+ *
+ * @param steps the negative or positive number of steps to move the
+ * history
+ */
public boolean canGoBackOrForward(int steps) {
- return false;
+ checkThread();
+ return mProvider.canGoBackOrForward(steps);
}
+ /**
+ * Goes to the history item that is the number of steps away from
+ * the current item. Steps is negative if backward and positive
+ * if forward.
+ *
+ * @param steps the number of steps to take back or forward in the back
+ * forward list
+ */
public void goBackOrForward(int steps) {
+ checkThread();
+ mProvider.goBackOrForward(steps);
}
+ /**
+ * Gets whether private browsing is enabled in this WebView.
+ */
+ public boolean isPrivateBrowsingEnabled() {
+ checkThread();
+ return mProvider.isPrivateBrowsingEnabled();
+ }
+
+ /**
+ * Scrolls the contents of this WebView up by half the view size.
+ *
+ * @param top {@code true} to jump to the top of the page
+ * @return {@code true} if the page was scrolled
+ */
public boolean pageUp(boolean top) {
- return false;
+ checkThread();
+ return mProvider.pageUp(top);
}
-
+
+ /**
+ * Scrolls the contents of this WebView down by half the page size.
+ *
+ * @param bottom {@code true} to jump to bottom of page
+ * @return {@code true} if the page was scrolled
+ */
public boolean pageDown(boolean bottom) {
- return false;
+ checkThread();
+ return mProvider.pageDown(bottom);
}
+ /**
+ * Posts a {@link VisualStateCallback}, which will be called when
+ * the current state of the WebView is ready to be drawn.
+ *
+ * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not
+ * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The
+ * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents of
+ * the DOM at the current time are ready to be drawn the next time the {@link WebView}
+ * draws.
+ *
+ * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the
+ * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may also
+ * contain updates applied after the callback was posted.
+ *
+ * <p>The state of the DOM covered by this API includes the following:
+ * <ul>
+ * <li>primitive HTML elements (div, img, span, etc..)</li>
+ * <li>images</li>
+ * <li>CSS animations</li>
+ * <li>WebGL</li>
+ * <li>canvas</li>
+ * </ul>
+ * It does not include the state of:
+ * <ul>
+ * <li>the video tag</li>
+ * </ul>
+ *
+ * <p>To guarantee that the {@link WebView} will successfully render the first frame
+ * after the {@link VisualStateCallback#onComplete} method has been called a set of conditions
+ * must be met:
+ * <ul>
+ * <li>If the {@link WebView}'s visibility is set to {@link View#VISIBLE VISIBLE} then
+ * the {@link WebView} must be attached to the view hierarchy.</li>
+ * <li>If the {@link WebView}'s visibility is set to {@link View#INVISIBLE INVISIBLE}
+ * then the {@link WebView} must be attached to the view hierarchy and must be made
+ * {@link View#VISIBLE VISIBLE} from the {@link VisualStateCallback#onComplete} method.</li>
+ * <li>If the {@link WebView}'s visibility is set to {@link View#GONE GONE} then the
+ * {@link WebView} must be attached to the view hierarchy and its
+ * {@link AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be set to fixed
+ * values and must be made {@link View#VISIBLE VISIBLE} from the
+ * {@link VisualStateCallback#onComplete} method.</li>
+ * </ul>
+ *
+ * <p>When using this API it is also recommended to enable pre-rasterization if the {@link
+ * WebView} is off screen to avoid flickering. See {@link WebSettings#setOffscreenPreRaster} for
+ * more details and do consider its caveats.
+ *
+ * @param requestId An id that will be returned in the callback to allow callers to match
+ * requests with callbacks.
+ * @param callback The callback to be invoked.
+ */
+ public void postVisualStateCallback(long requestId, VisualStateCallback callback) {
+ checkThread();
+ mProvider.insertVisualStateCallback(requestId, callback);
+ }
+
+ /**
+ * Clears this WebView so that onDraw() will draw nothing but white background,
+ * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY.
+ * @deprecated Use WebView.loadUrl("about:blank") to reliably reset the view state
+ * and release page resources (including any running JavaScript).
+ */
+ @Deprecated
public void clearView() {
+ checkThread();
+ mProvider.clearView();
}
-
+
+ /**
+ * Gets a new picture that captures the current contents of this WebView.
+ * The picture is of the entire document being displayed, and is not
+ * limited to the area currently displayed by this WebView. Also, the
+ * picture is a static copy and is unaffected by later changes to the
+ * content being displayed.
+ * <p>
+ * Note that due to internal changes, for API levels between
+ * {@link android.os.Build.VERSION_CODES#HONEYCOMB} and
+ * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} inclusive, the
+ * picture does not include fixed position elements or scrollable divs.
+ * <p>
+ * Note that from {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1} the returned picture
+ * should only be drawn into bitmap-backed Canvas - using any other type of Canvas will involve
+ * additional conversion at a cost in memory and performance. Also the
+ * {@link android.graphics.Picture#createFromStream} and
+ * {@link android.graphics.Picture#writeToStream} methods are not supported on the
+ * returned object.
+ *
+ * @deprecated Use {@link #onDraw} to obtain a bitmap snapshot of the WebView, or
+ * {@link #saveWebArchive} to save the content to a file.
+ *
+ * @return a picture that captures the current contents of this WebView
+ */
+ @Deprecated
public Picture capturePicture() {
- return null;
+ checkThread();
+ return mProvider.capturePicture();
}
+ /**
+ * @deprecated Use {@link #createPrintDocumentAdapter(String)} which requires user
+ * to provide a print document name.
+ */
+ @Deprecated
+ public PrintDocumentAdapter createPrintDocumentAdapter() {
+ checkThread();
+ return mProvider.createPrintDocumentAdapter("default");
+ }
+
+ /**
+ * Creates a PrintDocumentAdapter that provides the content of this WebView for printing.
+ *
+ * The adapter works by converting the WebView contents to a PDF stream. The WebView cannot
+ * be drawn during the conversion process - any such draws are undefined. It is recommended
+ * to use a dedicated off screen WebView for the printing. If necessary, an application may
+ * temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance
+ * wrapped around the object returned and observing the onStart and onFinish methods. See
+ * {@link android.print.PrintDocumentAdapter} for more information.
+ *
+ * @param documentName The user-facing name of the printed document. See
+ * {@link android.print.PrintDocumentInfo}
+ */
+ public PrintDocumentAdapter createPrintDocumentAdapter(String documentName) {
+ checkThread();
+ return mProvider.createPrintDocumentAdapter(documentName);
+ }
+
+ /**
+ * Gets the current scale of this WebView.
+ *
+ * @return the current scale
+ *
+ * @deprecated This method is prone to inaccuracy due to race conditions
+ * between the web rendering and UI threads; prefer
+ * {@link WebViewClient#onScaleChanged}.
+ */
+ @Deprecated
+ @ViewDebug.ExportedProperty(category = "webview")
public float getScale() {
- return 0;
+ checkThread();
+ return mProvider.getScale();
}
+ /**
+ * Sets the initial scale for this WebView. 0 means default.
+ * The behavior for the default scale depends on the state of
+ * {@link WebSettings#getUseWideViewPort()} and
+ * {@link WebSettings#getLoadWithOverviewMode()}.
+ * If the content fits into the WebView control by width, then
+ * the zoom is set to 100%. For wide content, the behavior
+ * depends on the state of {@link WebSettings#getLoadWithOverviewMode()}.
+ * If its value is {@code true}, the content will be zoomed out to be fit
+ * by width into the WebView control, otherwise not.
+ *
+ * If initial scale is greater than 0, WebView starts with this value
+ * as initial scale.
+ * Please note that unlike the scale properties in the viewport meta tag,
+ * this method doesn't take the screen density into account.
+ *
+ * @param scaleInPercent the initial scale in percent
+ */
public void setInitialScale(int scaleInPercent) {
+ checkThread();
+ mProvider.setInitialScale(scaleInPercent);
}
+ /**
+ * Invokes the graphical zoom picker widget for this WebView. This will
+ * result in the zoom widget appearing on the screen to control the zoom
+ * level of this WebView.
+ */
public void invokeZoomPicker() {
+ checkThread();
+ mProvider.invokeZoomPicker();
}
- public void requestFocusNodeHref(Message hrefMsg) {
+ /**
+ * Gets a HitTestResult based on the current cursor node. If a HTML::a
+ * tag is found and the anchor has a non-JavaScript URL, the HitTestResult
+ * type is set to SRC_ANCHOR_TYPE and the URL is set in the "extra" field.
+ * If the anchor does not have a URL or if it is a JavaScript URL, the type
+ * will be UNKNOWN_TYPE and the URL has to be retrieved through
+ * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is
+ * found, the HitTestResult type is set to IMAGE_TYPE and the URL is set in
+ * the "extra" field. A type of
+ * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a URL that has an image as
+ * a child node. If a phone number is found, the HitTestResult type is set
+ * to PHONE_TYPE and the phone number is set in the "extra" field of
+ * HitTestResult. If a map address is found, the HitTestResult type is set
+ * to GEO_TYPE and the address is set in the "extra" field of HitTestResult.
+ * If an email address is found, the HitTestResult type is set to EMAIL_TYPE
+ * and the email is set in the "extra" field of HitTestResult. Otherwise,
+ * HitTestResult type is set to UNKNOWN_TYPE.
+ */
+ public HitTestResult getHitTestResult() {
+ checkThread();
+ return mProvider.getHitTestResult();
}
+ /**
+ * Requests the anchor or image element URL at the last tapped point.
+ * If hrefMsg is {@code null}, this method returns immediately and does not
+ * dispatch hrefMsg to its target. If the tapped point hits an image,
+ * an anchor, or an image in an anchor, the message associates
+ * strings in named keys in its data. The value paired with the key
+ * may be an empty string.
+ *
+ * @param hrefMsg the message to be dispatched with the result of the
+ * request. The message data contains three keys. "url"
+ * returns the anchor's href attribute. "title" returns the
+ * anchor's text. "src" returns the image's src attribute.
+ */
+ public void requestFocusNodeHref(@Nullable Message hrefMsg) {
+ checkThread();
+ mProvider.requestFocusNodeHref(hrefMsg);
+ }
+
+ /**
+ * Requests the URL of the image last touched by the user. msg will be sent
+ * to its target with a String representing the URL as its object.
+ *
+ * @param msg the message to be dispatched with the result of the request
+ * as the data member with "url" as key. The result can be {@code null}.
+ */
public void requestImageRef(Message msg) {
+ checkThread();
+ mProvider.requestImageRef(msg);
}
+ /**
+ * Gets the URL for the current page. This is not always the same as the URL
+ * passed to WebViewClient.onPageStarted because although the load for
+ * that URL has begun, the current page may not have changed.
+ *
+ * @return the URL for the current page
+ */
+ @ViewDebug.ExportedProperty(category = "webview")
public String getUrl() {
- return null;
+ checkThread();
+ return mProvider.getUrl();
}
+ /**
+ * Gets the original URL for the current page. This is not always the same
+ * as the URL passed to WebViewClient.onPageStarted because although the
+ * load for that URL has begun, the current page may not have changed.
+ * Also, there may have been redirects resulting in a different URL to that
+ * originally requested.
+ *
+ * @return the URL that was originally requested for the current page
+ */
+ @ViewDebug.ExportedProperty(category = "webview")
+ public String getOriginalUrl() {
+ checkThread();
+ return mProvider.getOriginalUrl();
+ }
+
+ /**
+ * Gets the title for the current page. This is the title of the current page
+ * until WebViewClient.onReceivedTitle is called.
+ *
+ * @return the title for the current page
+ */
+ @ViewDebug.ExportedProperty(category = "webview")
public String getTitle() {
- return null;
+ checkThread();
+ return mProvider.getTitle();
}
+ /**
+ * Gets the favicon for the current page. This is the favicon of the current
+ * page until WebViewClient.onReceivedIcon is called.
+ *
+ * @return the favicon for the current page
+ */
public Bitmap getFavicon() {
- return null;
+ checkThread();
+ return mProvider.getFavicon();
+ }
+
+ /**
+ * Gets the touch icon URL for the apple-touch-icon <link> element, or
+ * a URL on this site's server pointing to the standard location of a
+ * touch icon.
+ *
+ * @hide
+ */
+ public String getTouchIconUrl() {
+ return mProvider.getTouchIconUrl();
}
+ /**
+ * Gets the progress for the current page.
+ *
+ * @return the progress for the current page between 0 and 100
+ */
public int getProgress() {
- return 0;
+ checkThread();
+ return mProvider.getProgress();
}
-
+
+ /**
+ * Gets the height of the HTML content.
+ *
+ * @return the height of the HTML content
+ */
+ @ViewDebug.ExportedProperty(category = "webview")
public int getContentHeight() {
- return 0;
+ checkThread();
+ return mProvider.getContentHeight();
+ }
+
+ /**
+ * Gets the width of the HTML content.
+ *
+ * @return the width of the HTML content
+ * @hide
+ */
+ @ViewDebug.ExportedProperty(category = "webview")
+ public int getContentWidth() {
+ return mProvider.getContentWidth();
}
+ /**
+ * Pauses all layout, parsing, and JavaScript timers for all WebViews. This
+ * is a global requests, not restricted to just this WebView. This can be
+ * useful if the application has been paused.
+ */
public void pauseTimers() {
+ checkThread();
+ mProvider.pauseTimers();
}
+ /**
+ * Resumes all layout, parsing, and JavaScript timers for all WebViews.
+ * This will resume dispatching all timers.
+ */
public void resumeTimers() {
+ checkThread();
+ mProvider.resumeTimers();
}
- public void clearCache() {
+ /**
+ * Does a best-effort attempt to pause any processing that can be paused
+ * safely, such as animations and geolocation. Note that this call
+ * does not pause JavaScript. To pause JavaScript globally, use
+ * {@link #pauseTimers}.
+ *
+ * To resume WebView, call {@link #onResume}.
+ */
+ public void onPause() {
+ checkThread();
+ mProvider.onPause();
}
+ /**
+ * Resumes a WebView after a previous call to {@link #onPause}.
+ */
+ public void onResume() {
+ checkThread();
+ mProvider.onResume();
+ }
+
+ /**
+ * Gets whether this WebView is paused, meaning onPause() was called.
+ * Calling onResume() sets the paused state back to {@code false}.
+ *
+ * @hide
+ */
+ public boolean isPaused() {
+ return mProvider.isPaused();
+ }
+
+ /**
+ * Informs this WebView that memory is low so that it can free any available
+ * memory.
+ * @deprecated Memory caches are automatically dropped when no longer needed, and in response
+ * to system memory pressure.
+ */
+ @Deprecated
+ public void freeMemory() {
+ checkThread();
+ mProvider.freeMemory();
+ }
+
+ /**
+ * Clears the resource cache. Note that the cache is per-application, so
+ * this will clear the cache for all WebViews used.
+ *
+ * @param includeDiskFiles if {@code false}, only the RAM cache is cleared
+ */
+ public void clearCache(boolean includeDiskFiles) {
+ checkThread();
+ mProvider.clearCache(includeDiskFiles);
+ }
+
+ /**
+ * Removes the autocomplete popup from the currently focused form field, if
+ * present. Note this only affects the display of the autocomplete popup,
+ * it does not remove any saved form data from this WebView's store. To do
+ * that, use {@link WebViewDatabase#clearFormData}.
+ */
public void clearFormData() {
+ checkThread();
+ mProvider.clearFormData();
}
+ /**
+ * Tells this WebView to clear its internal back/forward list.
+ */
public void clearHistory() {
+ checkThread();
+ mProvider.clearHistory();
}
+ /**
+ * Clears the SSL preferences table stored in response to proceeding with
+ * SSL certificate errors.
+ */
public void clearSslPreferences() {
+ checkThread();
+ mProvider.clearSslPreferences();
+ }
+
+ /**
+ * Clears the client certificate preferences stored in response
+ * to proceeding/cancelling client cert requests. Note that WebView
+ * automatically clears these preferences when it receives a
+ * {@link KeyChain#ACTION_STORAGE_CHANGED} intent. The preferences are
+ * shared by all the WebViews that are created by the embedder application.
+ *
+ * @param onCleared A runnable to be invoked when client certs are cleared.
+ * The runnable will be called in UI thread.
+ */
+ public static void clearClientCertPreferences(@Nullable Runnable onCleared) {
+ getFactory().getStatics().clearClientCertPreferences(onCleared);
}
+ /**
+ * Starts Safe Browsing initialization.
+ * <p>
+ * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is
+ * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those
+ * devices {@code callback} will receive {@code false}.
+ * <p>
+ * This should not be called if Safe Browsing has been disabled by manifest tag or {@link
+ * WebSettings#setSafeBrowsingEnabled}. This prepares resources used for Safe Browsing.
+ * <p>
+ * This should be called with the Application Context (and will always use the Application
+ * context to do its work regardless).
+ *
+ * @param context Application Context.
+ * @param callback will be called on the UI thread with {@code true} if initialization is
+ * successful, {@code false} otherwise.
+ */
+ public static void startSafeBrowsing(@NonNull Context context,
+ @Nullable ValueCallback<Boolean> callback) {
+ getFactory().getStatics().initSafeBrowsing(context, callback);
+ }
+
+ /**
+ * Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks.
+ * The list is global for all the WebViews.
+ * <p>
+ * Each rule should take one of these:
+ * <table>
+ * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr>
+ * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr>
+ * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr>
+ * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr>
+ * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr>
+ * </table>
+ * <p>
+ * All other rules, including wildcards, are invalid.
+ * <p>
+ * The correct syntax for hosts is defined by <a
+ * href="https://tools.ietf.org/html/rfc3986#section-3.2.2">RFC 3986</a>.
+ *
+ * @param hosts the list of hosts
+ * @param callback will be called with {@code true} if hosts are successfully added to the
+ * whitelist. It will be called with {@code false} if any hosts are malformed. The callback
+ * will be run on the UI thread
+ */
+ public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts,
+ @Nullable ValueCallback<Boolean> callback) {
+ getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback);
+ }
+
+ /**
+ * Returns a URL pointing to the privacy policy for Safe Browsing reporting.
+ *
+ * @return the url pointing to a privacy policy document which can be displayed to users.
+ */
+ @NonNull
+ public static Uri getSafeBrowsingPrivacyPolicyUrl() {
+ return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
+ }
+
+ /**
+ * Gets the WebBackForwardList for this WebView. This contains the
+ * back/forward list for use in querying each item in the history stack.
+ * This is a copy of the private WebBackForwardList so it contains only a
+ * snapshot of the current state. Multiple calls to this method may return
+ * different objects. The object returned from this method will not be
+ * updated to reflect any new state.
+ */
+ public WebBackForwardList copyBackForwardList() {
+ checkThread();
+ return mProvider.copyBackForwardList();
+
+ }
+
+ /**
+ * Registers the listener to be notified as find-on-page operations
+ * progress. This will replace the current listener.
+ *
+ * @param listener an implementation of {@link FindListener}
+ */
+ public void setFindListener(FindListener listener) {
+ checkThread();
+ setupFindListenerIfNeeded();
+ mFindListener.mUserFindListener = listener;
+ }
+
+ /**
+ * Highlights and scrolls to the next match found by
+ * {@link #findAllAsync}, wrapping around page boundaries as necessary.
+ * Notifies any registered {@link FindListener}. If {@link #findAllAsync(String)}
+ * has not been called yet, or if {@link #clearMatches} has been called since the
+ * last find operation, this function does nothing.
+ *
+ * @param forward the direction to search
+ * @see #setFindListener
+ */
+ public void findNext(boolean forward) {
+ checkThread();
+ mProvider.findNext(forward);
+ }
+
+ /**
+ * Finds all instances of find on the page and highlights them.
+ * Notifies any registered {@link FindListener}.
+ *
+ * @param find the string to find
+ * @return the number of occurrences of the String "find" that were found
+ * @deprecated {@link #findAllAsync} is preferred.
+ * @see #setFindListener
+ */
+ @Deprecated
+ public int findAll(String find) {
+ checkThread();
+ StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync");
+ return mProvider.findAll(find);
+ }
+
+ /**
+ * Finds all instances of find on the page and highlights them,
+ * asynchronously. Notifies any registered {@link FindListener}.
+ * Successive calls to this will cancel any pending searches.
+ *
+ * @param find the string to find.
+ * @see #setFindListener
+ */
+ public void findAllAsync(String find) {
+ checkThread();
+ mProvider.findAllAsync(find);
+ }
+
+ /**
+ * Starts an ActionMode for finding text in this WebView. Only works if this
+ * WebView is attached to the view system.
+ *
+ * @param text if non-null, will be the initial text to search for.
+ * Otherwise, the last String searched for in this WebView will
+ * be used to start.
+ * @param showIme if {@code true}, show the IME, assuming the user will begin typing.
+ * If {@code false} and text is non-null, perform a find all.
+ * @return {@code true} if the find dialog is shown, {@code false} otherwise
+ * @deprecated This method does not work reliably on all Android versions;
+ * implementing a custom find dialog using WebView.findAllAsync()
+ * provides a more robust solution.
+ */
+ @Deprecated
+ public boolean showFindDialog(@Nullable String text, boolean showIme) {
+ checkThread();
+ return mProvider.showFindDialog(text, showIme);
+ }
+
+ /**
+ * Gets the first substring consisting of the address of a physical
+ * location. Currently, only addresses in the United States are detected,
+ * and consist of:
+ * <ul>
+ * <li>a house number</li>
+ * <li>a street name</li>
+ * <li>a street type (Road, Circle, etc), either spelled out or
+ * abbreviated</li>
+ * <li>a city name</li>
+ * <li>a state or territory, either spelled out or two-letter abbr</li>
+ * <li>an optional 5 digit or 9 digit zip code</li>
+ * </ul>
+ * All names must be correctly capitalized, and the zip code, if present,
+ * must be valid for the state. The street type must be a standard USPS
+ * spelling or abbreviation. The state or territory must also be spelled
+ * or abbreviated using USPS standards. The house number may not exceed
+ * five digits.
+ *
+ * @param addr the string to search for addresses
+ * @return the address, or if no address is found, {@code null}
+ * @deprecated this method is superseded by {@link TextClassifier#generateLinks(
+ * android.view.textclassifier.TextLinks.Request)}. Avoid using this method even when targeting
+ * API levels where no alternative is available.
+ */
+ @Nullable
+ @Deprecated
public static String findAddress(String addr) {
- return null;
+ if (addr == null) {
+ throw new NullPointerException("addr is null");
+ }
+ return FindAddress.findAddress(addr);
+ }
+
+ /**
+ * For apps targeting the L release, WebView has a new default behavior that reduces
+ * memory footprint and increases performance by intelligently choosing
+ * the portion of the HTML document that needs to be drawn. These
+ * optimizations are transparent to the developers. However, under certain
+ * circumstances, an App developer may want to disable them:
+ * <ol>
+ * <li>When an app uses {@link #onDraw} to do own drawing and accesses portions
+ * of the page that is way outside the visible portion of the page.</li>
+ * <li>When an app uses {@link #capturePicture} to capture a very large HTML document.
+ * Note that capturePicture is a deprecated API.</li>
+ * </ol>
+ * Enabling drawing the entire HTML document has a significant performance
+ * cost. This method should be called before any WebViews are created.
+ */
+ public static void enableSlowWholeDocumentDraw() {
+ getFactory().getStatics().enableSlowWholeDocumentDraw();
+ }
+
+ /**
+ * Clears the highlighting surrounding text matches created by
+ * {@link #findAllAsync}.
+ */
+ public void clearMatches() {
+ checkThread();
+ mProvider.clearMatches();
}
+ /**
+ * Queries the document to see if it contains any image references. The
+ * message object will be dispatched with arg1 being set to 1 if images
+ * were found and 0 if the document does not reference any images.
+ *
+ * @param response the message that will be dispatched with the result
+ */
public void documentHasImages(Message response) {
+ checkThread();
+ mProvider.documentHasImages(response);
}
+ /**
+ * Sets the WebViewClient that will receive various notifications and
+ * requests. This will replace the current handler.
+ *
+ * @param client an implementation of WebViewClient
+ * @see #getWebViewClient
+ */
public void setWebViewClient(WebViewClient client) {
+ checkThread();
+ mProvider.setWebViewClient(client);
+ }
+
+ /**
+ * Gets the WebViewClient.
+ *
+ * @return the WebViewClient, or a default client if not yet set
+ * @see #setWebViewClient
+ */
+ public WebViewClient getWebViewClient() {
+ checkThread();
+ return mProvider.getWebViewClient();
}
+ /**
+ * Registers the interface to be used when content can not be handled by
+ * the rendering engine, and should be downloaded instead. This will replace
+ * the current handler.
+ *
+ * @param listener an implementation of DownloadListener
+ */
public void setDownloadListener(DownloadListener listener) {
+ checkThread();
+ mProvider.setDownloadListener(listener);
}
+ /**
+ * Sets the chrome handler. This is an implementation of WebChromeClient for
+ * use in handling JavaScript dialogs, favicons, titles, and the progress.
+ * This will replace the current handler.
+ *
+ * @param client an implementation of WebChromeClient
+ * @see #getWebChromeClient
+ */
public void setWebChromeClient(WebChromeClient client) {
+ checkThread();
+ mProvider.setWebChromeClient(client);
+ }
+
+ /**
+ * Gets the chrome handler.
+ *
+ * @return the WebChromeClient, or {@code null} if not yet set
+ * @see #setWebChromeClient
+ */
+ @Nullable
+ public WebChromeClient getWebChromeClient() {
+ checkThread();
+ return mProvider.getWebChromeClient();
+ }
+
+ /**
+ * Sets the Picture listener. This is an interface used to receive
+ * notifications of a new Picture.
+ *
+ * @param listener an implementation of WebView.PictureListener
+ * @deprecated This method is now obsolete.
+ */
+ @Deprecated
+ public void setPictureListener(PictureListener listener) {
+ checkThread();
+ mProvider.setPictureListener(listener);
+ }
+
+ /**
+ * Injects the supplied Java object into this WebView. The object is
+ * injected into the JavaScript context of the main frame, using the
+ * supplied name. This allows the Java object's methods to be
+ * accessed from JavaScript. For applications targeted to API
+ * level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ * and above, only public methods that are annotated with
+ * {@link android.webkit.JavascriptInterface} can be accessed from JavaScript.
+ * For applications targeted to API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or below,
+ * all public methods (including the inherited ones) can be accessed, see the
+ * important security note below for implications.
+ * <p> Note that injected objects will not appear in JavaScript until the page is next
+ * (re)loaded. JavaScript should be enabled before injecting the object. For example:
+ * <pre>
+ * class JsObject {
+ * {@literal @}JavascriptInterface
+ * public String toString() { return "injectedObject"; }
+ * }
+ * webview.getSettings().setJavaScriptEnabled(true);
+ * webView.addJavascriptInterface(new JsObject(), "injectedObject");
+ * webView.loadData("<!DOCTYPE html><title></title>", "text/html", null);
+ * webView.loadUrl("javascript:alert(injectedObject.toString())");</pre>
+ * <p>
+ * <strong>IMPORTANT:</strong>
+ * <ul>
+ * <li> This method can be used to allow JavaScript to control the host
+ * application. This is a powerful feature, but also presents a security
+ * risk for apps targeting {@link android.os.Build.VERSION_CODES#JELLY_BEAN} or earlier.
+ * Apps that target a version later than {@link android.os.Build.VERSION_CODES#JELLY_BEAN}
+ * are still vulnerable if the app runs on a device running Android earlier than 4.2.
+ * The most secure way to use this method is to target {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ * and to ensure the method is called only when running on Android 4.2 or later.
+ * With these older versions, JavaScript could use reflection to access an
+ * injected object's public fields. Use of this method in a WebView
+ * containing untrusted content could allow an attacker to manipulate the
+ * host application in unintended ways, executing Java code with the
+ * permissions of the host application. Use extreme care when using this
+ * method in a WebView which could contain untrusted content.</li>
+ * <li> JavaScript interacts with Java object on a private, background
+ * thread of this WebView. Care is therefore required to maintain thread
+ * safety.
+ * </li>
+ * <li> The Java object's fields are not accessible.</li>
+ * <li> For applications targeted to API level {@link android.os.Build.VERSION_CODES#LOLLIPOP}
+ * and above, methods of injected Java objects are enumerable from
+ * JavaScript.</li>
+ * </ul>
+ *
+ * @param object the Java object to inject into this WebView's JavaScript
+ * context. {@code null} values are ignored.
+ * @param name the name used to expose the object in JavaScript
+ */
+ public void addJavascriptInterface(Object object, String name) {
+ checkThread();
+ mProvider.addJavascriptInterface(object, name);
+ }
+
+ /**
+ * Removes a previously injected Java object from this WebView. Note that
+ * the removal will not be reflected in JavaScript until the page is next
+ * (re)loaded. See {@link #addJavascriptInterface}.
+ *
+ * @param name the name used to expose the object in JavaScript
+ */
+ public void removeJavascriptInterface(@NonNull String name) {
+ checkThread();
+ mProvider.removeJavascriptInterface(name);
+ }
+
+ /**
+ * Creates a message channel to communicate with JS and returns the message
+ * ports that represent the endpoints of this message channel. The HTML5 message
+ * channel functionality is described
+ * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here
+ * </a>
+ *
+ * <p>The returned message channels are entangled and already in started state.
+ *
+ * @return the two message ports that form the message channel.
+ */
+ public WebMessagePort[] createWebMessageChannel() {
+ checkThread();
+ return mProvider.createWebMessageChannel();
+ }
+
+ /**
+ * Post a message to main frame. The embedded application can restrict the
+ * messages to a certain target origin. See
+ * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages">
+ * HTML5 spec</a> for how target origin can be used.
+ * <p>
+ * A target origin can be set as a wildcard ("*"). However this is not recommended.
+ * See the page above for security issues.
+ *
+ * @param message the WebMessage
+ * @param targetOrigin the target origin.
+ */
+ public void postWebMessage(WebMessage message, Uri targetOrigin) {
+ checkThread();
+ mProvider.postMessageToMainFrame(message, targetOrigin);
+ }
+
+ /**
+ * Gets the WebSettings object used to control the settings for this
+ * WebView.
+ *
+ * @return a WebSettings object that can be used to control this WebView's
+ * settings
+ */
+ public WebSettings getSettings() {
+ checkThread();
+ return mProvider.getSettings();
+ }
+
+ /**
+ * Enables debugging of web contents (HTML / CSS / JavaScript)
+ * loaded into any WebViews of this application. This flag can be enabled
+ * in order to facilitate debugging of web layouts and JavaScript
+ * code running inside WebViews. Please refer to WebView documentation
+ * for the debugging guide.
+ *
+ * The default is {@code false}.
+ *
+ * @param enabled whether to enable web contents debugging
+ */
+ public static void setWebContentsDebuggingEnabled(boolean enabled) {
+ getFactory().getStatics().setWebContentsDebuggingEnabled(enabled);
+ }
+
+ /**
+ * Gets the list of currently loaded plugins.
+ *
+ * @return the list of currently loaded plugins
+ * @deprecated This was used for Gears, which has been deprecated.
+ * @hide
+ */
+ @Deprecated
+ public static synchronized PluginList getPluginList() {
+ return new PluginList();
+ }
+
+ /**
+ * Define the directory used to store WebView data for the current process.
+ * The provided suffix will be used when constructing data and cache
+ * directory paths. If this API is not called, no suffix will be used.
+ * Each directory can be used by only one process in the application. If more
+ * than one process in an app wishes to use WebView, only one process can use
+ * the default directory, and other processes must call this API to define
+ * a unique suffix.
+ * <p>
+ * This means that different processes in the same application cannot directly
+ * share WebView-related data, since the data directories must be distinct.
+ * Applications that use this API may have to explicitly pass data between
+ * processes. For example, login cookies may have to be copied from one
+ * process's cookie jar to the other using {@link CookieManager} if both
+ * processes' WebViews are intended to be logged in.
+ * <p>
+ * Most applications should simply ensure that all components of the app
+ * that rely on WebView are in the same process, to avoid needing multiple
+ * data directories. The {@link #disableWebView} method can be used to ensure
+ * that the other processes do not use WebView by accident in this case.
+ * <p>
+ * This API must be called before any instances of WebView are created in
+ * this process and before any other methods in the android.webkit package
+ * are called by this process.
+ *
+ * @param suffix The directory name suffix to be used for the current
+ * process. Must not contain a path separator.
+ * @throws IllegalStateException if WebView has already been initialized
+ * in the current process.
+ * @throws IllegalArgumentException if the suffix contains a path separator.
+ */
+ public static void setDataDirectorySuffix(String suffix) {
+ WebViewFactory.setDataDirectorySuffix(suffix);
}
- public void addJavascriptInterface(Object obj, String interfaceName) {
+ /**
+ * Indicate that the current process does not intend to use WebView, and
+ * that an exception should be thrown if a WebView is created or any other
+ * methods in the android.webkit package are used.
+ * <p>
+ * Applications with multiple processes may wish to call this in processes
+ * that are not intended to use WebView to avoid accidentally incurring
+ * the memory usage of initializing WebView in long-lived processes that
+ * have no need for it, and to prevent potential data directory conflicts
+ * (see {@link #setDataDirectorySuffix}).
+ * <p>
+ * For example, an audio player application with one process for its
+ * activities and another process for its playback service may wish to call
+ * this method in the playback service's {@link android.app.Service#onCreate}.
+ *
+ * @throws IllegalStateException if WebView has already been initialized
+ * in the current process.
+ */
+ public static void disableWebView() {
+ WebViewFactory.disableWebView();
}
+
+ /**
+ * @deprecated This was used for Gears, which has been deprecated.
+ * @hide
+ */
+ @Deprecated
+ public void refreshPlugins(boolean reloadOpenPages) {
+ checkThread();
+ }
+
+ /**
+ * Puts this WebView into text selection mode. Do not rely on this
+ * functionality; it will be deprecated in the future.
+ *
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
+ public void emulateShiftHeld() {
+ checkThread();
+ }
+
+ /**
+ * @deprecated WebView no longer needs to implement
+ * ViewGroup.OnHierarchyChangeListener. This method does nothing now.
+ */
+ @Override
+ // Cannot add @hide as this can always be accessed via the interface.
+ @Deprecated
+ public void onChildViewAdded(View parent, View child) {}
+
+ /**
+ * @deprecated WebView no longer needs to implement
+ * ViewGroup.OnHierarchyChangeListener. This method does nothing now.
+ */
+ @Override
+ // Cannot add @hide as this can always be accessed via the interface.
+ @Deprecated
+ public void onChildViewRemoved(View p, View child) {}
+
+ /**
+ * @deprecated WebView should not have implemented
+ * ViewTreeObserver.OnGlobalFocusChangeListener. This method does nothing now.
+ */
+ @Override
+ // Cannot add @hide as this can always be accessed via the interface.
+ @Deprecated
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ }
+
+ /**
+ * @deprecated Only the default case, {@code true}, will be supported in a future version.
+ */
+ @Deprecated
+ public void setMapTrackballToArrowKeys(boolean setMap) {
+ checkThread();
+ mProvider.setMapTrackballToArrowKeys(setMap);
+ }
+
+
+ public void flingScroll(int vx, int vy) {
+ checkThread();
+ mProvider.flingScroll(vx, vy);
+ }
+
+ /**
+ * Gets the zoom controls for this WebView, as a separate View. The caller
+ * is responsible for inserting this View into the layout hierarchy.
+ * <p/>
+ * API level {@link android.os.Build.VERSION_CODES#CUPCAKE} introduced
+ * built-in zoom mechanisms for the WebView, as opposed to these separate
+ * zoom controls. The built-in mechanisms are preferred and can be enabled
+ * using {@link WebSettings#setBuiltInZoomControls}.
+ *
+ * @deprecated the built-in zoom mechanisms are preferred
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN}
+ */
+ @Deprecated
public View getZoomControls() {
- return null;
+ checkThread();
+ return mProvider.getZoomControls();
+ }
+
+ /**
+ * Gets whether this WebView can be zoomed in.
+ *
+ * @return {@code true} if this WebView can be zoomed in
+ *
+ * @deprecated This method is prone to inaccuracy due to race conditions
+ * between the web rendering and UI threads; prefer
+ * {@link WebViewClient#onScaleChanged}.
+ */
+ @Deprecated
+ public boolean canZoomIn() {
+ checkThread();
+ return mProvider.canZoomIn();
+ }
+
+ /**
+ * Gets whether this WebView can be zoomed out.
+ *
+ * @return {@code true} if this WebView can be zoomed out
+ *
+ * @deprecated This method is prone to inaccuracy due to race conditions
+ * between the web rendering and UI threads; prefer
+ * {@link WebViewClient#onScaleChanged}.
+ */
+ @Deprecated
+ public boolean canZoomOut() {
+ checkThread();
+ return mProvider.canZoomOut();
+ }
+
+ /**
+ * Performs a zoom operation in this WebView.
+ *
+ * @param zoomFactor the zoom factor to apply. The zoom factor will be clamped to the WebView's
+ * zoom limits. This value must be in the range 0.01 to 100.0 inclusive.
+ */
+ public void zoomBy(float zoomFactor) {
+ checkThread();
+ if (zoomFactor < 0.01)
+ throw new IllegalArgumentException("zoomFactor must be greater than 0.01.");
+ if (zoomFactor > 100.0)
+ throw new IllegalArgumentException("zoomFactor must be less than 100.");
+ mProvider.zoomBy(zoomFactor);
}
+ /**
+ * Performs zoom in in this WebView.
+ *
+ * @return {@code true} if zoom in succeeds, {@code false} if no zoom changes
+ */
public boolean zoomIn() {
- return false;
+ checkThread();
+ return mProvider.zoomIn();
}
+ /**
+ * Performs zoom out in this WebView.
+ *
+ * @return {@code true} if zoom out succeeds, {@code false} if no zoom changes
+ */
public boolean zoomOut() {
- return false;
+ checkThread();
+ return mProvider.zoomOut();
+ }
+
+ /**
+ * @deprecated This method is now obsolete.
+ * @hide Since API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}
+ */
+ @Deprecated
+ public void debugDump() {
+ checkThread();
+ }
+
+ /**
+ * See {@link ViewDebug.HierarchyHandler#dumpViewHierarchyWithProperties(BufferedWriter, int)}
+ * @hide
+ */
+ @Override
+ public void dumpViewHierarchyWithProperties(BufferedWriter out, int level) {
+ mProvider.dumpViewHierarchyWithProperties(out, level);
+ }
+
+ /**
+ * See {@link ViewDebug.HierarchyHandler#findHierarchyView(String, int)}
+ * @hide
+ */
+ @Override
+ public View findHierarchyView(String className, int hashCode) {
+ return mProvider.findHierarchyView(className, hashCode);
+ }
+
+ /** @hide */
+ @IntDef(prefix = { "RENDERER_PRIORITY_" }, value = {
+ RENDERER_PRIORITY_WAIVED,
+ RENDERER_PRIORITY_BOUND,
+ RENDERER_PRIORITY_IMPORTANT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RendererPriority {}
+
+ /**
+ * The renderer associated with this WebView is bound with
+ * {@link Context#BIND_WAIVE_PRIORITY}. At this priority level
+ * {@link WebView} renderers will be strong targets for out of memory
+ * killing.
+ *
+ * Use with {@link #setRendererPriorityPolicy}.
+ */
+ public static final int RENDERER_PRIORITY_WAIVED = 0;
+ /**
+ * The renderer associated with this WebView is bound with
+ * the default priority for services.
+ *
+ * Use with {@link #setRendererPriorityPolicy}.
+ */
+ public static final int RENDERER_PRIORITY_BOUND = 1;
+ /**
+ * The renderer associated with this WebView is bound with
+ * {@link Context#BIND_IMPORTANT}.
+ *
+ * Use with {@link #setRendererPriorityPolicy}.
+ */
+ public static final int RENDERER_PRIORITY_IMPORTANT = 2;
+
+ /**
+ * Set the renderer priority policy for this {@link WebView}. The
+ * priority policy will be used to determine whether an out of
+ * process renderer should be considered to be a target for OOM
+ * killing.
+ *
+ * Because a renderer can be associated with more than one
+ * WebView, the final priority it is computed as the maximum of
+ * any attached WebViews. When a WebView is destroyed it will
+ * cease to be considerered when calculating the renderer
+ * priority. Once no WebViews remain associated with the renderer,
+ * the priority of the renderer will be reduced to
+ * {@link #RENDERER_PRIORITY_WAIVED}.
+ *
+ * The default policy is to set the priority to
+ * {@link #RENDERER_PRIORITY_IMPORTANT} regardless of visibility,
+ * and this should not be changed unless the caller also handles
+ * renderer crashes with
+ * {@link WebViewClient#onRenderProcessGone}. Any other setting
+ * will result in WebView renderers being killed by the system
+ * more aggressively than the application.
+ *
+ * @param rendererRequestedPriority the minimum priority at which
+ * this WebView desires the renderer process to be bound.
+ * @param waivedWhenNotVisible if {@code true}, this flag specifies that
+ * when this WebView is not visible, it will be treated as
+ * if it had requested a priority of
+ * {@link #RENDERER_PRIORITY_WAIVED}.
+ */
+ public void setRendererPriorityPolicy(
+ @RendererPriority int rendererRequestedPriority,
+ boolean waivedWhenNotVisible) {
+ mProvider.setRendererPriorityPolicy(rendererRequestedPriority, waivedWhenNotVisible);
+ }
+
+ /**
+ * Get the requested renderer priority for this WebView.
+ *
+ * @return the requested renderer priority policy.
+ */
+ @RendererPriority
+ public int getRendererRequestedPriority() {
+ return mProvider.getRendererRequestedPriority();
+ }
+
+ /**
+ * Return whether this WebView requests a priority of
+ * {@link #RENDERER_PRIORITY_WAIVED} when not visible.
+ *
+ * @return whether this WebView requests a priority of
+ * {@link #RENDERER_PRIORITY_WAIVED} when not visible.
+ */
+ public boolean getRendererPriorityWaivedWhenNotVisible() {
+ return mProvider.getRendererPriorityWaivedWhenNotVisible();
+ }
+
+ /**
+ * Sets the {@link TextClassifier} for this WebView.
+ */
+ public void setTextClassifier(@Nullable TextClassifier textClassifier) {
+ mProvider.setTextClassifier(textClassifier);
+ }
+
+ /**
+ * Returns the {@link TextClassifier} used by this WebView.
+ * If no TextClassifier has been set, this WebView uses the default set by the system.
+ */
+ @NonNull
+ public TextClassifier getTextClassifier() {
+ return mProvider.getTextClassifier();
+ }
+
+ /**
+ * Returns the {@link ClassLoader} used to load internal WebView classes.
+ * This method is meant for use by the WebView Support Library, there is no reason to use this
+ * method otherwise.
+ */
+ @NonNull
+ public static ClassLoader getWebViewClassLoader() {
+ return getFactory().getWebViewClassLoader();
+ }
+
+ /**
+ * Returns the {@link Looper} corresponding to the thread on which WebView calls must be made.
+ */
+ @NonNull
+ public Looper getWebViewLooper() {
+ return mWebViewThread;
+ }
+
+ //-------------------------------------------------------------------------
+ // Interface for WebView providers
+ //-------------------------------------------------------------------------
+
+ /**
+ * Gets the WebViewProvider. Used by providers to obtain the underlying
+ * implementation, e.g. when the application responds to
+ * WebViewClient.onCreateWindow() request.
+ *
+ * @hide WebViewProvider is not public API.
+ */
+ @SystemApi
+ public WebViewProvider getWebViewProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Callback interface, allows the provider implementation to access non-public methods
+ * and fields, and make super-class calls in this WebView instance.
+ * @hide Only for use by WebViewProvider implementations
+ */
+ @SystemApi
+ public class PrivateAccess {
+ // ---- Access to super-class methods ----
+ public int super_getScrollBarStyle() {
+ return WebView.super.getScrollBarStyle();
+ }
+
+ public void super_scrollTo(int scrollX, int scrollY) {
+ WebView.super.scrollTo(scrollX, scrollY);
+ }
+
+ public void super_computeScroll() {
+ WebView.super.computeScroll();
+ }
+
+ public boolean super_onHoverEvent(MotionEvent event) {
+ return WebView.super.onHoverEvent(event);
+ }
+
+ public boolean super_performAccessibilityAction(int action, Bundle arguments) {
+ return WebView.super.performAccessibilityActionInternal(action, arguments);
+ }
+
+ public boolean super_performLongClick() {
+ return WebView.super.performLongClick();
+ }
+
+ public boolean super_setFrame(int left, int top, int right, int bottom) {
+ return WebView.super.setFrame(left, top, right, bottom);
+ }
+
+ public boolean super_dispatchKeyEvent(KeyEvent event) {
+ return WebView.super.dispatchKeyEvent(event);
+ }
+
+ public boolean super_onGenericMotionEvent(MotionEvent event) {
+ return WebView.super.onGenericMotionEvent(event);
+ }
+
+ public boolean super_requestFocus(int direction, Rect previouslyFocusedRect) {
+ return WebView.super.requestFocus(direction, previouslyFocusedRect);
+ }
+
+ public void super_setLayoutParams(ViewGroup.LayoutParams params) {
+ WebView.super.setLayoutParams(params);
+ }
+
+ public void super_startActivityForResult(Intent intent, int requestCode) {
+ WebView.super.startActivityForResult(intent, requestCode);
+ }
+
+ // ---- Access to non-public methods ----
+ public void overScrollBy(int deltaX, int deltaY,
+ int scrollX, int scrollY,
+ int scrollRangeX, int scrollRangeY,
+ int maxOverScrollX, int maxOverScrollY,
+ boolean isTouchEvent) {
+ WebView.this.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
+ maxOverScrollX, maxOverScrollY, isTouchEvent);
+ }
+
+ public void awakenScrollBars(int duration) {
+ WebView.this.awakenScrollBars(duration);
+ }
+
+ public void awakenScrollBars(int duration, boolean invalidate) {
+ WebView.this.awakenScrollBars(duration, invalidate);
+ }
+
+ public float getVerticalScrollFactor() {
+ return WebView.this.getVerticalScrollFactor();
+ }
+
+ public float getHorizontalScrollFactor() {
+ return WebView.this.getHorizontalScrollFactor();
+ }
+
+ public void setMeasuredDimension(int measuredWidth, int measuredHeight) {
+ WebView.this.setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ public void onScrollChanged(int l, int t, int oldl, int oldt) {
+ WebView.this.onScrollChanged(l, t, oldl, oldt);
+ }
+
+ public int getHorizontalScrollbarHeight() {
+ return WebView.this.getHorizontalScrollbarHeight();
+ }
+
+ public void super_onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
+ int l, int t, int r, int b) {
+ WebView.super.onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b);
+ }
+
+ // ---- Access to (non-public) fields ----
+ /** Raw setter for the scroll X value, without invoking onScrollChanged handlers etc. */
+ public void setScrollXRaw(int scrollX) {
+ WebView.this.mScrollX = scrollX;
+ }
+
+ /** Raw setter for the scroll Y value, without invoking onScrollChanged handlers etc. */
+ public void setScrollYRaw(int scrollY) {
+ WebView.this.mScrollY = scrollY;
+ }
+
+ }
+
+ //-------------------------------------------------------------------------
+ // Package-private internal stuff
+ //-------------------------------------------------------------------------
+
+ // Only used by android.webkit.FindActionModeCallback.
+ void setFindDialogFindListener(FindListener listener) {
+ checkThread();
+ setupFindListenerIfNeeded();
+ mFindListener.mFindDialogFindListener = listener;
+ }
+
+ // Only used by android.webkit.FindActionModeCallback.
+ void notifyFindDialogDismissed() {
+ checkThread();
+ mProvider.notifyFindDialogDismissed();
+ }
+
+ //-------------------------------------------------------------------------
+ // Private internal stuff
+ //-------------------------------------------------------------------------
+
+ private WebViewProvider mProvider;
+
+ /**
+ * In addition to the FindListener that the user may set via the WebView.setFindListener
+ * API, FindActionModeCallback will register it's own FindListener. We keep them separate
+ * via this class so that the two FindListeners can potentially exist at once.
+ */
+ private class FindListenerDistributor implements FindListener {
+ private FindListener mFindDialogFindListener;
+ private FindListener mUserFindListener;
+
+ @Override
+ public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
+ boolean isDoneCounting) {
+ if (mFindDialogFindListener != null) {
+ mFindDialogFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches,
+ isDoneCounting);
+ }
+
+ if (mUserFindListener != null) {
+ mUserFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches,
+ isDoneCounting);
+ }
+ }
+ }
+ private FindListenerDistributor mFindListener;
+
+ private void setupFindListenerIfNeeded() {
+ if (mFindListener == null) {
+ mFindListener = new FindListenerDistributor();
+ mProvider.setFindListener(mFindListener);
+ }
+ }
+
+ private void ensureProviderCreated() {
+ checkThread();
+ if (mProvider == null) {
+ // As this can get called during the base class constructor chain, pass the minimum
+ // number of dependencies here; the rest are deferred to init().
+ mProvider = getFactory().createWebView(this, new PrivateAccess());
+ }
+ }
+
+ private static WebViewFactoryProvider getFactory() {
+ return WebViewFactory.getProvider();
+ }
+
+ private final Looper mWebViewThread = Looper.myLooper();
+
+ private void checkThread() {
+ // Ignore mWebViewThread == null because this can be called during in the super class
+ // constructor, before this class's own constructor has even started.
+ if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) {
+ Throwable throwable = new Throwable(
+ "A WebView method was called on thread '" +
+ Thread.currentThread().getName() + "'. " +
+ "All WebView methods must be called on the same thread. " +
+ "(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() +
+ ", FYI main Looper is " + Looper.getMainLooper() + ")");
+ Log.w(LOGTAG, Log.getStackTraceString(throwable));
+ StrictMode.onWebViewMethodCalledOnWrongThread(throwable);
+
+ if (sEnforceThreadChecking) {
+ throw new RuntimeException(throwable);
+ }
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // Override View methods
+ //-------------------------------------------------------------------------
+
+ // TODO: Add a test that enumerates all methods in ViewDelegte & ScrollDelegate, and ensures
+ // there's a corresponding override (or better, caller) for each of them in here.
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mProvider.getViewDelegate().onAttachedToWindow();
+ }
+
+ /** @hide */
+ @Override
+ protected void onDetachedFromWindowInternal() {
+ mProvider.getViewDelegate().onDetachedFromWindow();
+ super.onDetachedFromWindowInternal();
+ }
+
+ /** @hide */
+ @Override
+ public void onMovedToDisplay(int displayId, Configuration config) {
+ mProvider.getViewDelegate().onMovedToDisplay(displayId, config);
+ }
+
+ @Override
+ public void setLayoutParams(ViewGroup.LayoutParams params) {
+ mProvider.getViewDelegate().setLayoutParams(params);
+ }
+
+ @Override
+ public void setOverScrollMode(int mode) {
+ super.setOverScrollMode(mode);
+ // This method may be called in the constructor chain, before the WebView provider is
+ // created.
+ ensureProviderCreated();
+ mProvider.getViewDelegate().setOverScrollMode(mode);
+ }
+
+ @Override
+ public void setScrollBarStyle(int style) {
+ mProvider.getViewDelegate().setScrollBarStyle(style);
+ super.setScrollBarStyle(style);
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange() {
+ return mProvider.getScrollDelegate().computeHorizontalScrollRange();
+ }
+
+ @Override
+ protected int computeHorizontalScrollOffset() {
+ return mProvider.getScrollDelegate().computeHorizontalScrollOffset();
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return mProvider.getScrollDelegate().computeVerticalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return mProvider.getScrollDelegate().computeVerticalScrollOffset();
+ }
+
+ @Override
+ protected int computeVerticalScrollExtent() {
+ return mProvider.getScrollDelegate().computeVerticalScrollExtent();
+ }
+
+ @Override
+ public void computeScroll() {
+ mProvider.getScrollDelegate().computeScroll();
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ return mProvider.getViewDelegate().onHoverEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return mProvider.getViewDelegate().onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ return mProvider.getViewDelegate().onGenericMotionEvent(event);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ return mProvider.getViewDelegate().onTrackballEvent(event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mProvider.getViewDelegate().onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return mProvider.getViewDelegate().onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ return mProvider.getViewDelegate().onKeyMultiple(keyCode, repeatCount, event);
+ }
+
+ /*
+ TODO: These are not currently implemented in WebViewClassic, but it seems inconsistent not
+ to be delegating them too.
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ return mProvider.getViewDelegate().onKeyPreIme(keyCode, event);
+ }
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ return mProvider.getViewDelegate().onKeyLongPress(keyCode, event);
+ }
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ return mProvider.getViewDelegate().onKeyShortcut(keyCode, event);
+ }
+ */
+
+ @Override
+ public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+ AccessibilityNodeProvider provider =
+ mProvider.getViewDelegate().getAccessibilityNodeProvider();
+ return provider == null ? super.getAccessibilityNodeProvider() : provider;
+ }
+
+ @Deprecated
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return mProvider.getViewDelegate().shouldDelayChildPressedState();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return WebView.class.getName();
+ }
+
+ @Override
+ public void onProvideVirtualStructure(ViewStructure structure) {
+ mProvider.getViewDelegate().onProvideVirtualStructure(structure);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>The {@link ViewStructure} traditionally represents a {@link View}, while for web pages
+ * it represent HTML nodes. Hence, it's necessary to "map" the HTML properties in a way that is
+ * understood by the {@link android.service.autofill.AutofillService} implementations:
+ *
+ * <ol>
+ * <li>Only the HTML nodes inside a {@code FORM} are generated.
+ * <li>The source of the HTML is set using {@link ViewStructure#setWebDomain(String)} in the
+ * node representing the WebView.
+ * <li>If a web page has multiple {@code FORM}s, only the data for the current form is
+ * represented&mdash;if the user taps a field from another form, then the current autofill
+ * context is canceled (by calling {@link android.view.autofill.AutofillManager#cancel()} and
+ * a new context is created for that {@code FORM}.
+ * <li>Similarly, if the page has {@code IFRAME} nodes, they are not initially represented in
+ * the view structure until the user taps a field from a {@code FORM} inside the
+ * {@code IFRAME}, in which case it would be treated the same way as multiple forms described
+ * above, except that the {@link ViewStructure#setWebDomain(String) web domain} of the
+ * {@code FORM} contains the {@code src} attribute from the {@code IFRAME} node.
+ * <li>The W3C autofill field ({@code autocomplete} tag attribute) maps to
+ * {@link ViewStructure#setAutofillHints(String[])}.
+ * <li>If the view is editable, the {@link ViewStructure#setAutofillType(int)} and
+ * {@link ViewStructure#setAutofillValue(AutofillValue)} must be set.
+ * <li>The {@code placeholder} attribute maps to {@link ViewStructure#setHint(CharSequence)}.
+ * <li>Other HTML attributes can be represented through
+ * {@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)}.
+ * </ol>
+ *
+ * <p>If the WebView implementation can determine that the value of a field was set statically
+ * (for example, not through Javascript), it should also call
+ * {@code structure.setDataIsSensitive(false)}.
+ *
+ * <p>For example, an HTML form with 2 fields for username and password:
+ *
+ * <pre class="prettyprint">
+ * &lt;label&gt;Username:&lt;/label&gt;
+ * &lt;input type="text" name="username" id="user" value="Type your username" autocomplete="username" placeholder="Email or username"&gt;
+ * &lt;label&gt;Password:&lt;/label&gt;
+ * &lt;input type="password" name="password" id="pass" autocomplete="current-password" placeholder="Password"&gt;
+ * </pre>
+ *
+ * <p>Would map to:
+ *
+ * <pre class="prettyprint">
+ * int index = structure.addChildCount(2);
+ * ViewStructure username = structure.newChild(index);
+ * username.setAutofillId(structure.getAutofillId(), 1); // id 1 - first child
+ * username.setAutofillHints("username");
+ * username.setHtmlInfo(username.newHtmlInfoBuilder("input")
+ * .addAttribute("type", "text")
+ * .addAttribute("name", "username")
+ * .addAttribute("label", "Username:")
+ * .build());
+ * username.setHint("Email or username");
+ * username.setAutofillType(View.AUTOFILL_TYPE_TEXT);
+ * username.setAutofillValue(AutofillValue.forText("Type your username"));
+ * // Value of the field is not sensitive because it was created statically and not changed.
+ * username.setDataIsSensitive(false);
+ *
+ * ViewStructure password = structure.newChild(index + 1);
+ * username.setAutofillId(structure, 2); // id 2 - second child
+ * password.setAutofillHints("current-password");
+ * password.setHtmlInfo(password.newHtmlInfoBuilder("input")
+ * .addAttribute("type", "password")
+ * .addAttribute("name", "password")
+ * .addAttribute("label", "Password:")
+ * .build());
+ * password.setHint("Password");
+ * password.setAutofillType(View.AUTOFILL_TYPE_TEXT);
+ * </pre>
+ */
+ @Override
+ public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) {
+ mProvider.getViewDelegate().onProvideAutofillVirtualStructure(structure, flags);
+ }
+
+ @Override
+ public void autofill(SparseArray<AutofillValue>values) {
+ mProvider.getViewDelegate().autofill(values);
+ }
+
+ @Override
+ public boolean isVisibleToUserForAutofill(int virtualId) {
+ return mProvider.getViewDelegate().isVisibleToUserForAutofill(virtualId);
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ mProvider.getViewDelegate().onInitializeAccessibilityNodeInfo(info);
+ }
+
+ /** @hide */
+ @Override
+ public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEventInternal(event);
+ mProvider.getViewDelegate().onInitializeAccessibilityEvent(event);
+ }
+
+ /** @hide */
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ return mProvider.getViewDelegate().performAccessibilityAction(action, arguments);
+ }
+
+ /** @hide */
+ @Override
+ protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar,
+ int l, int t, int r, int b) {
+ mProvider.getViewDelegate().onDrawVerticalScrollBar(canvas, scrollBar, l, t, r, b);
+ }
+
+ @Override
+ protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+ mProvider.getViewDelegate().onOverScrolled(scrollX, scrollY, clampedX, clampedY);
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ super.onWindowVisibilityChanged(visibility);
+ mProvider.getViewDelegate().onWindowVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mProvider.getViewDelegate().onDraw(canvas);
+ }
+
+ @Override
+ public boolean performLongClick() {
+ return mProvider.getViewDelegate().performLongClick();
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ mProvider.getViewDelegate().onConfigurationChanged(newConfig);
+ }
+
+ /**
+ * Creates a new InputConnection for an InputMethod to interact with the WebView.
+ * This is similar to {@link View#onCreateInputConnection} but note that WebView
+ * calls InputConnection methods on a thread other than the UI thread.
+ * If these methods are overridden, then the overriding methods should respect
+ * thread restrictions when calling View methods or accessing data.
+ */
+ @Override
+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return mProvider.getViewDelegate().onCreateInputConnection(outAttrs);
+ }
+
+ @Override
+ public boolean onDragEvent(DragEvent event) {
+ return mProvider.getViewDelegate().onDragEvent(event);
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ // This method may be called in the constructor chain, before the WebView provider is
+ // created.
+ ensureProviderCreated();
+ mProvider.getViewDelegate().onVisibilityChanged(changedView, visibility);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ mProvider.getViewDelegate().onWindowFocusChanged(hasWindowFocus);
+ super.onWindowFocusChanged(hasWindowFocus);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ mProvider.getViewDelegate().onFocusChanged(focused, direction, previouslyFocusedRect);
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ }
+
+ /** @hide */
+ @Override
+ protected boolean setFrame(int left, int top, int right, int bottom) {
+ return mProvider.getViewDelegate().setFrame(left, top, right, bottom);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int ow, int oh) {
+ super.onSizeChanged(w, h, ow, oh);
+ mProvider.getViewDelegate().onSizeChanged(w, h, ow, oh);
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+ mProvider.getViewDelegate().onScrollChanged(l, t, oldl, oldt);
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mProvider.getViewDelegate().dispatchKeyEvent(event);
+ }
+
+ @Override
+ public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+ return mProvider.getViewDelegate().requestFocus(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mProvider.getViewDelegate().onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+ return mProvider.getViewDelegate().requestChildRectangleOnScreen(child, rect, immediate);
+ }
+
+ @Override
+ public void setBackgroundColor(int color) {
+ mProvider.getViewDelegate().setBackgroundColor(color);
+ }
+
+ @Override
+ public void setLayerType(int layerType, Paint paint) {
+ super.setLayerType(layerType, paint);
+ mProvider.getViewDelegate().setLayerType(layerType, paint);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ mProvider.getViewDelegate().preDispatchDraw(canvas);
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ public void onStartTemporaryDetach() {
+ super.onStartTemporaryDetach();
+ mProvider.getViewDelegate().onStartTemporaryDetach();
+ }
+
+ @Override
+ public void onFinishTemporaryDetach() {
+ super.onFinishTemporaryDetach();
+ mProvider.getViewDelegate().onFinishTemporaryDetach();
+ }
+
+ @Override
+ public Handler getHandler() {
+ return mProvider.getViewDelegate().getHandler(super.getHandler());
+ }
+
+ @Override
+ public View findFocus() {
+ return mProvider.getViewDelegate().findFocus(super.findFocus());
+ }
+
+ /**
+ * If WebView has already been loaded into the current process this method will return the
+ * package that was used to load it. Otherwise, the package that would be used if the WebView
+ * was loaded right now will be returned; this does not cause WebView to be loaded, so this
+ * information may become outdated at any time.
+ * The WebView package changes either when the current WebView package is updated, disabled, or
+ * uninstalled. It can also be changed through a Developer Setting.
+ * If the WebView package changes, any app process that has loaded WebView will be killed. The
+ * next time the app starts and loads WebView it will use the new WebView package instead.
+ * @return the current WebView package, or {@code null} if there is none.
+ */
+ @Nullable
+ public static PackageInfo getCurrentWebViewPackage() {
+ PackageInfo webviewPackage = WebViewFactory.getLoadedPackageInfo();
+ if (webviewPackage != null) {
+ return webviewPackage;
+ }
+
+ IWebViewUpdateService service = WebViewFactory.getUpdateService();
+ if (service == null) {
+ return null;
+ }
+ try {
+ return service.getCurrentWebViewPackage();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Receive the result from a previous call to {@link #startActivityForResult(Intent, int)}.
+ *
+ * @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").
+ * @hide
+ */
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mProvider.getViewDelegate().onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public boolean onCheckIsTextEditor() {
+ return mProvider.getViewDelegate().onCheckIsTextEditor();
+ }
+
+ /** @hide */
+ @Override
+ protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+ super.encodeProperties(encoder);
+
+ checkThread();
+ encoder.addProperty("webview:contentHeight", mProvider.getContentHeight());
+ encoder.addProperty("webview:contentWidth", mProvider.getContentWidth());
+ encoder.addProperty("webview:scale", mProvider.getScale());
+ encoder.addProperty("webview:title", mProvider.getTitle());
+ encoder.addProperty("webview:url", mProvider.getUrl());
+ encoder.addProperty("webview:originalUrl", mProvider.getOriginalUrl());
}
}
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
index 99467265..dac100a4 100644
--- a/android/widget/Editor.java
+++ b/android/widget/Editor.java
@@ -39,6 +39,7 @@ import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -4837,14 +4838,48 @@ public class Editor {
return true;
}
- private boolean handleOverlapsMagnifier() {
- final int handleY = mContainer.getDecorViewLayoutParams().y;
- final int magnifierBottomWhenAtWindowTop =
- mTextView.getRootWindowInsets().getSystemWindowInsetTop()
- + mMagnifierAnimator.mMagnifier.getHeight();
- return handleY <= magnifierBottomWhenAtWindowTop;
+ private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
+ @NonNull final Rect magnifierRect) {
+ final PopupWindow window = handle.mContainer;
+ if (!window.hasDecorView()) {
+ return false;
+ }
+ final Rect handleRect = new Rect(
+ window.getDecorViewLayoutParams().x,
+ window.getDecorViewLayoutParams().y,
+ window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
+ window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
+ return Rect.intersects(handleRect, magnifierRect);
+ }
+
+ private @Nullable HandleView getOtherSelectionHandle() {
+ final SelectionModifierCursorController controller = getSelectionController();
+ if (controller == null || !controller.isActive()) {
+ return null;
+ }
+ return controller.mStartHandle != this
+ ? controller.mStartHandle
+ : controller.mEndHandle;
}
+ private final Magnifier.Callback mHandlesVisibilityCallback = new Magnifier.Callback() {
+ @Override
+ public void onOperationComplete() {
+ final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getWindowCoords();
+ if (magnifierTopLeft == null) {
+ return;
+ }
+ final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
+ magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
+ magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
+ setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect));
+ final HandleView otherHandle = getOtherSelectionHandle();
+ if (otherHandle != null) {
+ otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
+ }
+ }
+ };
+
protected final void updateMagnifier(@NonNull final MotionEvent event) {
if (mMagnifierAnimator == null) {
return;
@@ -4858,12 +4893,8 @@ public class Editor {
mRenderCursorRegardlessTiming = true;
mTextView.invalidateCursorPath();
suspendBlink();
- // Hide handle if it overlaps the magnifier.
- if (handleOverlapsMagnifier()) {
- setVisible(false);
- } else {
- setVisible(true);
- }
+ mMagnifierAnimator.mMagnifier
+ .setOnOperationCompleteCallback(mHandlesVisibilityCallback);
mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
} else {
@@ -4877,6 +4908,10 @@ public class Editor {
mRenderCursorRegardlessTiming = false;
resumeBlink();
setVisible(true);
+ final HandleView otherHandle = getOtherSelectionHandle();
+ if (otherHandle != null) {
+ otherHandle.setVisible(true);
+ }
}
}
@@ -6031,7 +6066,9 @@ public class Editor {
mSwitchedLines = false;
final int selectionStart = mTextView.getSelectionStart();
final int selectionEnd = mTextView.getSelectionEnd();
- if (selectionStart > selectionEnd) {
+ if (selectionStart < 0 || selectionEnd < 0) {
+ Selection.removeSelection((Spannable) mTextView.getText());
+ } else if (selectionStart > selectionEnd) {
Selection.setSelection((Spannable) mTextView.getText(),
selectionEnd, selectionStart);
}
diff --git a/android/widget/ImageView.java b/android/widget/ImageView.java
index 4b951fa1..13729874 100644
--- a/android/widget/ImageView.java
+++ b/android/widget/ImageView.java
@@ -817,8 +817,6 @@ public class ImageView extends View {
if (mScaleType != scaleType) {
mScaleType = scaleType;
- setWillNotCacheDrawing(mScaleType == ScaleType.CENTER);
-
requestLayout();
invalidate();
}
diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java
index d32e93c7..40f9652c 100644
--- a/android/widget/LinearLayout.java
+++ b/android/widget/LinearLayout.java
@@ -217,6 +217,17 @@ public class LinearLayout extends ViewGroup {
private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
+ /**
+ * Signals that compatibility booleans have been initialized according to
+ * target SDK versions.
+ */
+ private static boolean sCompatibilityDone = false;
+
+ /**
+ * Behavior change in P; always remeasure weighted children, regardless of excess space.
+ */
+ private static boolean sRemeasureWeightedChildren = true;
+
public LinearLayout(Context context) {
this(context, null);
}
@@ -232,6 +243,15 @@ public class LinearLayout extends ViewGroup {
public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
+ if (!sCompatibilityDone && context != null) {
+ final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+
+ // Older apps only remeasure non-zero children
+ sRemeasureWeightedChildren = targetSdkVersion >= Build.VERSION_CODES.P;
+
+ sCompatibilityDone = true;
+ }
+
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
@@ -917,7 +937,8 @@ public class LinearLayout extends ViewGroup {
// measurement on any children, we need to measure them now.
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
- if (skippedMeasure || totalWeight > 0.0f) {
+ if (skippedMeasure
+ || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
@@ -1300,7 +1321,8 @@ public class LinearLayout extends ViewGroup {
// measurement on any children, we need to measure them now.
int remainingExcess = widthSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : usedExcessSpace);
- if (skippedMeasure || totalWeight > 0.0f) {
+ if (skippedMeasure
+ || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
index 5eb66999..cb362e65 100644
--- a/android/widget/Magnifier.java
+++ b/android/widget/Magnifier.java
@@ -233,6 +233,17 @@ public final class Magnifier {
return mZoom;
}
+ /**
+ * @hide
+ */
+ @Nullable
+ public Point getWindowCoords() {
+ if (mWindow == null) {
+ return null;
+ }
+ return new Point(mWindow.mLastDrawContentPositionX, mWindow.mLastDrawContentPositionY);
+ }
+
@Nullable
private Surface getValidViewSurface() {
// TODO: deduplicate this against the first part of #performPixelCopy
@@ -374,8 +385,11 @@ public final class Magnifier {
private final Runnable mMagnifierUpdater;
// The handler where the magnifier updater jobs will be post'd.
private final Handler mHandler;
- // The callback to be run after the next draw. Only used for testing.
+ // The callback to be run after the next draw.
private Callback mCallback;
+ // The position of the magnifier content when the last draw was requested.
+ private int mLastDrawContentPositionX;
+ private int mLastDrawContentPositionY;
// Members below describe the state of the magnifier. Reads/writes to them
// have to be synchronized between the UI thread and the thread that handles
@@ -598,6 +612,8 @@ public final class Magnifier {
callback = null;
}
+ mLastDrawContentPositionX = mWindowPositionX + mOffsetX;
+ mLastDrawContentPositionY = mWindowPositionY + mOffsetY;
mFrameDrawScheduled = false;
}
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
index b3327a70..1f2b90a1 100644
--- a/android/widget/SelectionActionModeHelper.java
+++ b/android/widget/SelectionActionModeHelper.java
@@ -33,9 +33,9 @@ import android.text.Spannable;
import android.text.TextUtils;
import android.util.Log;
import android.view.ActionMode;
-import android.view.textclassifier.Logger;
import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.SelectionEvent.InvocationMethod;
+import android.view.textclassifier.SelectionSessionLogger;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassificationManager;
@@ -663,7 +663,6 @@ public final class SelectionActionModeHelper {
private static final String LOG_TAG = "SelectionMetricsLogger";
private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
- private final Logger mLogger;
private final boolean mEditTextLogger;
private final BreakIterator mTokenIterator;
@@ -673,10 +672,8 @@ public final class SelectionActionModeHelper {
SelectionMetricsLogger(TextView textView) {
Preconditions.checkNotNull(textView);
- mLogger = textView.getTextClassifier().getLogger(
- new Logger.Config(textView.getContext(), getWidetType(textView), null));
mEditTextLogger = textView.isTextEditable();
- mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale());
+ mTokenIterator = SelectionSessionLogger.getTokenIterator(textView.getTextLocale());
}
@TextClassifier.WidgetType
@@ -702,8 +699,6 @@ public final class SelectionActionModeHelper {
}
mTokenIterator.setText(mText);
mStartIndex = index;
- mLogger.logSelectionStartedEvent(invocationMethod, 0);
- // TODO: Remove the above legacy logging.
mClassificationSession = classificationSession;
mClassificationSession.onSelectionEvent(
SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
@@ -720,27 +715,18 @@ public final class SelectionActionModeHelper {
Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
int[] wordIndices = getWordDelta(start, end);
if (selection != null) {
- mLogger.logSelectionModifiedEvent(
- wordIndices[0], wordIndices[1], selection);
- // TODO: Remove the above legacy logging.
if (mClassificationSession != null) {
mClassificationSession.onSelectionEvent(
SelectionEvent.createSelectionModifiedEvent(
wordIndices[0], wordIndices[1], selection));
}
} else if (classification != null) {
- mLogger.logSelectionModifiedEvent(
- wordIndices[0], wordIndices[1], classification);
- // TODO: Remove the above legacy logging.
if (mClassificationSession != null) {
mClassificationSession.onSelectionEvent(
SelectionEvent.createSelectionModifiedEvent(
wordIndices[0], wordIndices[1], classification));
}
} else {
- mLogger.logSelectionModifiedEvent(
- wordIndices[0], wordIndices[1]);
- // TODO: Remove the above legacy logging.
if (mClassificationSession != null) {
mClassificationSession.onSelectionEvent(
SelectionEvent.createSelectionModifiedEvent(
@@ -762,18 +748,12 @@ public final class SelectionActionModeHelper {
Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
int[] wordIndices = getWordDelta(start, end);
if (classification != null) {
- mLogger.logSelectionActionEvent(
- wordIndices[0], wordIndices[1], action, classification);
- // TODO: Remove the above legacy logging.
if (mClassificationSession != null) {
mClassificationSession.onSelectionEvent(
SelectionEvent.createSelectionActionEvent(
wordIndices[0], wordIndices[1], action, classification));
}
} else {
- mLogger.logSelectionActionEvent(
- wordIndices[0], wordIndices[1], action);
- // TODO: Remove the above legacy logging.
if (mClassificationSession != null) {
mClassificationSession.onSelectionEvent(
SelectionEvent.createSelectionActionEvent(
@@ -989,7 +969,7 @@ public final class SelectionActionModeHelper {
mHot = true;
trimText();
final TextSelection selection;
- if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) {
+ if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
final TextSelection.Request request = new TextSelection.Request.Builder(
mTrimmedText, mRelativeStart, mRelativeEnd)
.setDefaultLocales(mDefaultLocales)
@@ -1043,7 +1023,7 @@ public final class SelectionActionModeHelper {
trimText();
final TextClassification classification;
- if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) {
+ if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
final TextClassification.Request request =
new TextClassification.Request.Builder(
mTrimmedText, mRelativeStart, mRelativeEnd)
diff --git a/android/widget/TextClock.java b/android/widget/TextClock.java
index 53318c99..d8a9ccad 100644
--- a/android/widget/TextClock.java
+++ b/android/widget/TextClock.java
@@ -408,6 +408,15 @@ public class TextClock extends TextView {
}
/**
+ * Update the displayed time if necessary and invalidate the view.
+ * @hide
+ */
+ public void refresh() {
+ onTimeChanged();
+ invalidate();
+ }
+
+ /**
* Indicates whether the system is currently using the 24-hour mode.
*
* When the system is in 24-hour mode, this view will use the pattern
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
index 11db6b65..7b9ecca0 100644
--- a/android/widget/TextView.java
+++ b/android/widget/TextView.java
@@ -317,7 +317,6 @@ import java.util.function.Supplier;
* @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
* @attr ref android.R.styleable#TextView_autoSizeStepGranularity
* @attr ref android.R.styleable#TextView_autoSizePresetSizes
- * @attr ref android.R.styleable#TextView_accessibilityHeading
*/
@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
@@ -417,7 +416,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private int mCurTextColor;
private int mCurHintTextColor;
private boolean mFreezesText;
- private boolean mIsAccessibilityHeading;
private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
@@ -1294,8 +1292,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
case com.android.internal.R.styleable.TextView_lineHeight:
lineHeight = a.getDimensionPixelSize(attr, -1);
break;
- case com.android.internal.R.styleable.TextView_accessibilityHeading:
- mIsAccessibilityHeading = a.getBoolean(attr, false);
}
}
@@ -5213,32 +5209,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
- * Gets whether this view is a heading for accessibility purposes.
- *
- * @return {@code true} if the view is a heading, {@code false} otherwise.
- *
- * @attr ref android.R.styleable#TextView_accessibilityHeading
- */
- public boolean isAccessibilityHeading() {
- return mIsAccessibilityHeading;
- }
-
- /**
- * Set if view is a heading for a section of content for accessibility purposes.
- *
- * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
- *
- * @attr ref android.R.styleable#TextView_accessibilityHeading
- */
- public void setAccessibilityHeading(boolean isHeading) {
- if (isHeading != mIsAccessibilityHeading) {
- mIsAccessibilityHeading = isHeading;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
- }
- }
-
- /**
* Convenience method to append the specified text to the TextView's
* display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
* if it was not already editable.
@@ -9380,7 +9350,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
final int selectionStart = getSelectionStart();
final int selectionEnd = getSelectionEnd();
- return selectionStart >= 0 && selectionStart != selectionEnd;
+ return selectionStart >= 0 && selectionEnd > 0 && selectionStart != selectionEnd;
}
String getSelectedText() {
@@ -10833,7 +10803,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
info.setText(getTextForAccessibility());
info.setHintText(mHint);
info.setShowingHintText(isShowingHint());
- info.setHeading(mIsAccessibilityHeading);
if (mBufferType == BufferType.EDITABLE) {
info.setEditable(true);
diff --git a/androidx/car/app/CarAlertDialog.java b/androidx/car/app/CarAlertDialog.java
index 453ad4e1..58262b63 100644
--- a/androidx/car/app/CarAlertDialog.java
+++ b/androidx/car/app/CarAlertDialog.java
@@ -23,6 +23,7 @@ import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.TypedValue;
+import android.view.Gravity;
import android.view.MotionEvent;
import android.view.TouchDelegate;
import android.view.View;
@@ -90,17 +91,24 @@ public class CarAlertDialog extends Dialog {
private void setTitleInternal(CharSequence title) {
boolean hasTitle = !TextUtils.isEmpty(title);
+ boolean hasBody = mBodyView.getVisibility() == View.VISIBLE;
+ boolean hasButton = mButtonPanel.getVisibility() == View.VISIBLE;
mTitleView.setText(title);
mTitleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE);
+ // Center title if there is no button.
+ mTitleView.setGravity(hasButton ? Gravity.CENTER_VERTICAL | Gravity.START : Gravity.CENTER);
+
// If there's a title, then remove the padding at the top of the content view.
int topPadding = hasTitle ? 0 : mTopPadding;
+ // If there is only title, also remove the padding at the bottom so title is centered.
+ int bottomPadding = !hasButton && !hasBody ? 0 : mContentView.getPaddingBottom();
mContentView.setPaddingRelative(
mContentView.getPaddingStart(),
topPadding,
mContentView.getPaddingEnd(),
- mContentView.getPaddingBottom());
+ bottomPadding);
}
private void setBody(CharSequence body) {
@@ -226,10 +234,12 @@ public class CarAlertDialog extends Dialog {
* contents based on what data is present.
*/
private void initializeDialogWithData() {
- setTitleInternal(mData.mTitle);
setBody(mData.mBody);
setPositiveButton(mData.mPositiveButtonText);
setNegativeButton(mData.mNegativeButtonText);
+ // setTitleInternal() should be called last because we want to center title and adjust
+ // padding depending on body/button configuration.
+ setTitleInternal(mData.mTitle);
}
/**
diff --git a/androidx/car/utils/CarUxRestrictionsTestUtils.java b/androidx/car/utils/CarUxRestrictionsTestUtils.java
index 71004887..bc1377f6 100644
--- a/androidx/car/utils/CarUxRestrictionsTestUtils.java
+++ b/androidx/car/utils/CarUxRestrictionsTestUtils.java
@@ -27,10 +27,12 @@ public class CarUxRestrictionsTestUtils {
private CarUxRestrictionsTestUtils() {};
public static CarUxRestrictions getFullyRestricted() {
- return new CarUxRestrictions(true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0);
+ return new CarUxRestrictions.Builder(
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0).build();
}
public static CarUxRestrictions getBaseline() {
- return new CarUxRestrictions(false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 0);
+ return new CarUxRestrictions.Builder(
+ false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE, 0).build();
}
}
diff --git a/androidx/car/widget/DayNightStyle.java b/androidx/car/widget/DayNightStyle.java
index 37d0d515..73f9ce48 100644
--- a/androidx/car/widget/DayNightStyle.java
+++ b/androidx/car/widget/DayNightStyle.java
@@ -16,8 +16,12 @@
package androidx.car.widget;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+
/**
* Specifies how the system UI should respond to day/night mode events.
*
@@ -37,6 +41,7 @@ import androidx.annotation.IntDef;
DayNightStyle.FORCE_NIGHT,
DayNightStyle.FORCE_DAY,
})
+@Retention(SOURCE)
public @interface DayNightStyle {
/**
* Sets the foreground color to be automatically changed based on day/night mode, assuming the
diff --git a/androidx/car/widget/PagedListView.java b/androidx/car/widget/PagedListView.java
index 3665fd68..4d16e0cf 100644
--- a/androidx/car/widget/PagedListView.java
+++ b/androidx/car/widget/PagedListView.java
@@ -18,6 +18,8 @@ package androidx.car.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -50,6 +52,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
+import java.lang.annotation.Retention;
+
/**
* View that wraps a {@link RecyclerView} and a scroll bar that has
* page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
@@ -187,6 +191,7 @@ public class PagedListView extends FrameLayout {
Gutter.END,
Gutter.BOTH,
})
+ @Retention(SOURCE)
public @interface Gutter {
/**
* No gutter on either side of the list items. The items will span the full width of the
@@ -260,7 +265,7 @@ public class PagedListView extends FrameLayout {
mSnapHelper = new PagedSnapHelper(context);
mSnapHelper.attachToRecyclerView(mRecyclerView);
- mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener);
+ mRecyclerView.addOnScrollListener(mRecyclerViewOnScrollListener);
mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
int defaultGutterSize = getResources().getDimensionPixelSize(R.dimen.car_margin);
@@ -850,6 +855,13 @@ public class PagedListView extends FrameLayout {
int screenSize = mRecyclerView.getHeight();
int scrollDistance = screenSize;
+ // If the last item is partially visible, page down should bring it to the top.
+ View lastChild = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
+ if (mRecyclerView.getLayoutManager().isViewPartiallyVisible(lastChild,
+ /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)) {
+ scrollDistance = orientationHelper.getDecoratedStart(lastChild);
+ }
+
// The iteration order matters. In case where there are 2 items longer than screen size, we
// want to focus on upcoming view (the one at the bottom of screen).
for (int i = mRecyclerView.getChildCount() - 1; i >= 0; i--) {
diff --git a/androidx/car/widget/SeekbarListItem.java b/androidx/car/widget/SeekbarListItem.java
index 368e6c03..24dd58b1 100644
--- a/androidx/car/widget/SeekbarListItem.java
+++ b/androidx/car/widget/SeekbarListItem.java
@@ -21,10 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.IdRes;
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
@@ -38,6 +34,9 @@ import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import androidx.car.R;
import androidx.car.utils.CarUxRestrictionsUtils;
@@ -96,7 +95,6 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
@PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
- private int mPrimaryActionIconResId;
private Drawable mPrimaryActionIconDrawable;
private String mText;
@@ -106,7 +104,7 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
@SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
- private int mSupplementalIconResId;
+ private Drawable mSupplementalIconDrawable;
private View.OnClickListener mSupplementalIconOnClickListener;
private boolean mShowSupplementalIconDivider;
@@ -250,12 +248,7 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
case PRIMARY_ACTION_TYPE_SMALL_ICON:
mBinders.add(vh -> {
vh.getPrimaryIcon().setVisibility(View.VISIBLE);
-
- if (mPrimaryActionIconDrawable != null) {
- vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
- } else if (mPrimaryActionIconResId != 0) {
- vh.getPrimaryIcon().setImageResource(mPrimaryActionIconResId);
- }
+ vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
});
break;
default:
@@ -405,7 +398,8 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
}
- vh.getSupplementalIcon().setImageResource(mSupplementalIconResId);
+ vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable);
+
vh.getSupplementalIcon().setOnClickListener(
mSupplementalIconOnClickListener);
vh.getSupplementalIcon().setClickable(
@@ -423,7 +417,7 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
* @param iconResId the resource identifier of the drawable.
*/
public void setPrimaryActionIcon(@DrawableRes int iconResId) {
- setPrimaryActionIcon(null, iconResId);
+ setPrimaryActionIcon(mContext.getDrawable(iconResId));
}
/**
@@ -432,15 +426,8 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
* @param drawable the Drawable to set, or null to clear the content.
*/
public void setPrimaryActionIcon(Drawable drawable) {
- setPrimaryActionIcon(drawable, 0);
- }
-
- private void setPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId) {
mPrimaryActionType = PRIMARY_ACTION_TYPE_SMALL_ICON;
-
mPrimaryActionIconDrawable = drawable;
- mPrimaryActionIconResId = iconResId;
-
markDirty();
}
@@ -458,18 +445,34 @@ public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
/**
* Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
*/
- public void setSupplementalIcon(int iconResId, boolean showSupplementalIconDivider) {
- setSupplementalIcon(iconResId, showSupplementalIconDivider, null);
+ public void setSupplementalIcon(@DrawableRes int iconResId,
+ boolean showSupplementalIconDivider) {
+ setSupplementalIcon(mContext.getDrawable(iconResId), showSupplementalIconDivider, null);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ */
+ public void setSupplementalIcon(@DrawableRes int iconResId, boolean showSupplementalIconDivider,
+ @Nullable View.OnClickListener listener) {
+ setSupplementalIcon(mContext.getDrawable(iconResId), showSupplementalIconDivider, listener);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ */
+ public void setSupplementalIcon(Drawable drawable, boolean showSupplementalIconDivider) {
+ setSupplementalIcon(drawable, showSupplementalIconDivider, null);
}
/**
* Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
*/
- public void setSupplementalIcon(@IdRes int iconResId,
- boolean showSupplementalIconDivider, @Nullable View.OnClickListener listener) {
+ public void setSupplementalIcon(Drawable drawable, boolean showSupplementalIconDivider,
+ @Nullable View.OnClickListener listener) {
mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
- mSupplementalIconResId = iconResId;
+ mSupplementalIconDrawable = drawable;
mShowSupplementalIconDivider = showSupplementalIconDivider;
mSupplementalIconOnClickListener = listener;
diff --git a/androidx/car/widget/SeekbarListItemTest.java b/androidx/car/widget/SeekbarListItemTest.java
index 207a51ef..f8b8d33b 100644
--- a/androidx/car/widget/SeekbarListItemTest.java
+++ b/androidx/car/widget/SeekbarListItemTest.java
@@ -30,6 +30,7 @@ import static org.junit.Assert.assertTrue;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.filters.SmallTest;
@@ -278,6 +279,18 @@ public class SeekbarListItemTest {
}
}
+ @Test
+ public void testSettingSupplementalIconWithDrawable() {
+ Drawable drawable = mActivity.getDrawable(android.R.drawable.sym_def_app_icon);
+ SeekbarListItem item = new SeekbarListItem(mActivity, 0, 0, null, null);
+ item.setSupplementalIcon(drawable, false);
+
+ setupPagedListView(Arrays.asList(item));
+
+ assertThat(getViewHolderAtPosition(0).getSupplementalIcon().getDrawable(),
+ is(equalTo(drawable)));
+ }
+
private static ViewAction clickChildViewWithId(final int id) {
return new ViewAction() {
@Override
diff --git a/androidx/car/widget/SubheaderListItem.java b/androidx/car/widget/SubheaderListItem.java
index 87246961..dd5a00be 100644
--- a/androidx/car/widget/SubheaderListItem.java
+++ b/androidx/car/widget/SubheaderListItem.java
@@ -16,6 +16,8 @@
package androidx.car.widget;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.res.TypedArray;
@@ -23,15 +25,16 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
-import java.util.ArrayList;
-import java.util.List;
-
import androidx.annotation.DimenRes;
import androidx.annotation.IntDef;
import androidx.annotation.StyleRes;
import androidx.car.R;
import androidx.car.utils.CarUxRestrictionsUtils;
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Class to build a sub-header list item.
*
@@ -72,6 +75,7 @@ public class SubheaderListItem extends ListItem<SubheaderListItem.ViewHolder> {
@IntDef({
TEXT_START_MARGIN_TYPE_NONE, TEXT_START_MARGIN_TYPE_LARGE,
TEXT_START_MARGIN_TYPE_SMALL})
+ @Retention(SOURCE)
public @interface TextStartMarginType {}
/**
diff --git a/androidx/car/widget/TextListItem.java b/androidx/car/widget/TextListItem.java
index ce64a2dd..3681d3e6 100644
--- a/androidx/car/widget/TextListItem.java
+++ b/androidx/car/widget/TextListItem.java
@@ -109,7 +109,6 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
private View.OnClickListener mOnClickListener;
@PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
- private int mPrimaryActionIconResId;
private Drawable mPrimaryActionIconDrawable;
private String mTitle;
@@ -117,7 +116,7 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
private boolean mIsBodyPrimary;
@SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
- private int mSupplementalIconResId;
+ private Drawable mSupplementalIconDrawable;
private View.OnClickListener mSupplementalIconOnClickListener;
private boolean mShowSupplementalIconDivider;
@@ -257,11 +256,7 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
mBinders.add(vh -> {
vh.getPrimaryIcon().setVisibility(View.VISIBLE);
- if (mPrimaryActionIconDrawable != null) {
- vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
- } else if (mPrimaryActionIconResId != 0) {
- vh.getPrimaryIcon().setImageResource(mPrimaryActionIconResId);
- }
+ vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
});
break;
case PRIMARY_ACTION_TYPE_EMPTY_ICON:
@@ -544,7 +539,7 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
}
- vh.getSupplementalIcon().setImageResource(mSupplementalIconResId);
+ vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable);
vh.getSupplementalIcon().setOnClickListener(
mSupplementalIconOnClickListener);
vh.getSupplementalIcon().setClickable(
@@ -606,7 +601,7 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
* @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item.
*/
public void setPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) {
- setPrimaryActionIcon(null, iconResId, useLargeIcon);
+ setPrimaryActionIcon(mContext.getDrawable(iconResId), useLargeIcon);
}
/**
@@ -616,15 +611,9 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
* @param useLargeIcon the size of primary icon. Large Icon is a square as tall as an item.
*/
public void setPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) {
- setPrimaryActionIcon(drawable, 0, useLargeIcon);
- }
-
- private void setPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId,
- boolean useLargeIcon) {
mPrimaryActionType = useLargeIcon
? PRIMARY_ACTION_TYPE_LARGE_ICON
: PRIMARY_ACTION_TYPE_SMALL_ICON;
- mPrimaryActionIconResId = iconResId;
mPrimaryActionIconDrawable = drawable;
markDirty();
@@ -696,7 +685,18 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
* {@code Supplemental Icon}.
*/
public void setSupplementalIcon(int iconResId, boolean showDivider) {
- setSupplementalIcon(iconResId, showDivider, null);
+ setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, null);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ *
+ * @param drawable the Drawable to set, or null to clear the content.
+ * @param showDivider whether to display a vertical bar that separates {@code text} and
+ * {@code Supplemental Icon}.
+ */
+ public void setSupplementalIcon(Drawable drawable, boolean showDivider) {
+ setSupplementalIcon(drawable, showDivider, null);
}
/**
@@ -709,9 +709,22 @@ public class TextListItem extends ListItem<TextListItem.ViewHolder> {
*/
public void setSupplementalIcon(int iconResId, boolean showDivider,
View.OnClickListener listener) {
+ setSupplementalIcon(mContext.getDrawable(iconResId), showDivider, listener);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ *
+ * @param drawable the Drawable to set, or null to clear the content.
+ * @param showDivider whether to display a vertical bar that separates {@code text} and
+ * {@code Supplemental Icon}.
+ * @param listener the callback that will run when icon is clicked.
+ */
+ public void setSupplementalIcon(Drawable drawable, boolean showDivider,
+ View.OnClickListener listener) {
mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
- mSupplementalIconResId = iconResId;
+ mSupplementalIconDrawable = drawable;
mSupplementalIconOnClickListener = listener;
mShowSupplementalIconDivider = showDivider;
markDirty();
diff --git a/androidx/car/widget/TextListItemTest.java b/androidx/car/widget/TextListItemTest.java
index 9a9e46d4..c96b46db 100644
--- a/androidx/car/widget/TextListItemTest.java
+++ b/androidx/car/widget/TextListItemTest.java
@@ -212,6 +212,18 @@ public class TextListItemTest {
}
@Test
+ public void testSetSupplementalActionWithDrawable() {
+ Drawable drawable = mActivity.getDrawable(android.R.drawable.sym_def_app_icon);
+ TextListItem item = new TextListItem(mActivity);
+ item.setSupplementalIcon(drawable, true);
+
+ setupPagedListView(Arrays.asList(item));
+
+ assertThat(getViewHolderAtPosition(0).getSupplementalIcon().getDrawable(),
+ is(equalTo(drawable)));
+ }
+
+ @Test
public void testSwitchVisibleAndCheckedState() {
TextListItem item0 = new TextListItem(mActivity);
item0.setSwitch(true, true, null);
diff --git a/androidx/contentpager/content/Query.java b/androidx/contentpager/content/Query.java
index 948c6daa..d1cfbde1 100644
--- a/androidx/contentpager/content/Query.java
+++ b/androidx/contentpager/content/Query.java
@@ -28,7 +28,6 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
@@ -52,7 +51,6 @@ public final class Query {
private final CancellationSignal mCancellationSignal;
private final ContentPager.ContentCallback mCallback;
- @VisibleForTesting
Query(
@NonNull Uri uri,
@Nullable String[] projection,
diff --git a/androidx/core/app/ActivityCompat.java b/androidx/core/app/ActivityCompat.java
index 9d9b9bcc..f9c677be 100644
--- a/androidx/core/app/ActivityCompat.java
+++ b/androidx/core/app/ActivityCompat.java
@@ -356,6 +356,7 @@ public class ActivityCompat extends ContextCompat {
* @see Activity#findViewById(int)
* @see androidx.core.view.ViewCompat#requireViewById(View, int)
*/
+ @SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public static <T extends View> T requireViewById(@NonNull Activity activity, @IdRes int id) {
// TODO: use and link to Activity#requireViewById() directly, once available
diff --git a/androidx/core/app/NotificationCompat.java b/androidx/core/app/NotificationCompat.java
index 6d1ce462..aba40d1e 100644
--- a/androidx/core/app/NotificationCompat.java
+++ b/androidx/core/app/NotificationCompat.java
@@ -2214,9 +2214,14 @@ public class NotificationCompat {
* @see Message#Message(CharSequence, long, CharSequence)
*
* @return this object for method chaining
+ *
+ * @deprecated Use {@link #addMessage(CharSequence, long, Person)} or
+ * {@link #addMessage(Message)}
*/
+ @Deprecated
public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
- mMessages.add(new Message(text, timestamp, sender));
+ mMessages.add(
+ new Message(text, timestamp, new Person.Builder().setName(sender).build()));
if (mMessages.size() > MAXIMUM_RETAINED_MESSAGES) {
mMessages.remove(0);
}
@@ -2224,8 +2229,23 @@ public class NotificationCompat {
}
/**
+ * Adds a message for display by this notification. Convenience call for
+ * {@link #addMessage(Message)}.
+ *
+ * @see Message#Message(CharSequence, long, Person)
+ *
+ * @return this for method chaining
+ */
+ public MessagingStyle addMessage(CharSequence text, long timestamp, Person person) {
+ addMessage(new Message(text, timestamp, person));
+ return this;
+ }
+
+ /**
* Adds a {@link Message} for display in this notification.
+ *
* @param message The {@link Message} to be displayed
+ *
* @return this object for method chaining
*/
public MessagingStyle addMessage(Message message) {
@@ -2462,24 +2482,42 @@ public class NotificationCompat {
}
public static final class Message {
-
static final String KEY_TEXT = "text";
static final String KEY_TIMESTAMP = "time";
static final String KEY_SENDER = "sender";
static final String KEY_DATA_MIME_TYPE = "type";
static final String KEY_DATA_URI= "uri";
static final String KEY_EXTRAS_BUNDLE = "extras";
+ static final String KEY_PERSON = "person";
private final CharSequence mText;
private final long mTimestamp;
- private final CharSequence mSender;
+ @Nullable private final Person mPerson;
private Bundle mExtras = new Bundle();
- private String mDataMimeType;
- private Uri mDataUri;
+ @Nullable private String mDataMimeType;
+ @Nullable private Uri mDataUri;
+
+ /**
+ * Creates a new {@link Message} with the given text, timestamp, and sender.
+ *
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived in ms since Unix epoch
+ * @param person A {@link Person} whose {@link Person#getName()} value is used as the
+ * display name for the sender. This should be {@code null} for messages by the current
+ * user, in which case, the platform will insert
+ * {@link MessagingStyle#getUserDisplayName()}. A {@link Person}'s key should be
+ * consistent during re-posts of the notification.
+ */
+ public Message(CharSequence text, long timestamp, @Nullable Person person) {
+ mText = text;
+ mTimestamp = timestamp;
+ mPerson = person;
+ }
/**
* Constructor
+ *
* @param text A {@link CharSequence} to be displayed as the message content
* @param timestamp Time at which the message arrived in ms since Unix epoch
* @param sender A {@link CharSequence} to be used for displaying the name of the
@@ -2487,17 +2525,19 @@ public class NotificationCompat {
* the platform will insert {@link MessagingStyle#getUserDisplayName()}.
* Should be unique amongst all individuals in the conversation, and should be
* consistent during re-posts of the notification.
+ *
+ * @deprecated Use the alternative constructor instead.
*/
+ @Deprecated
public Message(CharSequence text, long timestamp, CharSequence sender){
- mText = text;
- mTimestamp = timestamp;
- mSender = sender;
+ this(text, timestamp, new Person.Builder().setName(sender).build());
}
/**
* Sets a binary blob of data and an associated MIME type for a message. In the case
* where the platform doesn't support the MIME type, the original text provided in the
* constructor will be used.
+ *
* @param dataMimeType The MIME type of the content. See
* <a href="{@docRoot}notifications/messaging.html"> for the list of supported MIME
* types on Android and Android Wear.
@@ -2519,6 +2559,7 @@ public class NotificationCompat {
* Note that once added to the system MediaStore the content is accessible to any
* app on the device.</li>
* </ol>
+ *
* @return this object for method chaining
*/
public Message setData(String dataMimeType, Uri dataUri) {
@@ -2531,34 +2572,41 @@ public class NotificationCompat {
* Get the text to be used for this message, or the fallback text if a type and content
* Uri have been set
*/
+ @NonNull
public CharSequence getText() {
return mText;
}
- /**
- * Get the time at which this message arrived in ms since Unix epoch
- */
+ /** Get the time at which this message arrived in ms since Unix epoch. */
public long getTimestamp() {
return mTimestamp;
}
- /**
- * Get the extras Bundle for this message.
- */
+ /** Get the extras Bundle for this message. */
+ @NonNull
public Bundle getExtras() {
return mExtras;
}
/**
* Get the text used to display the contact's name in the messaging experience
+ *
+ * @deprecated Use {@link #getPerson()}
*/
+ @Deprecated
+ @Nullable
public CharSequence getSender() {
- return mSender;
+ return mPerson.getName();
}
- /**
- * Get the MIME type of the data pointed to by the Uri
- */
+ /** Returns the {@link Person} sender of this message. */
+ @Nullable
+ public Person getPerson() {
+ return mPerson;
+ }
+
+ /** Get the MIME type of the data pointed to by the URI. */
+ @Nullable
public String getDataMimeType() {
return mDataMimeType;
}
@@ -2567,6 +2615,7 @@ public class NotificationCompat {
* Get the the Uri pointing to the content of the message. Can be null, in which case
* {@see #getText()} is used.
*/
+ @Nullable
public Uri getDataUri() {
return mDataUri;
}
@@ -2577,8 +2626,8 @@ public class NotificationCompat {
bundle.putCharSequence(KEY_TEXT, mText);
}
bundle.putLong(KEY_TIMESTAMP, mTimestamp);
- if (mSender != null) {
- bundle.putCharSequence(KEY_SENDER, mSender);
+ if (mPerson != null) {
+ bundle.putBundle(KEY_PERSON, mPerson.toBundle());
}
if (mDataMimeType != null) {
bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType);
@@ -2592,6 +2641,7 @@ public class NotificationCompat {
return bundle;
}
+ @NonNull
static Bundle[] getBundleArrayForMessages(List<Message> messages) {
Bundle[] bundles = new Bundle[messages.size()];
final int N = messages.size();
@@ -2601,6 +2651,7 @@ public class NotificationCompat {
return bundles;
}
+ @NonNull
static List<Message> getMessagesFromBundleArray(Parcelable[] bundles) {
List<Message> messages = new ArrayList<>(bundles.length);
for (int i = 0; i < bundles.length; i++) {
@@ -2614,23 +2665,38 @@ public class NotificationCompat {
return messages;
}
+ @Nullable
static Message getMessageFromBundle(Bundle bundle) {
try {
if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) {
return null;
+ }
+
+ Message message;
+ if (bundle.containsKey(KEY_SENDER)) {
+ // Legacy sender
+ message = new Message(
+ bundle.getCharSequence(KEY_TEXT),
+ bundle.getLong(KEY_TIMESTAMP),
+ new Person.Builder()
+ .setName(bundle.getCharSequence(KEY_SENDER))
+ .build());
} else {
- Message message = new Message(bundle.getCharSequence(KEY_TEXT),
- bundle.getLong(KEY_TIMESTAMP), bundle.getCharSequence(KEY_SENDER));
- if (bundle.containsKey(KEY_DATA_MIME_TYPE) &&
- bundle.containsKey(KEY_DATA_URI)) {
- message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
- (Uri) bundle.getParcelable(KEY_DATA_URI));
- }
- if (bundle.containsKey(KEY_EXTRAS_BUNDLE)) {
- message.getExtras().putAll(bundle.getBundle(KEY_EXTRAS_BUNDLE));
- }
- return message;
+ message = new Message(
+ bundle.getCharSequence(KEY_TEXT),
+ bundle.getLong(KEY_TIMESTAMP),
+ Person.fromBundle(bundle.getBundle(KEY_PERSON)));
+ }
+
+ if (bundle.containsKey(KEY_DATA_MIME_TYPE)
+ && bundle.containsKey(KEY_DATA_URI)) {
+ message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
+ (Uri) bundle.getParcelable(KEY_DATA_URI));
+ }
+ if (bundle.containsKey(KEY_EXTRAS_BUNDLE)) {
+ message.getExtras().putAll(bundle.getBundle(KEY_EXTRAS_BUNDLE));
}
+ return message;
} catch (ClassCastException e) {
return null;
}
diff --git a/androidx/core/app/NotificationCompatTest.java b/androidx/core/app/NotificationCompatTest.java
index 2c28cc8b..a8828912 100644
--- a/androidx/core/app/NotificationCompatTest.java
+++ b/androidx/core/app/NotificationCompatTest.java
@@ -44,6 +44,8 @@ import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.BaseInstrumentationTestCase;
+import androidx.core.app.NotificationCompat.MessagingStyle.Message;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -315,18 +317,18 @@ public class NotificationCompatTest extends BaseInstrumentationTestCase<TestSupp
public void testMessage_setAndGetExtras() throws Throwable {
String extraKey = "extra_key";
CharSequence extraValue = "extra_value";
- NotificationCompat.MessagingStyle.Message m =
- new NotificationCompat.MessagingStyle.Message("text", 0 /*timestamp */, "sender");
+ Message m =
+ new Message("text", 0 /*timestamp */, "sender");
m.getExtras().putCharSequence(extraKey, extraValue);
assertEquals(extraValue, m.getExtras().getCharSequence(extraKey));
- ArrayList<NotificationCompat.MessagingStyle.Message> messages = new ArrayList<>(1);
+ ArrayList<Message> messages = new ArrayList<>(1);
messages.add(m);
Bundle[] bundleArray =
- NotificationCompat.MessagingStyle.Message.getBundleArrayForMessages(messages);
+ Message.getBundleArrayForMessages(messages);
assertEquals(1, bundleArray.length);
- NotificationCompat.MessagingStyle.Message fromBundle =
- NotificationCompat.MessagingStyle.Message.getMessageFromBundle(bundleArray[0]);
+ Message fromBundle =
+ Message.getMessageFromBundle(bundleArray[0]);
assertEquals(extraValue, fromBundle.getExtras().getCharSequence(extraKey));
}
@@ -526,6 +528,38 @@ public class NotificationCompatTest extends BaseInstrumentationTestCase<TestSupp
}
@Test
+ public void testMessagingStyle_message() {
+ NotificationCompat.MessagingStyle messagingStyle =
+ new NotificationCompat.MessagingStyle("self name");
+ Person person = new Person.Builder().setName("test name").setKey("key").build();
+ Person person2 = new Person.Builder()
+ .setName("test name 2").setKey("key 2").setImportant(true).build();
+ messagingStyle.addMessage("text", 200, person);
+ messagingStyle.addMessage("text2", 300, person2);
+
+ Notification notification = new NotificationCompat.Builder(mContext, "test id")
+ .setSmallIcon(1)
+ .setContentTitle("test title")
+ .setStyle(messagingStyle)
+ .build();
+
+ List<Message> result = NotificationCompat.MessagingStyle
+ .extractMessagingStyleFromNotification(notification)
+ .getMessages();
+
+ assertEquals(2, result.size());
+ assertEquals("text", result.get(0).getText());
+ assertEquals(200, result.get(0).getTimestamp());
+ assertEquals("test name", result.get(0).getPerson().getName());
+ assertEquals("key", result.get(0).getPerson().getKey());
+ assertEquals("text2", result.get(1).getText());
+ assertEquals(300, result.get(1).getTimestamp());
+ assertEquals("test name 2", result.get(1).getPerson().getName());
+ assertEquals("key 2", result.get(1).getPerson().getKey());
+ assertTrue(result.get(1).getPerson().isImportant());
+ }
+
+ @Test
public void testMessagingStyle_isGroupConversation() {
mContext.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.P;
NotificationCompat.MessagingStyle messagingStyle =
@@ -665,6 +699,17 @@ public class NotificationCompatTest extends BaseInstrumentationTestCase<TestSupp
}
@Test
+ public void testMessagingStyleMessage_bundle_legacySender() {
+ Bundle legacyBundle = new Bundle();
+ legacyBundle.putCharSequence(Message.KEY_TEXT, "message");
+ legacyBundle.putLong(Message.KEY_TIMESTAMP, 100);
+ legacyBundle.putCharSequence(Message.KEY_SENDER, "sender");
+
+ Message result = Message.getMessageFromBundle(legacyBundle);
+ assertEquals("sender", result.getPerson().getName());
+ }
+
+ @Test
public void action_builder_hasDefault() {
NotificationCompat.Action action =
new NotificationCompat.Action.Builder(0, "Test Title", null).build();
diff --git a/androidx/core/app/Person.java b/androidx/core/app/Person.java
index 3bda5102..e79076e3 100644
--- a/androidx/core/app/Person.java
+++ b/androidx/core/app/Person.java
@@ -16,11 +16,11 @@
package androidx.core.app;
-import android.graphics.Bitmap;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.IconCompat;
/**
* Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
@@ -39,9 +39,10 @@ public class Person {
* created from a {@link Person} using {@link #toBundle()}.
*/
public static Person fromBundle(Bundle bundle) {
+ Bundle iconBundle = bundle.getBundle(ICON_KEY);
return new Builder()
.setName(bundle.getCharSequence(NAME_KEY))
- .setIcon((Bitmap) bundle.getParcelable(ICON_KEY))
+ .setIcon(iconBundle != null ? IconCompat.createFromBundle(iconBundle) : null)
.setUri(bundle.getString(URI_KEY))
.setKey(bundle.getString(KEY_KEY))
.setBot(bundle.getBoolean(IS_BOT_KEY))
@@ -50,7 +51,7 @@ public class Person {
}
@Nullable private CharSequence mName;
- @Nullable private Bitmap mIcon;
+ @Nullable private IconCompat mIcon;
@Nullable private String mUri;
@Nullable private String mKey;
private boolean mIsBot;
@@ -72,7 +73,7 @@ public class Person {
public Bundle toBundle() {
Bundle result = new Bundle();
result.putCharSequence(NAME_KEY, mName);
- result.putParcelable(ICON_KEY, mIcon);
+ result.putBundle(ICON_KEY, mIcon != null ? mIcon.toBundle() : null);
result.putString(URI_KEY, mUri);
result.putString(KEY_KEY, mKey);
result.putBoolean(IS_BOT_KEY, mIsBot);
@@ -96,7 +97,7 @@ public class Person {
/** Returns the icon for this {@link Person} or {@code null} if no icon was provided. */
@Nullable
- public Bitmap getIcon() {
+ public IconCompat getIcon() {
return mIcon;
}
@@ -146,7 +147,7 @@ public class Person {
/** Builder for the immutable {@link Person} class. */
public static class Builder {
@Nullable private CharSequence mName;
- @Nullable private Bitmap mIcon;
+ @Nullable private IconCompat mIcon;
@Nullable private String mUri;
@Nullable private String mKey;
private boolean mIsBot;
@@ -181,7 +182,7 @@ public class Person {
* {@link #setUri(String)}.
*/
@NonNull
- public Builder setIcon(@Nullable Bitmap icon) {
+ public Builder setIcon(@Nullable IconCompat icon) {
mIcon = icon;
return this;
}
diff --git a/androidx/core/app/PersonTest.java b/androidx/core/app/PersonTest.java
index 20b2090f..12f61176 100644
--- a/androidx/core/app/PersonTest.java
+++ b/androidx/core/app/PersonTest.java
@@ -17,14 +17,16 @@
package androidx.core.app;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import android.graphics.Bitmap;
import android.os.Bundle;
-import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import androidx.core.graphics.drawable.IconCompat;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,14 +34,14 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class PersonTest {
private static final CharSequence TEST_NAME = "Example Name";
- private static final Bitmap TEST_ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ private static final IconCompat TEST_ICON =
+ IconCompat.createWithBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888));
private static final String TEST_URI = "mailto:example@example.com";
private static final String TEST_KEY = "example-key";
private static final boolean TEST_IS_BOT = true;
private static final boolean TEST_IS_IMPORTANT = true;
@Test
- @SdkSuppress(minSdkVersion = 12)
public void bundle() {
Person person = new Person.Builder()
.setImportant(TEST_IS_IMPORTANT)
@@ -58,13 +60,25 @@ public class PersonTest {
assertEquals(TEST_KEY, result.getKey());
assertEquals(TEST_IS_BOT, result.isBot());
assertEquals(TEST_IS_IMPORTANT, result.isImportant());
+ assertEquals(TEST_ICON.toBundle().toString(), result.getIcon().toBundle().toString());
+ }
+
+ @Test
+ public void bundle_defaultValues() {
+ Person person = new Person.Builder().build();
- // Requires SDK >= 12
- assertTrue(TEST_ICON.sameAs(result.getIcon()));
+ Bundle personBundle = person.toBundle();
+ Person result = Person.fromBundle(personBundle);
+
+ assertNull(result.getIcon());
+ assertNull(result.getKey());
+ assertNull(result.getName());
+ assertNull(result.getUri());
+ assertFalse(result.isImportant());
+ assertFalse(result.isBot());
}
@Test
- @SdkSuppress(minSdkVersion = 12)
public void toBuilder() {
Person person = new Person.Builder()
.setImportant(TEST_IS_IMPORTANT)
@@ -81,9 +95,7 @@ public class PersonTest {
assertEquals(TEST_KEY, result.getKey());
assertEquals(TEST_IS_BOT, result.isBot());
assertEquals(TEST_IS_IMPORTANT, result.isImportant());
-
- // Requires SDK >= 12
- assertTrue(TEST_ICON.sameAs(result.getIcon()));
+ assertEquals(TEST_ICON.toBundle().toString(), result.getIcon().toBundle().toString());
}
@Test
diff --git a/androidx/core/content/pm/PackageInfoCompat.java b/androidx/core/content/pm/PackageInfoCompat.java
new file mode 100644
index 00000000..71c53f22
--- /dev/null
+++ b/androidx/core/content/pm/PackageInfoCompat.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content.pm;
+
+import android.content.pm.PackageInfo;
+
+import androidx.annotation.NonNull;
+import androidx.core.os.BuildCompat;
+
+/** Helper for accessing features in {@link PackageInfo}. */
+public final class PackageInfoCompat {
+ /**
+ * Return {@link android.R.attr#versionCode} and {@link android.R.attr#versionCodeMajor}
+ * combined together as a single long value. The {@code versionCodeMajor} is placed in the
+ * upper 32 bits on Android P or newer, otherwise these bits are all set to 0.
+ *
+ * @see PackageInfo#getLongVersionCode()
+ */
+ public static long getLongVersionCode(@NonNull PackageInfo info) {
+ if (BuildCompat.isAtLeastP()) {
+ return info.getLongVersionCode();
+ }
+
+ return info.versionCode;
+ }
+
+ private PackageInfoCompat() {
+ }
+}
diff --git a/androidx/core/content/pm/PackageInfoCompatTest.java b/androidx/core/content/pm/PackageInfoCompatTest.java
new file mode 100644
index 00000000..e3e55047
--- /dev/null
+++ b/androidx/core/content/pm/PackageInfoCompatTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.content.pm;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.pm.PackageInfo;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public final class PackageInfoCompatTest {
+ @Test
+ public void getLongVersionCodeLowerBitsOnly() {
+ PackageInfo info = new PackageInfo();
+ info.versionCode = 12345;
+
+ assertEquals(12345L, PackageInfoCompat.getLongVersionCode(info));
+ }
+
+ @SdkSuppress(minSdkVersion = P)
+ @Test
+ public void getLongVersionCodeLowerAndUpperBits() {
+ PackageInfo info = new PackageInfo();
+ info.setLongVersionCode(Long.MAX_VALUE);
+
+ assertEquals(Long.MAX_VALUE, PackageInfoCompat.getLongVersionCode(info));
+ }
+}
diff --git a/androidx/core/content/pm/ShortcutInfoCompat.java b/androidx/core/content/pm/ShortcutInfoCompat.java
index 7eea5519..67415acf 100644
--- a/androidx/core/content/pm/ShortcutInfoCompat.java
+++ b/androidx/core/content/pm/ShortcutInfoCompat.java
@@ -26,7 +26,6 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import java.util.Arrays;
@@ -74,7 +73,6 @@ public class ShortcutInfoCompat {
return builder.build();
}
- @VisibleForTesting
Intent addToIntent(Intent outIntent) {
outIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, mIntents[mIntents.length - 1])
.putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabel.toString());
diff --git a/androidx/core/text/HtmlCompat.java b/androidx/core/text/HtmlCompat.java
index b8468051..a5b97eab 100644
--- a/androidx/core/text/HtmlCompat.java
+++ b/androidx/core/text/HtmlCompat.java
@@ -18,6 +18,8 @@ package androidx.core.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.os.Build;
@@ -33,6 +35,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import java.lang.annotation.Retention;
+
/**
* Backwards compatible version of {@link Html}.
*/
@@ -119,6 +123,7 @@ public final class HtmlCompat {
FROM_HTML_MODE_LEGACY
}, flag = true)
@RestrictTo(LIBRARY)
+ @Retention(SOURCE)
@interface FromHtmlFlags {
}
@@ -128,6 +133,7 @@ public final class HtmlCompat {
TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
})
@RestrictTo(LIBRARY)
+ @Retention(SOURCE)
@interface ToHtmlOptions {
}
diff --git a/androidx/core/text/util/FindAddress.java b/androidx/core/text/util/FindAddress.java
index d66fa055..0602428c 100644
--- a/androidx/core/text/util/FindAddress.java
+++ b/androidx/core/text/util/FindAddress.java
@@ -495,8 +495,7 @@ class FindAddress {
* @param content The string to search.
* @return The first valid address, or null if no address was matched.
*/
- @VisibleForTesting
- public static String findAddress(String content) {
+ static String findAddress(String content) {
Matcher houseNumberMatcher = sHouseNumberRe.matcher(content);
int start = 0;
while (houseNumberMatcher.find(start)) {
diff --git a/androidx/core/view/ViewCompat.java b/androidx/core/view/ViewCompat.java
index 7b76c384..33026340 100644
--- a/androidx/core/view/ViewCompat.java
+++ b/androidx/core/view/ViewCompat.java
@@ -1344,6 +1344,7 @@ public class ViewCompat {
* @return a view with given ID
* @see View#findViewById(int)
*/
+ @SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public static <T extends View> T requireViewById(@NonNull View view, @IdRes int id) {
// TODO: use and link to View#requireViewById() directly, once available
diff --git a/androidx/core/view/WindowCompat.java b/androidx/core/view/WindowCompat.java
index 5ee42a27..ee5d44c1 100644
--- a/androidx/core/view/WindowCompat.java
+++ b/androidx/core/view/WindowCompat.java
@@ -78,6 +78,7 @@ public final class WindowCompat {
* @see ViewCompat#requireViewById(View, int)
* @see Window#findViewById(int)
*/
+ @SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public static <T extends View> T requireViewById(@NonNull Window window, @IdRes int id) {
// TODO: use and link to Window#requireViewById() directly, once available
diff --git a/androidx/leanback/system/Settings.java b/androidx/leanback/system/Settings.java
index 04f66d1b..011c6a5e 100644
--- a/androidx/leanback/system/Settings.java
+++ b/androidx/leanback/system/Settings.java
@@ -43,7 +43,7 @@ public class Settings {
// The intent action that must be provided by a broadcast receiver
// in a customization package.
private static final String ACTION_PARTNER_CUSTOMIZATION =
- "androidx.leanback.action.PARTNER_CUSTOMIZATION";
+ "android.support.v17.leanback.action.PARTNER_CUSTOMIZATION";
public static final String PREFER_STATIC_SHADOWS = "PREFER_STATIC_SHADOWS";
diff --git a/androidx/lifecycle/ComputableLiveData.java b/androidx/lifecycle/ComputableLiveData.java
index e2e2a1be..da5a15ee 100644
--- a/androidx/lifecycle/ComputableLiveData.java
+++ b/androidx/lifecycle/ComputableLiveData.java
@@ -1,148 +1,9 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
+//ComputableLiveData interface for tests
package androidx.lifecycle;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.executor.ArchTaskExecutor;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A LiveData class that can be invalidated & computed when there are active observers.
- * <p>
- * It can be invalidated via {@link #invalidate()}, which will result in a call to
- * {@link #compute()} if there are active observers (or when they start observing)
- * <p>
- * This is an internal class for now, might be public if we see the necessity.
- *
- * @param <T> The type of the live data
- * @hide internal
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+import androidx.lifecycle.LiveData;
public abstract class ComputableLiveData<T> {
-
- private final Executor mExecutor;
- private final LiveData<T> mLiveData;
-
- private AtomicBoolean mInvalid = new AtomicBoolean(true);
- private AtomicBoolean mComputing = new AtomicBoolean(false);
-
- /**
- * Creates a computable live data that computes values on the arch IO thread executor.
- */
- @SuppressWarnings("WeakerAccess")
- public ComputableLiveData() {
- this(ArchTaskExecutor.getIOThreadExecutor());
- }
-
- /**
- *
- * Creates a computable live data that computes values on the specified executor.
- *
- * @param executor Executor that is used to compute new LiveData values.
- */
- @SuppressWarnings("WeakerAccess")
- public ComputableLiveData(@NonNull Executor executor) {
- mExecutor = executor;
- mLiveData = new LiveData<T>() {
- @Override
- protected void onActive() {
- mExecutor.execute(mRefreshRunnable);
- }
- };
- }
-
- /**
- * Returns the LiveData managed by this class.
- *
- * @return A LiveData that is controlled by ComputableLiveData.
- */
- @SuppressWarnings("WeakerAccess")
- @NonNull
- public LiveData<T> getLiveData() {
- return mLiveData;
- }
-
- @VisibleForTesting
- final Runnable mRefreshRunnable = new Runnable() {
- @WorkerThread
- @Override
- public void run() {
- boolean computed;
- do {
- computed = false;
- // compute can happen only in 1 thread but no reason to lock others.
- if (mComputing.compareAndSet(false, true)) {
- // as long as it is invalid, keep computing.
- try {
- T value = null;
- while (mInvalid.compareAndSet(true, false)) {
- computed = true;
- value = compute();
- }
- if (computed) {
- mLiveData.postValue(value);
- }
- } finally {
- // release compute lock
- mComputing.set(false);
- }
- }
- // check invalid after releasing compute lock to avoid the following scenario.
- // Thread A runs compute()
- // Thread A checks invalid, it is false
- // Main thread sets invalid to true
- // Thread B runs, fails to acquire compute lock and skips
- // Thread A releases compute lock
- // We've left invalid in set state. The check below recovers.
- } while (computed && mInvalid.get());
- }
- };
-
- // invalidation check always happens on the main thread
- @VisibleForTesting
- final Runnable mInvalidationRunnable = new Runnable() {
- @MainThread
- @Override
- public void run() {
- boolean isActive = mLiveData.hasActiveObservers();
- if (mInvalid.compareAndSet(false, true)) {
- if (isActive) {
- mExecutor.execute(mRefreshRunnable);
- }
- }
- }
- };
-
- /**
- * Invalidates the LiveData.
- * <p>
- * When there are active observers, this will trigger a call to {@link #compute()}.
- */
- public void invalidate() {
- ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
- }
-
- @SuppressWarnings("WeakerAccess")
- @WorkerThread
- protected abstract T compute();
+ public ComputableLiveData(){}
+ abstract protected T compute();
+ public LiveData<T> getLiveData() {return null;}
+ public void invalidate() {}
}
diff --git a/androidx/lifecycle/LiveData.java b/androidx/lifecycle/LiveData.java
index 8c94a950..c961d1c4 100644
--- a/androidx/lifecycle/LiveData.java
+++ b/androidx/lifecycle/LiveData.java
@@ -1,441 +1,4 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
+//LiveData interface for tests
package androidx.lifecycle;
-
-import static androidx.lifecycle.Lifecycle.State.DESTROYED;
-import static androidx.lifecycle.Lifecycle.State.STARTED;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.arch.core.internal.SafeIterableMap;
-import androidx.arch.core.executor.ArchTaskExecutor;
-
-import java.util.Iterator;
-import java.util.Map;
-
-/**
- * LiveData is a data holder class that can be observed within a given lifecycle.
- * This means that an {@link Observer} can be added in a pair with a {@link LifecycleOwner}, and
- * this observer will be notified about modifications of the wrapped data only if the paired
- * LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is
- * {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}. An observer added via
- * {@link #observeForever(Observer)} is considered as always active and thus will be always notified
- * about modifications. For those observers, you should manually call
- * {@link #removeObserver(Observer)}.
- *
- * <p> An observer added with a Lifecycle will be automatically removed if the corresponding
- * Lifecycle moves to {@link Lifecycle.State#DESTROYED} state. This is especially useful for
- * activities and fragments where they can safely observe LiveData and not worry about leaks:
- * they will be instantly unsubscribed when they are destroyed.
- *
- * <p>
- * In addition, LiveData has {@link LiveData#onActive()} and {@link LiveData#onInactive()} methods
- * to get notified when number of active {@link Observer}s change between 0 and 1.
- * This allows LiveData to release any heavy resources when it does not have any Observers that
- * are actively observing.
- * <p>
- * This class is designed to hold individual data fields of {@link ViewModel},
- * but can also be used for sharing data between different modules in your application
- * in a decoupled fashion.
- *
- * @param <T> The type of data held by this instance
- * @see ViewModel
- */
-public abstract class LiveData<T> {
- private final Object mDataLock = new Object();
- static final int START_VERSION = -1;
- private static final Object NOT_SET = new Object();
-
- private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
- new SafeIterableMap<>();
-
- // how many observers are in active state
- private int mActiveCount = 0;
- private volatile Object mData = NOT_SET;
- // when setData is called, we set the pending data and actual data swap happens on the main
- // thread
- private volatile Object mPendingData = NOT_SET;
- private int mVersion = START_VERSION;
-
- private boolean mDispatchingValue;
- @SuppressWarnings("FieldCanBeLocal")
- private boolean mDispatchInvalidated;
- private final Runnable mPostValueRunnable = new Runnable() {
- @Override
- public void run() {
- Object newValue;
- synchronized (mDataLock) {
- newValue = mPendingData;
- mPendingData = NOT_SET;
- }
- //noinspection unchecked
- setValue((T) newValue);
- }
- };
-
- private void considerNotify(ObserverWrapper observer) {
- if (!observer.mActive) {
- return;
- }
- // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
- //
- // we still first check observer.active to keep it as the entrance for events. So even if
- // the observer moved to an active state, if we've not received that event, we better not
- // notify for a more predictable notification order.
- if (!observer.shouldBeActive()) {
- observer.activeStateChanged(false);
- return;
- }
- if (observer.mLastVersion >= mVersion) {
- return;
- }
- observer.mLastVersion = mVersion;
- //noinspection unchecked
- observer.mObserver.onChanged((T) mData);
- }
-
- private void dispatchingValue(@Nullable ObserverWrapper initiator) {
- if (mDispatchingValue) {
- mDispatchInvalidated = true;
- return;
- }
- mDispatchingValue = true;
- do {
- mDispatchInvalidated = false;
- if (initiator != null) {
- considerNotify(initiator);
- initiator = null;
- } else {
- for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
- mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
- considerNotify(iterator.next().getValue());
- if (mDispatchInvalidated) {
- break;
- }
- }
- }
- } while (mDispatchInvalidated);
- mDispatchingValue = false;
- }
-
- /**
- * Adds the given observer to the observers list within the lifespan of the given
- * owner. The events are dispatched on the main thread. If LiveData already has data
- * set, it will be delivered to the observer.
- * <p>
- * The observer will only receive events if the owner is in {@link Lifecycle.State#STARTED}
- * or {@link Lifecycle.State#RESUMED} state (active).
- * <p>
- * If the owner moves to the {@link Lifecycle.State#DESTROYED} state, the observer will
- * automatically be removed.
- * <p>
- * When data changes while the {@code owner} is not active, it will not receive any updates.
- * If it becomes active again, it will receive the last available data automatically.
- * <p>
- * LiveData keeps a strong reference to the observer and the owner as long as the
- * given LifecycleOwner is not destroyed. When it is destroyed, LiveData removes references to
- * the observer &amp; the owner.
- * <p>
- * If the given owner is already in {@link Lifecycle.State#DESTROYED} state, LiveData
- * ignores the call.
- * <p>
- * If the given owner, observer tuple is already in the list, the call is ignored.
- * If the observer is already in the list with another owner, LiveData throws an
- * {@link IllegalArgumentException}.
- *
- * @param owner The LifecycleOwner which controls the observer
- * @param observer The observer that will receive the events
- */
- @MainThread
- public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
- assertMainThread("observe");
- if (owner.getLifecycle().getCurrentState() == DESTROYED) {
- // ignore
- return;
- }
- LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
- ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && !existing.isAttachedTo(owner)) {
- throw new IllegalArgumentException("Cannot add the same observer"
- + " with different lifecycles");
- }
- if (existing != null) {
- return;
- }
- owner.getLifecycle().addObserver(wrapper);
- }
-
- /**
- * Adds the given observer to the observers list. This call is similar to
- * {@link LiveData#observe(LifecycleOwner, Observer)} with a LifecycleOwner, which
- * is always active. This means that the given observer will receive all events and will never
- * be automatically removed. You should manually call {@link #removeObserver(Observer)} to stop
- * observing this LiveData.
- * While LiveData has one of such observers, it will be considered
- * as active.
- * <p>
- * If the observer was already added with an owner to this LiveData, LiveData throws an
- * {@link IllegalArgumentException}.
- *
- * @param observer The observer that will receive the events
- */
- @MainThread
- public void observeForever(@NonNull Observer<? super T> observer) {
- assertMainThread("observeForever");
- AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
- ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
- if (existing != null && existing instanceof LiveData.LifecycleBoundObserver) {
- throw new IllegalArgumentException("Cannot add the same observer"
- + " with different lifecycles");
- }
- if (existing != null) {
- return;
- }
- wrapper.activeStateChanged(true);
- }
-
- /**
- * Removes the given observer from the observers list.
- *
- * @param observer The Observer to receive events.
- */
- @MainThread
- public void removeObserver(@NonNull final Observer<? super T> observer) {
- assertMainThread("removeObserver");
- ObserverWrapper removed = mObservers.remove(observer);
- if (removed == null) {
- return;
- }
- removed.detachObserver();
- removed.activeStateChanged(false);
- }
-
- /**
- * Removes all observers that are tied to the given {@link LifecycleOwner}.
- *
- * @param owner The {@code LifecycleOwner} scope for the observers to be removed.
- */
- @SuppressWarnings("WeakerAccess")
- @MainThread
- public void removeObservers(@NonNull final LifecycleOwner owner) {
- assertMainThread("removeObservers");
- for (Map.Entry<Observer<? super T>, ObserverWrapper> entry : mObservers) {
- if (entry.getValue().isAttachedTo(owner)) {
- removeObserver(entry.getKey());
- }
- }
- }
-
- /**
- * Posts a task to a main thread to set the given value. So if you have a following code
- * executed in the main thread:
- * <pre class="prettyprint">
- * liveData.postValue("a");
- * liveData.setValue("b");
- * </pre>
- * The value "b" would be set at first and later the main thread would override it with
- * the value "a".
- * <p>
- * If you called this method multiple times before a main thread executed a posted task, only
- * the last value would be dispatched.
- *
- * @param value The new value
- */
- protected void postValue(T value) {
- boolean postTask;
- synchronized (mDataLock) {
- postTask = mPendingData == NOT_SET;
- mPendingData = value;
- }
- if (!postTask) {
- return;
- }
- ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
- }
-
- /**
- * Sets the value. If there are active observers, the value will be dispatched to them.
- * <p>
- * This method must be called from the main thread. If you need set a value from a background
- * thread, you can use {@link #postValue(Object)}
- *
- * @param value The new value
- */
- @MainThread
- protected void setValue(T value) {
- assertMainThread("setValue");
- mVersion++;
- mData = value;
- dispatchingValue(null);
- }
-
- /**
- * Returns the current value.
- * Note that calling this method on a background thread does not guarantee that the latest
- * value set will be received.
- *
- * @return the current value
- */
- @Nullable
- public T getValue() {
- Object data = mData;
- if (data != NOT_SET) {
- //noinspection unchecked
- return (T) data;
- }
- return null;
- }
-
- int getVersion() {
- return mVersion;
- }
-
- /**
- * Called when the number of active observers change to 1 from 0.
- * <p>
- * This callback can be used to know that this LiveData is being used thus should be kept
- * up to date.
- */
- protected void onActive() {
-
- }
-
- /**
- * Called when the number of active observers change from 1 to 0.
- * <p>
- * This does not mean that there are no observers left, there may still be observers but their
- * lifecycle states aren't {@link Lifecycle.State#STARTED} or {@link Lifecycle.State#RESUMED}
- * (like an Activity in the back stack).
- * <p>
- * You can check if there are observers via {@link #hasObservers()}.
- */
- protected void onInactive() {
-
- }
-
- /**
- * Returns true if this LiveData has observers.
- *
- * @return true if this LiveData has observers
- */
- @SuppressWarnings("WeakerAccess")
- public boolean hasObservers() {
- return mObservers.size() > 0;
- }
-
- /**
- * Returns true if this LiveData has active observers.
- *
- * @return true if this LiveData has active observers
- */
- @SuppressWarnings("WeakerAccess")
- public boolean hasActiveObservers() {
- return mActiveCount > 0;
- }
-
- class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
- @NonNull final LifecycleOwner mOwner;
-
- LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
- super(observer);
- mOwner = owner;
- }
-
- @Override
- boolean shouldBeActive() {
- return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
- }
-
- @Override
- public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
- if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
- removeObserver(mObserver);
- return;
- }
- activeStateChanged(shouldBeActive());
- }
-
- @Override
- boolean isAttachedTo(LifecycleOwner owner) {
- return mOwner == owner;
- }
-
- @Override
- void detachObserver() {
- mOwner.getLifecycle().removeObserver(this);
- }
- }
-
- private abstract class ObserverWrapper {
- final Observer<? super T> mObserver;
- boolean mActive;
- int mLastVersion = START_VERSION;
-
- ObserverWrapper(Observer<? super T> observer) {
- mObserver = observer;
- }
-
- abstract boolean shouldBeActive();
-
- boolean isAttachedTo(LifecycleOwner owner) {
- return false;
- }
-
- void detachObserver() {
- }
-
- void activeStateChanged(boolean newActive) {
- if (newActive == mActive) {
- return;
- }
- // immediately set active state, so we'd never dispatch anything to inactive
- // owner
- mActive = newActive;
- boolean wasInactive = LiveData.this.mActiveCount == 0;
- LiveData.this.mActiveCount += mActive ? 1 : -1;
- if (wasInactive && mActive) {
- onActive();
- }
- if (LiveData.this.mActiveCount == 0 && !mActive) {
- onInactive();
- }
- if (mActive) {
- dispatchingValue(this);
- }
- }
- }
-
- private class AlwaysActiveObserver extends ObserverWrapper {
-
- AlwaysActiveObserver(Observer<? super T> observer) {
- super(observer);
- }
-
- @Override
- boolean shouldBeActive() {
- return true;
- }
- }
-
- private static void assertMainThread(String methodName) {
- if (!ArchTaskExecutor.getInstance().isMainThread()) {
- throw new IllegalStateException("Cannot invoke " + methodName + " on a background"
- + " thread");
- }
- }
+public class LiveData<T> {
}
diff --git a/androidx/media/DataSourceDesc.java b/androidx/media/DataSourceDesc.java
new file mode 100644
index 00000000..f76f651c
--- /dev/null
+++ b/androidx/media/DataSourceDesc.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Preconditions;
+
+import java.io.FileDescriptor;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Structure for data source descriptor. Used by {@link MediaItem2}.
+ * <p>
+ * Users should use {@link Builder} to change {@link DataSourceDesc}.
+ *
+ * @see MediaItem2
+ */
+public final class DataSourceDesc {
+ /* No data source has been set yet */
+ public static final int TYPE_NONE = 0;
+ /* data source is type of MediaDataSource */
+ public static final int TYPE_CALLBACK = 1;
+ /* data source is type of FileDescriptor */
+ public static final int TYPE_FD = 2;
+ /* data source is type of Uri */
+ public static final int TYPE_URI = 3;
+
+ // intentionally less than long.MAX_VALUE.
+ // Declare this first to avoid 'illegal forward reference'.
+ private static final long LONG_MAX = 0x7ffffffffffffffL;
+
+ /**
+ * Used when a position is unknown.
+ *
+ * @see #getEndPosition()
+ */
+ public static final long POSITION_UNKNOWN = LONG_MAX;
+
+ /**
+ * Used when the length of file descriptor is unknown.
+ *
+ * @see #getFileDescriptorLength()
+ */
+ public static final long FD_LENGTH_UNKNOWN = LONG_MAX;
+
+ private int mType = TYPE_NONE;
+
+ private Media2DataSource mMedia2DataSource;
+
+ private FileDescriptor mFD;
+ private long mFDOffset = 0;
+ private long mFDLength = FD_LENGTH_UNKNOWN;
+
+ private Uri mUri;
+ private Map<String, String> mUriHeader;
+ private List<HttpCookie> mUriCookies;
+ private Context mUriContext;
+
+ private String mMediaId;
+ private long mStartPositionMs = 0;
+ private long mEndPositionMs = POSITION_UNKNOWN;
+
+ private DataSourceDesc() {
+ }
+
+ /**
+ * Return the media Id of data source.
+ * @return the media Id of data source
+ */
+ public @Nullable String getMediaId() {
+ return mMediaId;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will start.
+ * @return the position in milliseconds at which the playback will start
+ */
+ public long getStartPosition() {
+ return mStartPositionMs;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will end.
+ * {@link #POSITION_UNKNOWN} means ending at the end of source content.
+ * @return the position in milliseconds at which the playback will end
+ */
+ public long getEndPosition() {
+ return mEndPositionMs;
+ }
+
+ /**
+ * Return the type of data source.
+ * @return the type of data source
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Return the Media2DataSource of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
+ * @return the Media2DataSource of this data source
+ */
+ public @Nullable Media2DataSource getMedia2DataSource() {
+ return mMedia2DataSource;
+ }
+
+ /**
+ * Return the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+ * @return the FileDescriptor of this data source
+ */
+ public @Nullable FileDescriptor getFileDescriptor() {
+ return mFD;
+ }
+
+ /**
+ * Return the offset associated with the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD} and it has
+ * been set by the {@link Builder}.
+ * @return the offset associated with the FileDescriptor of this data source
+ */
+ public long getFileDescriptorOffset() {
+ return mFDOffset;
+ }
+
+ /**
+ * Return the content length associated with the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+ * {@link #FD_LENGTH_UNKNOWN} means same as the length of source content.
+ * @return the content length associated with the FileDescriptor of this data source
+ */
+ public long getFileDescriptorLength() {
+ return mFDLength;
+ }
+
+ /**
+ * Return the Uri of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri of this data source
+ */
+ public @Nullable Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Return the Uri headers of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri headers of this data source
+ */
+ public @Nullable Map<String, String> getUriHeaders() {
+ if (mUriHeader == null) {
+ return null;
+ }
+ return new HashMap<String, String>(mUriHeader);
+ }
+
+ /**
+ * Return the Uri cookies of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri cookies of this data source
+ */
+ public @Nullable List<HttpCookie> getUriCookies() {
+ if (mUriCookies == null) {
+ return null;
+ }
+ return new ArrayList<HttpCookie>(mUriCookies);
+ }
+
+ /**
+ * Return the Context used for resolving the Uri of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Context used for resolving the Uri of this data source
+ */
+ public @Nullable Context getUriContext() {
+ return mUriContext;
+ }
+
+ /**
+ * Builder class for {@link DataSourceDesc} objects.
+ */
+ public static class Builder {
+ private int mType = TYPE_NONE;
+
+ private Media2DataSource mMedia2DataSource;
+
+ private FileDescriptor mFD;
+ private long mFDOffset = 0;
+ private long mFDLength = FD_LENGTH_UNKNOWN;
+
+ private Uri mUri;
+ private Map<String, String> mUriHeader;
+ private List<HttpCookie> mUriCookies;
+ private Context mUriContext;
+
+ private String mMediaId;
+ private long mStartPositionMs = 0;
+ private long mEndPositionMs = POSITION_UNKNOWN;
+
+ /**
+ * Constructs a new Builder with the defaults.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Constructs a new Builder from a given {@link DataSourceDesc} instance
+ * @param dsd the {@link DataSourceDesc} object whose data will be reused
+ * in the new Builder.
+ */
+ public Builder(@NonNull DataSourceDesc dsd) {
+ mType = dsd.mType;
+ mMedia2DataSource = dsd.mMedia2DataSource;
+ mFD = dsd.mFD;
+ mFDOffset = dsd.mFDOffset;
+ mFDLength = dsd.mFDLength;
+ mUri = dsd.mUri;
+ mUriHeader = dsd.mUriHeader;
+ mUriCookies = dsd.mUriCookies;
+ mUriContext = dsd.mUriContext;
+
+ mMediaId = dsd.mMediaId;
+ mStartPositionMs = dsd.mStartPositionMs;
+ mEndPositionMs = dsd.mEndPositionMs;
+ }
+
+ /**
+ * Combines all of the fields that have been set and return a new
+ * {@link DataSourceDesc} object. <code>IllegalStateException</code> will be
+ * thrown if there is conflict between fields.
+ *
+ * @return a new {@link DataSourceDesc} object
+ */
+ public @NonNull DataSourceDesc build() {
+ if (mType != TYPE_CALLBACK
+ && mType != TYPE_FD
+ && mType != TYPE_URI) {
+ throw new IllegalStateException("Illegal type: " + mType);
+ }
+ if (mStartPositionMs > mEndPositionMs) {
+ throw new IllegalStateException("Illegal start/end position: "
+ + mStartPositionMs + " : " + mEndPositionMs);
+ }
+
+ DataSourceDesc dsd = new DataSourceDesc();
+ dsd.mType = mType;
+ dsd.mMedia2DataSource = mMedia2DataSource;
+ dsd.mFD = mFD;
+ dsd.mFDOffset = mFDOffset;
+ dsd.mFDLength = mFDLength;
+ dsd.mUri = mUri;
+ dsd.mUriHeader = mUriHeader;
+ dsd.mUriCookies = mUriCookies;
+ dsd.mUriContext = mUriContext;
+
+ dsd.mMediaId = mMediaId;
+ dsd.mStartPositionMs = mStartPositionMs;
+ dsd.mEndPositionMs = mEndPositionMs;
+
+ return dsd;
+ }
+
+ /**
+ * Sets the media Id of this data source.
+ *
+ * @param mediaId the media Id of this data source
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setMediaId(String mediaId) {
+ mMediaId = mediaId;
+ return this;
+ }
+
+ /**
+ * Sets the start position in milliseconds at which the playback will start.
+ * Any negative number is treated as 0.
+ *
+ * @param position the start position in milliseconds at which the playback will start
+ * @return the same Builder instance.
+ *
+ */
+ public @NonNull Builder setStartPosition(long position) {
+ if (position < 0) {
+ position = 0;
+ }
+ mStartPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the end position in milliseconds at which the playback will end.
+ * Any negative number is treated as maximum length of the data source.
+ *
+ * @param position the end position in milliseconds at which the playback will end
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setEndPosition(long position) {
+ if (position < 0) {
+ position = POSITION_UNKNOWN;
+ }
+ mEndPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the data source (Media2DataSource) to use.
+ *
+ * @param m2ds the Media2DataSource for the media you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if m2ds is null.
+ */
+ public @NonNull Builder setDataSource(@NonNull Media2DataSource m2ds) {
+ Preconditions.checkNotNull(m2ds);
+ resetDataSource();
+ mType = TYPE_CALLBACK;
+ mMedia2DataSource = m2ds;
+ return this;
+ }
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor after the source has been used.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if fd is null.
+ */
+ public @NonNull Builder setDataSource(@NonNull FileDescriptor fd) {
+ Preconditions.checkNotNull(fd);
+ resetDataSource();
+ mType = TYPE_FD;
+ mFD = fd;
+ return this;
+ }
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor after the source has been used.
+ *
+ * Any negative number for offset is treated as 0.
+ * Any negative number for length is treated as maximum length of the data source.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @return the same Builder instance.
+ * @throws NullPointerException if fd is null.
+ */
+ public @NonNull Builder setDataSource(@NonNull FileDescriptor fd, long offset,
+ long length) {
+ Preconditions.checkNotNull(fd);
+ if (offset < 0) {
+ offset = 0;
+ }
+ if (length < 0) {
+ length = FD_LENGTH_UNKNOWN;
+ }
+ resetDataSource();
+ mType = TYPE_FD;
+ mFD = fd;
+ mFDOffset = offset;
+ mFDLength = length;
+ return this;
+ }
+
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if context or uri is null.
+ */
+ public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
+ Preconditions.checkNotNull(context, "context cannot be null");
+ Preconditions.checkNotNull(uri, "uri cannot be null");
+ resetDataSource();
+ mType = TYPE_URI;
+ mUri = uri;
+ mUriContext = context;
+ return this;
+ }
+
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * To provide cookies for the subsequent HTTP requests, you can install your own default
+ * cookie handler and use other variants of setDataSource APIs instead.
+ *
+ * <p><strong>Note</strong> that the cross domain redirection is allowed by default,
+ * but that can be changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
+ * disallow or allow cross domain redirection.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param headers the headers to be sent together with the request for the data
+ * The headers must not include cookies. Instead, use the cookies param.
+ * @param cookies the cookies to be sent together with the request
+ * @return the same Builder instance.
+ * @throws NullPointerException if context or uri is null.
+ * @throws IllegalArgumentException if the cookie handler is not of CookieManager type
+ * when cookies are provided.
+ */
+ public @NonNull Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
+ Preconditions.checkNotNull(context, "context cannot be null");
+ Preconditions.checkNotNull(uri);
+ if (cookies != null) {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
+ throw new IllegalArgumentException(
+ "The cookie handler has to be of CookieManager type "
+ + "when cookies are provided.");
+ }
+ }
+
+ resetDataSource();
+ mType = TYPE_URI;
+ mUri = uri;
+ if (headers != null) {
+ mUriHeader = new HashMap<String, String>(headers);
+ }
+ if (cookies != null) {
+ mUriCookies = new ArrayList<HttpCookie>(cookies);
+ }
+ mUriContext = context;
+ return this;
+ }
+
+ private void resetDataSource() {
+ mType = TYPE_NONE;
+ mMedia2DataSource = null;
+ mFD = null;
+ mFDOffset = 0;
+ mFDLength = FD_LENGTH_UNKNOWN;
+ mUri = null;
+ mUriHeader = null;
+ mUriCookies = null;
+ mUriContext = null;
+ }
+ }
+} \ No newline at end of file
diff --git a/androidx/media/Media2DataSource.java b/androidx/media/Media2DataSource.java
new file mode 100644
index 00000000..49902595
--- /dev/null
+++ b/androidx/media/Media2DataSource.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import androidx.annotation.RestrictTo;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * @hide
+ * For supplying media data to the framework. Implement this if your app has
+ * special requirements for the way media data is obtained.
+ *
+ * <p class="note">Methods of this interface may be called on multiple different
+ * threads. There will be a thread synchronization point between each call to ensure that
+ * modifications to the state of your Media2DataSource are visible to future calls. This means
+ * you don't need to do your own synchronization unless you're modifying the
+ * Media2DataSource from another thread while it's being used by the framework.</p>
+ *
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class Media2DataSource implements Closeable {
+ /**
+ * Called to request data from the given position.
+ *
+ * Implementations should should write up to {@code size} bytes into
+ * {@code buffer}, and return the number of bytes written.
+ *
+ * Return {@code 0} if size is zero (thus no bytes are read).
+ *
+ * Return {@code -1} to indicate that end of stream is reached.
+ *
+ * @param position the position in the data source to read from.
+ * @param buffer the buffer to read the data into.
+ * @param offset the offset within buffer to read the data into.
+ * @param size the number of bytes to read.
+ * @throws IOException on fatal errors.
+ * @return the number of bytes read, or -1 if there was an error.
+ */
+ public abstract int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException;
+
+ /**
+ * Called to get the size of the data source.
+ *
+ * @throws IOException on fatal errors
+ * @return the size of data source in bytes, or -1 if the size is unknown.
+ */
+ public abstract long getSize() throws IOException;
+}
diff --git a/androidx/media/MediaBrowser2.java b/androidx/media/MediaBrowser2.java
new file mode 100644
index 00000000..6ef7fcf7
--- /dev/null
+++ b/androidx/media/MediaBrowser2.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.ItemCallback;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaLibraryService2.MediaLibrarySession;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ * Browses media content offered by a {@link MediaLibraryService2}.
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class MediaBrowser2 extends MediaController2 {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final String EXTRA_ITEM_COUNT = "android.media.browse.extra.ITEM_COUNT";
+
+ /**
+ * Key for Bundle version of {@link MediaSession2.ControllerInfo}.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final String EXTRA_TARGET = "android.media.browse.extra.TARGET";
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final HashMap<Bundle, MediaBrowserCompat> mBrowserCompats = new HashMap<>();
+ @GuardedBy("mLock")
+ private final HashMap<String, List<SubscribeCallback>> mSubscribeCallbacks = new HashMap<>();
+
+ /**
+ * Callback to listen events from {@link MediaLibraryService2}.
+ */
+ public static class BrowserCallback extends MediaController2.ControllerCallback {
+ /**
+ * Called with the result of {@link #getLibraryRoot(Bundle)}.
+ * <p>
+ * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the library root isn't
+ * available.
+ *
+ * @param browser the browser for this event
+ * @param rootHints rootHints that you previously requested.
+ * @param rootMediaId media id of the library root. Can be {@code null}
+ * @param rootExtra extra of the library root. Can be {@code null}
+ */
+ public void onGetLibraryRootDone(@NonNull MediaBrowser2 browser, @Nullable Bundle rootHints,
+ @Nullable String rootMediaId, @Nullable Bundle rootExtra) { }
+
+ /**
+ * Called when there's change in the parent's children.
+ * <p>
+ * This API is called when the library service called
+ * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)} or
+ * {@link MediaLibrarySession#notifyChildrenChanged(String, int, Bundle)} for the parent.
+ *
+ * @param browser the browser for this event
+ * @param parentId parent id that you've specified with {@link #subscribe(String, Bundle)}
+ * @param itemCount number of children
+ * @param extras extra bundle from the library service. Can be differ from extras that
+ * you've specified with {@link #subscribe(String, Bundle)}.
+ */
+ public void onChildrenChanged(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+ int itemCount, @Nullable Bundle extras) { }
+
+ /**
+ * Called when the list of items has been returned by the library service for the previous
+ * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
+ *
+ * @param browser the browser for this event
+ * @param parentId parent id
+ * @param page page number that you've specified with
+ * {@link #getChildren(String, int, int, Bundle)}
+ * @param pageSize page size that you've specified with
+ * {@link #getChildren(String, int, int, Bundle)}
+ * @param result result. Can be {@code null}
+ * @param extras extra bundle from the library service
+ */
+ public void onGetChildrenDone(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+ int page, int pageSize, @Nullable List<MediaItem2> result,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when the item has been returned by the library service for the previous
+ * {@link MediaBrowser2#getItem(String)} call.
+ * <p>
+ * Result can be null if there had been error.
+ *
+ * @param browser the browser for this event
+ * @param mediaId media id
+ * @param result result. Can be {@code null}
+ */
+ public void onGetItemDone(@NonNull MediaBrowser2 browser, @NonNull String mediaId,
+ @Nullable MediaItem2 result) { }
+
+ /**
+ * Called when there's change in the search result requested by the previous
+ * {@link MediaBrowser2#search(String, Bundle)}.
+ *
+ * @param browser the browser for this event
+ * @param query search query that you've specified with {@link #search(String, Bundle)}
+ * @param itemCount The item count for the search result
+ * @param extras extra bundle from the library service
+ */
+ public void onSearchResultChanged(@NonNull MediaBrowser2 browser, @NonNull String query,
+ int itemCount, @Nullable Bundle extras) { }
+
+ /**
+ * Called when the search result has been returned by the library service for the previous
+ * {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
+ * <p>
+ * Result can be null if there had been error.
+ *
+ * @param browser the browser for this event
+ * @param query search query that you've specified with
+ * {@link #getSearchResult(String, int, int, Bundle)}
+ * @param page page number that you've specified with
+ * {@link #getSearchResult(String, int, int, Bundle)}
+ * @param pageSize page size that you've specified with
+ * {@link #getSearchResult(String, int, int, Bundle)}
+ * @param result result. Can be {@code null}.
+ * @param extras extra bundle from the library service
+ */
+ public void onGetSearchResultDone(@NonNull MediaBrowser2 browser, @NonNull String query,
+ int page, int pageSize, @Nullable List<MediaItem2> result,
+ @Nullable Bundle extras) { }
+ }
+
+ public MediaBrowser2(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull /*@CallbackExecutor*/ Executor executor, @NonNull BrowserCallback callback) {
+ super(context, token, executor, callback);
+ }
+
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ for (MediaBrowserCompat browser : mBrowserCompats.values()) {
+ browser.disconnect();
+ }
+ mBrowserCompats.clear();
+ // TODO: Ensure that ControllerCallback#onDisconnected() is called by super.close().
+ super.close();
+ }
+ }
+
+ /**
+ * Get the library root. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)}.
+ *
+ * @param extras extras for getting root
+ * @see BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)
+ */
+ public void getLibraryRoot(@Nullable final Bundle extras) {
+ final MediaBrowserCompat browser = getBrowserCompat(extras);
+ if (browser != null) {
+ // Already connected with the given extras.
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onGetLibraryRootDone(MediaBrowser2.this, extras,
+ browser.getRoot(), browser.getExtras());
+ }
+ });
+ } else {
+ MediaBrowserCompat newBrowser = new MediaBrowserCompat(getContext(),
+ getSessionToken().getComponentName(), new GetLibraryRootCallback(extras),
+ extras);
+ newBrowser.connect();
+ synchronized (mLock) {
+ mBrowserCompats.put(extras, newBrowser);
+ }
+ }
+ }
+
+ /**
+ * Subscribe to a parent id for the change in its children. When there's a change,
+ * {@link BrowserCallback#onChildrenChanged(MediaBrowser2, String, int, Bundle)} will be called
+ * with the bundle that you've specified. You should call
+ * {@link #getChildren(String, int, int, Bundle)} to get the actual contents for the parent.
+ *
+ * @param parentId parent id
+ * @param extras extra bundle
+ */
+ public void subscribe(@NonNull String parentId, @Nullable Bundle extras) {
+ if (parentId == null) {
+ throw new IllegalArgumentException("parentId shouldn't be null");
+ }
+ // TODO: Document this behavior
+ Bundle option;
+ if (extras != null && (extras.containsKey(MediaBrowserCompat.EXTRA_PAGE)
+ || extras.containsKey(MediaBrowserCompat.EXTRA_PAGE_SIZE))) {
+ option = new Bundle(extras);
+ option.remove(MediaBrowserCompat.EXTRA_PAGE);
+ option.remove(MediaBrowserCompat.EXTRA_PAGE_SIZE);
+ } else {
+ option = extras;
+ }
+ SubscribeCallback callback = new SubscribeCallback();
+ synchronized (mLock) {
+ List<SubscribeCallback> list = mSubscribeCallbacks.get(parentId);
+ if (list == null) {
+ list = new ArrayList<>();
+ mSubscribeCallbacks.put(parentId, list);
+ }
+ list.add(callback);
+ }
+ // TODO: Revisit using default browser is OK. Here's my concern.
+ // Assume that MediaBrowser2 is connected with the MediaBrowserServiceCompat.
+ // Since MediaBrowserServiceCompat can call MediaBrowserServiceCompat#
+ // getBrowserRootHints(), the service may refuse calls from MediaBrowser2
+ getBrowserCompat().subscribe(parentId, option, callback);
+ }
+
+ /**
+ * Unsubscribe for changes to the children of the parent, which was previously subscribed with
+ * {@link #subscribe(String, Bundle)}.
+ * <p>
+ * This unsubscribes all previous subscription with the parent id, regardless of the extra
+ * that was previously sent to the library service.
+ *
+ * @param parentId parent id
+ */
+ public void unsubscribe(@NonNull String parentId) {
+ if (parentId == null) {
+ throw new IllegalArgumentException("parentId shouldn't be null");
+ }
+ // Note: don't use MediaBrowserCompat#unsubscribe(String) here, to keep the subscription
+ // callback for getChildren.
+ synchronized (mLock) {
+ List<SubscribeCallback> list = mSubscribeCallbacks.get(parentId);
+ if (list == null) {
+ return;
+ }
+ MediaBrowserCompat browser = getBrowserCompat();
+ for (int i = 0; i < list.size(); i++) {
+ browser.unsubscribe(parentId, list.get(i));
+ }
+ }
+ }
+
+ /**
+ * Get list of children under the parent. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onGetChildrenDone(MediaBrowser2, String, int, int, List, Bundle)}.
+ *
+ * @param parentId parent id for getting the children.
+ * @param page page number to get the result. Starts from {@code 1}
+ * @param pageSize page size. Should be greater or equal to {@code 1}
+ * @param extras extra bundle
+ */
+ public void getChildren(@NonNull String parentId, int page, int pageSize,
+ @Nullable Bundle extras) {
+ if (parentId == null) {
+ throw new IllegalArgumentException("parentId shouldn't be null");
+ }
+ if (page < 1 || pageSize < 1) {
+ throw new IllegalArgumentException("Neither page nor pageSize should be less than 1");
+ }
+ Bundle options = new Bundle(extras);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+ options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+ // TODO: Revisit using default browser is OK. See TODO in subscribe
+ getBrowserCompat().subscribe(parentId, options,
+ new GetChildrenCallback(parentId, page, pageSize));
+ }
+
+ /**
+ * Get the media item with the given media id. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onGetItemDone(MediaBrowser2, String, MediaItem2)}.
+ *
+ * @param mediaId media id for specifying the item
+ */
+ public void getItem(@NonNull final String mediaId) {
+ // TODO: Revisit using default browser is OK. See TODO in subscribe
+ getBrowserCompat().getItem(mediaId, new ItemCallback() {
+ @Override
+ public void onItemLoaded(final MediaItem item) {
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onGetItemDone(MediaBrowser2.this, mediaId,
+ MediaUtils2.createMediaItem2(item));
+ }
+ });
+ }
+
+ @Override
+ public void onError(String itemId) {
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onGetItemDone(MediaBrowser2.this, mediaId, null);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Send a search request to the library service. When the search result is changed,
+ * {@link BrowserCallback#onSearchResultChanged(MediaBrowser2, String, int, Bundle)} will be
+ * called. You should call {@link #getSearchResult(String, int, int, Bundle)} to get the actual
+ * search result.
+ *
+ * @param query search query. Should not be an empty string.
+ * @param extras extra bundle
+ */
+ public void search(@NonNull String query, @Nullable Bundle extras) {
+ // TODO: Implement
+ }
+
+ /**
+ * Get the search result from lhe library service. Result would be sent back asynchronously with
+ * the
+ * {@link BrowserCallback#onGetSearchResultDone(MediaBrowser2, String, int, int, List, Bundle)}.
+ *
+ * @param query search query that you've specified with {@link #search(String, Bundle)}
+ * @param page page number to get search result. Starts from {@code 1}
+ * @param pageSize page size. Should be greater or equal to {@code 1}
+ * @param extras extra bundle
+ */
+ public void getSearchResult(@NonNull String query, int page, int pageSize,
+ @Nullable Bundle extras) {
+ // TODO: Implement
+ }
+
+ @Override
+ BrowserCallback getCallback() {
+ return (BrowserCallback) super.getCallback();
+ }
+
+ private MediaBrowserCompat getBrowserCompat(Bundle extras) {
+ synchronized (mLock) {
+ return mBrowserCompats.get(extras);
+ }
+ }
+
+ private class GetLibraryRootCallback extends MediaBrowserCompat.ConnectionCallback {
+ private final Bundle mExtras;
+
+ GetLibraryRootCallback(Bundle extras) {
+ super();
+ mExtras = extras;
+ }
+
+ @Override
+ public void onConnected() {
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ MediaBrowserCompat browser;
+ synchronized (mLock) {
+ browser = mBrowserCompats.get(mExtras);
+ }
+ if (browser == null) {
+ // Shouldn't be happen.
+ return;
+ }
+ getCallback().onGetLibraryRootDone(MediaBrowser2.this,
+ mExtras, browser.getRoot(), browser.getExtras());
+ }
+ });
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ close();
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ close();
+ }
+ }
+
+ private class SubscribeCallback extends SubscriptionCallback {
+ @Override
+ public void onError(String parentId) {
+ onChildrenLoaded(parentId, null, null);
+ }
+
+ @Override
+ public void onError(String parentId, Bundle options) {
+ onChildrenLoaded(parentId, null, options);
+ }
+
+ @Override
+ public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+ onChildrenLoaded(parentId, children, null);
+ }
+
+ @Override
+ public void onChildrenLoaded(final String parentId, List<MediaItem> children,
+ final Bundle options) {
+ final int itemCount;
+ if (options != null && options.containsKey(EXTRA_ITEM_COUNT)) {
+ itemCount = options.getInt(EXTRA_ITEM_COUNT);
+ } else if (children != null) {
+ itemCount = children.size();
+ } else {
+ // Currently no way to tell failures in MediaBrowser2#subscribe().
+ return;
+ }
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onChildrenChanged(MediaBrowser2.this, parentId, itemCount,
+ options);
+ }
+ });
+ }
+ }
+
+ private class GetChildrenCallback extends SubscriptionCallback {
+ private final String mParentId;
+ private final int mPage;
+ private final int mPageSize;
+
+ GetChildrenCallback(String parentId, int page, int pageSize) {
+ super();
+ mParentId = parentId;
+ mPage = page;
+ mPageSize = pageSize;
+ }
+
+ @Override
+ public void onError(String parentId) {
+ onChildrenLoaded(parentId, null, null);
+ }
+
+ @Override
+ public void onError(String parentId, Bundle options) {
+ onChildrenLoaded(parentId, null, options);
+ }
+
+ @Override
+ public void onChildrenLoaded(String parentId, List<MediaItem> children) {
+ onChildrenLoaded(parentId, children, null);
+ }
+
+ @Override
+ public void onChildrenLoaded(final String parentId, List<MediaItem> children,
+ final Bundle options) {
+ final List<MediaItem2> items;
+ if (children == null) {
+ items = null;
+ } else {
+ items = new ArrayList<>();
+ for (int i = 0; i < children.size(); i++) {
+ items.add(MediaUtils2.createMediaItem2(children.get(i)));
+ }
+ }
+ getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ getCallback().onGetChildrenDone(MediaBrowser2.this, parentId, mPage, mPageSize,
+ items, options);
+ getBrowserCompat().unsubscribe(mParentId, GetChildrenCallback.this);
+ }
+ });
+ }
+ }
+}
diff --git a/androidx/media/MediaBrowser2Test.java b/androidx/media/MediaBrowser2Test.java
new file mode 100644
index 00000000..4b759863
--- /dev/null
+++ b/androidx/media/MediaBrowser2Test.java
@@ -0,0 +1,703 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.MockMediaLibraryService2.EXTRAS;
+import static androidx.media.MockMediaLibraryService2.ROOT_ID;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import static org.junit.Assert.assertNotEquals;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaBrowser2.BrowserCallback;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaLibraryService2.MediaLibrarySession;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import junit.framework.Assert;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaBrowser2}.
+ * <p>
+ * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
+ * {@link MediaController2} works cleanly.
+ */
+// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+public class MediaBrowser2Test extends MediaController2Test {
+ private static final String TAG = "MediaBrowser2Test";
+
+ @Override
+ TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
+ @Nullable ControllerCallback callback) throws InterruptedException {
+ final BrowserCallback browserCallback =
+ callback != null ? (BrowserCallback) callback : new BrowserCallback() {};
+ final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ // Create controller on the test handler, for changing MediaBrowserCompat's Handler
+ // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
+ // and commands wouldn't be run if tests codes waits on the test handler.
+ controller.set(new TestMediaBrowser(
+ mContext, token, new TestBrowserCallback(browserCallback)));
+ }
+ });
+ return controller.get();
+ }
+
+ /**
+ * Test if the {@link TestBrowserCallback} wraps the callback proxy without missing any method.
+ */
+ @Test
+ public void testTestBrowserCallback() {
+ prepareLooper();
+ Method[] methods = TestBrowserCallback.class.getMethods();
+ assertNotNull(methods);
+ for (int i = 0; i < methods.length; i++) {
+ // For any methods in the controller callback, TestControllerCallback should have
+ // overriden the method and call matching API in the callback proxy.
+ assertNotEquals("TestBrowserCallback should override " + methods[i]
+ + " and call callback proxy",
+ BrowserCallback.class, methods[i].getDeclaringClass());
+ }
+ }
+
+ @Test
+ public void testGetLibraryRoot() throws InterruptedException {
+ prepareLooper();
+ final Bundle param = new Bundle();
+ param.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetLibraryRootDone(MediaBrowser2 browser,
+ Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+ assertTrue(TestUtils.equals(param, rootHints));
+ assertEquals(ROOT_ID, rootMediaId);
+ assertTrue(TestUtils.equals(EXTRAS, rootExtra));
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser =
+ (MediaBrowser2) createController(token, true, callback);
+ browser.getLibraryRoot(param);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetItem() throws InterruptedException {
+ prepareLooper();
+ final String mediaId = MockMediaLibraryService2.MEDIA_ID_GET_ITEM;
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
+ assertEquals(mediaId, mediaIdOut);
+ assertNotNull(result);
+ assertEquals(mediaId, result.getMediaId());
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getItem(mediaId);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetItemNullResult() throws InterruptedException {
+ prepareLooper();
+ final String mediaId = "random_media_id";
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetItemDone(MediaBrowser2 browser, String mediaIdOut, MediaItem2 result) {
+ assertEquals(mediaId, mediaIdOut);
+ assertNull(result);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getItem(mediaId);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetChildren() throws InterruptedException {
+ prepareLooper();
+ final String parentId = MockMediaLibraryService2.PARENT_ID;
+ final int page = 4;
+ final int pageSize = 10;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut, int pageOut,
+ int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertEquals(parentId, parentIdOut);
+ assertEquals(page, pageOut);
+ assertEquals(pageSize, pageSizeOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertNotNull(result);
+
+ int fromIndex = (page - 1) * pageSize;
+ int toIndex = Math.min(page * pageSize, MockMediaLibraryService2.CHILDREN_COUNT);
+
+ // Compare the given results with originals.
+ for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
+ int relativeIndex = originalIndex - fromIndex;
+ Assert.assertEquals(
+ MockMediaLibraryService2.GET_CHILDREN_RESULT.get(originalIndex)
+ .getMediaId(),
+ result.get(relativeIndex).getMediaId());
+ }
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getChildren(parentId, page, pageSize, extras);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetChildrenEmptyResult() throws InterruptedException {
+ prepareLooper();
+ final String parentId = MockMediaLibraryService2.PARENT_ID_NO_CHILDREN;
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
+ int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertNotNull(result);
+ assertEquals(0, result.size());
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getChildren(parentId, 1, 1, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testGetChildrenNullResult() throws InterruptedException {
+ prepareLooper();
+ final String parentId = MockMediaLibraryService2.PARENT_ID_ERROR;
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentIdOut,
+ int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertNull(result);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.getChildren(parentId, 1, 1, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Ignore
+ @Test
+ public void testSearch() throws InterruptedException {
+ prepareLooper();
+ final String query = MockMediaLibraryService2.SEARCH_QUERY;
+ final int page = 4;
+ final int pageSize = 10;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latchForSearch = new CountDownLatch(1);
+ final CountDownLatch latchForGetSearchResult = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onSearchResultChanged(MediaBrowser2 browser,
+ String queryOut, int itemCount, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
+ latchForSearch.countDown();
+ }
+
+ @Override
+ public void onGetSearchResultDone(MediaBrowser2 browser, String queryOut,
+ int pageOut, int pageSizeOut, List<MediaItem2> result, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertEquals(page, pageOut);
+ assertEquals(pageSize, pageSizeOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertNotNull(result);
+
+ int fromIndex = (page - 1) * pageSize;
+ int toIndex = Math.min(
+ page * pageSize, MockMediaLibraryService2.SEARCH_RESULT_COUNT);
+
+ // Compare the given results with originals.
+ for (int originalIndex = fromIndex; originalIndex < toIndex; originalIndex++) {
+ int relativeIndex = originalIndex - fromIndex;
+ Assert.assertEquals(
+ MockMediaLibraryService2.SEARCH_RESULT.get(originalIndex).getMediaId(),
+ result.get(relativeIndex).getMediaId());
+ }
+ latchForGetSearchResult.countDown();
+ }
+ };
+
+ // Request the search.
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.search(query, extras);
+ assertTrue(latchForSearch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+
+ // Get the search result.
+ browser.getSearchResult(query, page, pageSize, extras);
+ assertTrue(latchForGetSearchResult.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSearchTakesTime() throws InterruptedException {
+ prepareLooper();
+ final String query = MockMediaLibraryService2.SEARCH_QUERY_TAKES_TIME;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onSearchResultChanged(
+ MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertEquals(MockMediaLibraryService2.SEARCH_RESULT_COUNT, itemCount);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.search(query, extras);
+ assertTrue(latch.await(
+ MockMediaLibraryService2.SEARCH_TIME_IN_MS + WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSearchEmptyResult() throws InterruptedException {
+ prepareLooper();
+ final String query = MockMediaLibraryService2.SEARCH_QUERY_EMPTY_RESULT;
+ final Bundle extras = new Bundle();
+ extras.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final BrowserCallback callback = new BrowserCallback() {
+ @Override
+ public void onSearchResultChanged(
+ MediaBrowser2 browser, String queryOut, int itemCount, Bundle extrasOut) {
+ assertEquals(query, queryOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ assertEquals(0, itemCount);
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token, true, callback);
+ browser.search(query, extras);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSubscribe() throws InterruptedException {
+ prepareLooper();
+ final String testParentId = "testSubscribeId";
+ final Bundle testExtras = new Bundle();
+ testExtras.putString(testParentId, testParentId);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onSubscribe(@NonNull MediaLibraryService2.MediaLibrarySession session,
+ @NonNull MediaSession2.ControllerInfo info, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == info.getUid()) {
+ assertEquals(testParentId, parentId);
+ assertTrue(TestUtils.equals(testExtras, extras));
+ latch.countDown();
+ }
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(callback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token);
+ browser.subscribe(testParentId, testExtras);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Ignore
+ @Test
+ public void testUnsubscribe() throws InterruptedException {
+ prepareLooper();
+ final String testParentId = "testUnsubscribeId";
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback callback = new MediaLibrarySessionCallback() {
+ @Override
+ public void onUnsubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo info, @NonNull String parentId) {
+ if (Process.myUid() == info.getUid()) {
+ assertEquals(testParentId, parentId);
+ latch.countDown();
+ }
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(callback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser = (MediaBrowser2) createController(token);
+ browser.unsubscribe(testParentId);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testBrowserCallback_notifyChildrenChanged() throws InterruptedException {
+ prepareLooper();
+ // TODO(jaewan): Add test for the notifyChildrenChanged itself.
+ final String testParentId1 = "testBrowserCallback_notifyChildrenChanged_unexpectedParent";
+ final String testParentId2 = "testBrowserCallback_notifyChildrenChanged";
+ final int testChildrenCount = 101;
+ final Bundle testExtras = new Bundle();
+ testExtras.putString(testParentId1, testParentId1);
+
+ final CountDownLatch latch = new CountDownLatch(3);
+ final MediaLibrarySessionCallback sessionCallback =
+ new MediaLibrarySessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ assertTrue(session instanceof MediaLibrarySession);
+ if (mSession != null) {
+ mSession.close();
+ }
+ mSession = session;
+ // Shouldn't trigger onChildrenChanged() for the browser, because it
+ // hasn't subscribed.
+ ((MediaLibrarySession) session).notifyChildrenChanged(
+ testParentId1, testChildrenCount, null);
+ ((MediaLibrarySession) session).notifyChildrenChanged(
+ controller, testParentId1, testChildrenCount, null);
+ }
+ return super.onConnect(session, controller);
+ }
+
+ @Override
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo info, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ if (Process.myUid() == info.getUid()) {
+ session.notifyChildrenChanged(testParentId2, testChildrenCount, null);
+ session.notifyChildrenChanged(info, testParentId2, testChildrenCount,
+ testExtras);
+ }
+ }
+ };
+ final BrowserCallback controllerCallbackProxy =
+ new BrowserCallback() {
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId,
+ int itemCount, Bundle extras) {
+ switch ((int) latch.getCount()) {
+ case 3:
+ assertEquals(testParentId2, parentId);
+ assertEquals(testChildrenCount, itemCount);
+ assertNull(extras);
+ latch.countDown();
+ break;
+ case 2:
+ assertEquals(testParentId2, parentId);
+ assertEquals(testChildrenCount, itemCount);
+ assertTrue(TestUtils.equals(testExtras, extras));
+ latch.countDown();
+ break;
+ default:
+ // Unexpected call.
+ fail();
+ }
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ final MediaBrowser2 browser = (MediaBrowser2) createController(
+ token, true, controllerCallbackProxy);
+ assertTrue(mSession instanceof MediaLibrarySession);
+ browser.subscribe(testParentId2, null);
+ // This ensures that onChildrenChanged() is only called for the expected reasons.
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ public static class TestBrowserCallback extends BrowserCallback
+ implements TestControllerCallbackInterface {
+ private final ControllerCallback mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+ @GuardedBy("this")
+ private Runnable mOnCustomCommandRunnable;
+
+ TestBrowserCallback(ControllerCallback callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected(MediaController2 controller) {
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller,
+ MediaController2.PlaybackInfo info) {
+ mCallbackProxy.onPlaybackInfoChanged(controller, info);
+ }
+
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+ synchronized (this) {
+ if (mOnCustomCommandRunnable != null) {
+ mOnCustomCommandRunnable.run();
+ }
+ }
+ }
+
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+ mCallbackProxy.onCustomLayoutChanged(controller, layout);
+ }
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+ }
+
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ mCallbackProxy.onPlayerStateChanged(controller, state);
+ }
+
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ mCallbackProxy.onSeekCompleted(controller, position);
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+ mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+ }
+
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ mCallbackProxy.onBufferingStateChanged(controller, item, state);
+ }
+
+ @Override
+ public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+ mCallbackProxy.onError(controller, errorCode, extras);
+ }
+
+ @Override
+ public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+ mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> list, MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+ }
+
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+ }
+
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+ }
+
+ @Override
+ public void onGetLibraryRootDone(MediaBrowser2 browser, Bundle rootHints,
+ String rootMediaId, Bundle rootExtra) {
+ super.onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onGetLibraryRootDone(browser, rootHints, rootMediaId, rootExtra);
+ }
+ }
+
+ @Override
+ public void onGetItemDone(MediaBrowser2 browser, String mediaId, MediaItem2 result) {
+ super.onGetItemDone(browser, mediaId, result);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy).onGetItemDone(browser, mediaId, result);
+ }
+ }
+
+ @Override
+ public void onGetChildrenDone(MediaBrowser2 browser, String parentId, int page,
+ int pageSize, List<MediaItem2> result, Bundle extras) {
+ super.onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onGetChildrenDone(browser, parentId, page, pageSize, result, extras);
+ }
+ }
+
+ @Override
+ public void onSearchResultChanged(MediaBrowser2 browser, String query, int itemCount,
+ Bundle extras) {
+ super.onSearchResultChanged(browser, query, itemCount, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onSearchResultChanged(browser, query, itemCount, extras);
+ }
+ }
+
+ @Override
+ public void onGetSearchResultDone(MediaBrowser2 browser, String query, int page,
+ int pageSize, List<MediaItem2> result, Bundle extras) {
+ super.onGetSearchResultDone(browser, query, page, pageSize, result, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onGetSearchResultDone(browser, query, page, pageSize, result, extras);
+ }
+ }
+
+ @Override
+ public void onChildrenChanged(MediaBrowser2 browser, String parentId, int itemCount,
+ Bundle extras) {
+ super.onChildrenChanged(browser, parentId, itemCount, extras);
+ if (mCallbackProxy instanceof BrowserCallback) {
+ ((BrowserCallback) mCallbackProxy)
+ .onChildrenChanged(browser, parentId, itemCount, extras);
+ }
+ }
+
+ @Override
+ public void setRunnableForOnCustomCommand(Runnable runnable) {
+ synchronized (this) {
+ mOnCustomCommandRunnable = runnable;
+ }
+ }
+ }
+
+ public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
+ private final BrowserCallback mCallback;
+
+ public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, sHandlerExecutor, (BrowserCallback) callback);
+ mCallback = (BrowserCallback) callback;
+ }
+
+ @Override
+ public BrowserCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/androidx/media/MediaBrowserServiceCompat.java b/androidx/media/MediaBrowserServiceCompat.java
index 8f18cfc0..430913e6 100644
--- a/androidx/media/MediaBrowserServiceCompat.java
+++ b/androidx/media/MediaBrowserServiceCompat.java
@@ -49,6 +49,7 @@ import static androidx.media.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN;
import static androidx.media.MediaBrowserProtocol.SERVICE_VERSION_CURRENT;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Binder;
@@ -1012,6 +1013,18 @@ public abstract class MediaBrowserServiceCompat extends Service {
}
}
+ /**
+ * Attaches to the base context. This method is added to change the visibility of
+ * {@link Service#attachBaseContext(Context)}.
+ * <p>
+ * Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it
+ * because lint checks considers the overriden method as the new public API that needs update
+ * of current.txt.
+ */
+ void attachToBaseContext(Context base) {
+ attachBaseContext(base);
+ }
+
@Override
public void onCreate() {
super.onCreate();
diff --git a/androidx/media/MediaConstants2.java b/androidx/media/MediaConstants2.java
new file mode 100644
index 00000000..68a9a190
--- /dev/null
+++ b/androidx/media/MediaConstants2.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+class MediaConstants2 {
+
+ static final int CONNECT_RESULT_CONNECTED = 0;
+ static final int CONNECT_RESULT_DISCONNECTED = -1;
+
+ // Event string used by IMediaControllerCallback.onEvent()
+ static final String SESSION_EVENT_ON_PLAYER_STATE_CHANGED =
+ "androidx.media.session.event.ON_PLAYER_STATE_CHANGED";
+ static final String SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED =
+ "androidx.media.session.event.ON_CURRENT_MEDIA_ITEM_CHANGED";
+ static final String SESSION_EVENT_ON_ERROR = "androidx.media.session.event.ON_ERROR";
+ static final String SESSION_EVENT_ON_ROUTES_INFO_CHANGED =
+ "androidx.media.session.event.ON_ROUTES_INFO_CHANGED";
+ static final String SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED =
+ "androidx.media.session.event.ON_PLAYBACK_INFO_CHANGED";
+ static final String SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED =
+ "androidx.media.session.event.ON_PLAYBACK_SPEED_CHANGED";
+ static final String SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED =
+ "androidx.media.session.event.ON_BUFFERING_STATE_CHANGED";
+ static final String SESSION_EVENT_ON_REPEAT_MODE_CHANGED =
+ "androidx.media.session.event.ON_REPEAT_MODE_CHANGED";
+ static final String SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED =
+ "androidx.media.session.event.ON_SHUFFLE_MODE_CHANGED";
+ static final String SESSION_EVENT_ON_PLAYLIST_CHANGED =
+ "androidx.media.session.event.ON_PLAYLIST_CHANGED";
+ static final String SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED =
+ "androidx.media.session.event.ON_PLAYLIST_METADATA_CHANGED";
+ static final String SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED =
+ "androidx.media.session.event.ON_ALLOWED_COMMANDS_CHANGED";
+ static final String SESSION_EVENT_SEND_CUSTOM_COMMAND =
+ "androidx.media.session.event.SEND_CUSTOM_COMMAND";
+ static final String SESSION_EVENT_SET_CUSTOM_LAYOUT =
+ "androidx.media.session.event.SET_CUSTOM_LAYOUT";
+
+ // Command string used by MediaControllerCompat.sendCommand()
+ static final String CONTROLLER_COMMAND_CONNECT = "androidx.media.controller.command.CONNECT";
+ static final String CONTROLLER_COMMAND_DISCONNECT =
+ "androidx.media.controller.command.DISCONNECT";
+ static final String CONTROLLER_COMMAND_BY_COMMAND_CODE =
+ "androidx.media.controller.command.BY_COMMAND_CODE";
+ static final String CONTROLLER_COMMAND_BY_CUSTOM_COMMAND =
+ "androidx.media.controller.command.BY_CUSTOM_COMMAND";
+
+
+ static final String ARGUMENT_COMMAND_CODE = "androidx.media.argument.COMMAND_CODE";
+ static final String ARGUMENT_CUSTOM_COMMAND = "androidx.media.argument.CUSTOM_COMMAND";
+ static final String ARGUMENT_ALLOWED_COMMANDS = "androidx.media.argument.ALLOWED_COMMANDS";
+ static final String ARGUMENT_SEEK_POSITION = "androidx.media.argument.SEEK_POSITION";
+ static final String ARGUMENT_PLAYER_STATE = "androidx.media.argument.PLAYER_STATE";
+ static final String ARGUMENT_PLAYBACK_SPEED = "androidx.media.argument.PLAYBACK_SPEED";
+ static final String ARGUMENT_BUFFERING_STATE = "androidx.media.argument.BUFFERING_STATE";
+ static final String ARGUMENT_ERROR_CODE = "androidx.media.argument.ERROR_CODE";
+ static final String ARGUMENT_REPEAT_MODE = "androidx.media.argument.REPEAT_MODE";
+ static final String ARGUMENT_SHUFFLE_MODE = "androidx.media.argument.SHUFFLE_MODE";
+ static final String ARGUMENT_PLAYLIST = "androidx.media.argument.PLAYLIST";
+ static final String ARGUMENT_PLAYLIST_INDEX = "androidx.media.argument.PLAYLIST_INDEX";
+ static final String ARGUMENT_PLAYLIST_METADATA = "androidx.media.argument.PLAYLIST_METADATA";
+ static final String ARGUMENT_RATING = "androidx.media.argument.RATING";
+ static final String ARGUMENT_MEDIA_ITEM = "androidx.media.argument.MEDIA_ITEM";
+ static final String ARGUMENT_MEDIA_ID = "androidx.media.argument.MEDIA_ID";
+ static final String ARGUMENT_QUERY = "androidx.media.argument.QUERY";
+ static final String ARGUMENT_URI = "androidx.media.argument.URI";
+ static final String ARGUMENT_PLAYBACK_STATE_COMPAT =
+ "androidx.media.argument.PLAYBACK_STATE_COMPAT";
+ static final String ARGUMENT_VOLUME = "androidx.media.argument.VOLUME";
+ static final String ARGUMENT_VOLUME_DIRECTION = "androidx.media.argument.VOLUME_DIRECTION";
+ static final String ARGUMENT_VOLUME_FLAGS = "androidx.media.argument.VOLUME_FLAGS";
+ static final String ARGUMENT_EXTRAS = "androidx.media.argument.EXTRAS";
+ static final String ARGUMENT_ARGUMENTS = "androidx.media.argument.ARGUMENTS";
+ static final String ARGUMENT_RESULT_RECEIVER = "androidx.media.argument.RESULT_RECEIVER";
+ static final String ARGUMENT_COMMAND_BUTTONS = "androidx.media.argument.COMMAND_BUTTONS";
+ static final String ARGUMENT_ROUTE_BUNDLE = "androidx.media.argument.ROUTE_BUNDLE";
+ static final String ARGUMENT_PLAYBACK_INFO = "androidx.media.argument.PLAYBACK_INFO";
+
+ static final String ARGUMENT_ICONTROLLER_CALLBACK =
+ "androidx.media.argument.ICONTROLLER_CALLBACK";
+ static final String ARGUMENT_UID = "androidx.media.argument.UID";
+ static final String ARGUMENT_PID = "androidx.media.argument.PID";
+ static final String ARGUMENT_PACKAGE_NAME = "androidx.media.argument.PACKAGE_NAME";
+
+ static final String ROOT_EXTRA_DEFAULT = "androidx.media.root_default_root";
+
+ private MediaConstants2() {
+ }
+}
diff --git a/androidx/media/MediaController2.java b/androidx/media/MediaController2.java
new file mode 100644
index 00000000..1da552d3
--- /dev/null
+++ b/androidx/media/MediaController2.java
@@ -0,0 +1,1770 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_RATING;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.ARGUMENT_URI;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaPlayerBase.UNKNOWN_TIME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.SystemClock;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaPlaylistAgent.RepeatMode;
+import androidx.media.MediaPlaylistAgent.ShuffleMode;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.ErrorCode;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and
+ * other commands can be sent to the session.
+ * <p>
+ * When you're done, use {@link #close()} to clean up resources. This also helps session service
+ * to be destroyed when there's no controller associated with it.
+ * <p>
+ * When controlling {@link MediaSession2}, the controller will be available immediately after
+ * the creation.
+ * <p>
+ * MediaController2 objects are thread-safe.
+ * <p>
+ * @see MediaSession2
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class MediaController2 implements AutoCloseable {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, AudioManager.ADJUST_SAME,
+ AudioManager.ADJUST_MUTE, AudioManager.ADJUST_UNMUTE, AudioManager.ADJUST_TOGGLE_MUTE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VolumeDirection {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef(value = {AudioManager.FLAG_SHOW_UI, AudioManager.FLAG_ALLOW_RINGER_MODES,
+ AudioManager.FLAG_PLAY_SOUND, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE,
+ AudioManager.FLAG_VIBRATE}, flag = true)
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VolumeFlags {}
+
+ /**
+ * Interface for listening to change in activeness of the {@link MediaSession2}. It's
+ * active if and only if it has set a player.
+ */
+ public abstract static class ControllerCallback {
+ /**
+ * Called when the controller is successfully connected to the session. The controller
+ * becomes available afterwards.
+ *
+ * @param controller the controller for this event
+ * @param allowedCommands commands that's allowed by the session.
+ */
+ public void onConnected(@NonNull MediaController2 controller,
+ @NonNull SessionCommandGroup2 allowedCommands) { }
+
+ /**
+ * Called when the session refuses the controller or the controller is disconnected from
+ * the session. The controller becomes unavailable afterwards and the callback wouldn't
+ * be called.
+ * <p>
+ * It will be also called after the {@link #close()}, so you can put clean up code here.
+ * You don't need to call {@link #close()} after this.
+ *
+ * @param controller the controller for this event
+ */
+ public void onDisconnected(@NonNull MediaController2 controller) { }
+
+ /**
+ * Called when the session set the custom layout through the
+ * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}.
+ * <p>
+ * Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)}
+ * is called.
+ *
+ * @param controller the controller for this event
+ * @param layout
+ */
+ public void onCustomLayoutChanged(@NonNull MediaController2 controller,
+ @NonNull List<CommandButton> layout) { }
+
+ /**
+ * Called when the session has changed anything related with the {@link PlaybackInfo}.
+ *
+ * @param controller the controller for this event
+ * @param info new playback info
+ */
+ public void onPlaybackInfoChanged(@NonNull MediaController2 controller,
+ @NonNull PlaybackInfo info) { }
+
+ /**
+ * Called when the allowed commands are changed by session.
+ *
+ * @param controller the controller for this event
+ * @param commands newly allowed commands
+ */
+ public void onAllowedCommandsChanged(@NonNull MediaController2 controller,
+ @NonNull SessionCommandGroup2 commands) { }
+
+ /**
+ * Called when the session sent a custom command.
+ *
+ * @param controller the controller for this event
+ * @param command
+ * @param args
+ * @param receiver
+ */
+ public void onCustomCommand(@NonNull MediaController2 controller,
+ @NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) { }
+
+ /**
+ * Called when the player state is changed.
+ *
+ * @param controller the controller for this event
+ * @param state
+ */
+ public void onPlayerStateChanged(@NonNull MediaController2 controller, int state) { }
+
+ /**
+ * Called when playback speed is changed.
+ *
+ * @param controller the controller for this event
+ * @param speed speed
+ */
+ public void onPlaybackSpeedChanged(@NonNull MediaController2 controller,
+ float speed) { }
+
+ /**
+ * Called to report buffering events for a data source.
+ * <p>
+ * Use {@link #getBufferedPosition()} for current buffering position.
+ *
+ * @param controller the controller for this event
+ * @param item the media item for which buffering is happening.
+ * @param state the new buffering state.
+ */
+ public void onBufferingStateChanged(@NonNull MediaController2 controller,
+ @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
+
+ /**
+ * Called to indicate that seeking is completed.
+ *
+ * @param controller the controller for this event.
+ * @param position the previous seeking request.
+ */
+ public void onSeekCompleted(@NonNull MediaController2 controller, long position) { }
+
+ /**
+ * Called when a error from
+ *
+ * @param controller the controller for this event
+ * @param errorCode error code
+ * @param extras extra information
+ */
+ public void onError(@NonNull MediaController2 controller, @ErrorCode int errorCode,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when the player's currently playing item is changed
+ * <p>
+ * When it's called, you should invalidate previous playback information and wait for later
+ * callbacks.
+ *
+ * @param controller the controller for this event
+ * @param item new item
+ * @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
+ */
+ public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+ @NonNull MediaItem2 item) { }
+
+ /**
+ * Called when a playlist is changed.
+ *
+ * @param controller the controller for this event
+ * @param list new playlist
+ * @param metadata new metadata
+ */
+ public void onPlaylistChanged(@NonNull MediaController2 controller,
+ @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when a playlist metadata is changed.
+ *
+ * @param controller the controller for this event
+ * @param metadata new metadata
+ */
+ public void onPlaylistMetadataChanged(@NonNull MediaController2 controller,
+ @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when the shuffle mode is changed.
+ *
+ * @param controller the controller for this event
+ * @param shuffleMode repeat mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void onShuffleModeChanged(@NonNull MediaController2 controller,
+ @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+ /**
+ * Called when the repeat mode is changed.
+ *
+ * @param controller the controller for this event
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void onRepeatModeChanged(@NonNull MediaController2 controller,
+ @MediaPlaylistAgent.RepeatMode int repeatMode) { }
+
+ /**
+ * Called when a property of the indicated media route has changed.
+ *
+ * @param controller the controller for this event
+ * @param routes The list of Bundle from MediaRouteDescriptor.asBundle().
+ * See MediaRouteDescriptor.fromBundle(Bundle bundle) to get
+ * MediaRouteDescriptor object from the {@code routes}
+ */
+ public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+ @Nullable List<Bundle> routes) { }
+ }
+
+ /**
+ * Holds information about the the way volume is handled for this session.
+ */
+ // The same as MediaController.PlaybackInfo
+ public static final class PlaybackInfo {
+ private static final String KEY_PLAYBACK_TYPE = "android.media.audio_info.playback_type";
+ private static final String KEY_CONTROL_TYPE = "android.media.audio_info.control_type";
+ private static final String KEY_MAX_VOLUME = "android.media.audio_info.max_volume";
+ private static final String KEY_CURRENT_VOLUME = "android.media.audio_info.current_volume";
+ private static final String KEY_AUDIO_ATTRIBUTES = "android.media.audio_info.audio_attrs";
+
+ private final int mPlaybackType;
+ private final int mControlType;
+ private final int mMaxVolume;
+ private final int mCurrentVolume;
+ private final AudioAttributesCompat mAudioAttrsCompat;
+
+ /**
+ * The session uses remote playback.
+ */
+ public static final int PLAYBACK_TYPE_REMOTE = 2;
+ /**
+ * The session uses local playback.
+ */
+ public static final int PLAYBACK_TYPE_LOCAL = 1;
+
+ PlaybackInfo(int playbackType, AudioAttributesCompat attrs, int controlType, int max,
+ int current) {
+ mPlaybackType = playbackType;
+ mAudioAttrsCompat = attrs;
+ mControlType = controlType;
+ mMaxVolume = max;
+ mCurrentVolume = current;
+ }
+
+ /**
+ * Get the type of playback which affects volume handling. One of:
+ * <ul>
+ * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
+ * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
+ * </ul>
+ *
+ * @return The type of playback this session is using.
+ */
+ public int getPlaybackType() {
+ return mPlaybackType;
+ }
+
+ /**
+ * Get the audio attributes for this session. The attributes will affect
+ * volume handling for the session. When the volume type is
+ * {@link #PLAYBACK_TYPE_REMOTE} these may be ignored by the
+ * remote volume handler.
+ *
+ * @return The attributes for this session.
+ */
+ public AudioAttributesCompat getAudioAttributes() {
+ return mAudioAttrsCompat;
+ }
+
+ /**
+ * Get the type of volume control that can be used. One of:
+ * <ul>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
+ * </ul>
+ *
+ * @return The type of volume control that may be used with this session.
+ */
+ public int getControlType() {
+ return mControlType;
+ }
+
+ /**
+ * 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;
+ }
+
+ Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType);
+ bundle.putInt(KEY_CONTROL_TYPE, mControlType);
+ bundle.putInt(KEY_MAX_VOLUME, mMaxVolume);
+ bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume);
+ if (mAudioAttrsCompat != null) {
+ bundle.putParcelable(KEY_AUDIO_ATTRIBUTES,
+ MediaUtils2.toAudioAttributesBundle(mAudioAttrsCompat));
+ }
+ return bundle;
+ }
+
+ static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributesCompat attrs,
+ int controlType, int max, int current) {
+ return new PlaybackInfo(playbackType, attrs, controlType, max, current);
+ }
+
+ static PlaybackInfo fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE);
+ final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE);
+ final int maxVolume = bundle.getInt(KEY_MAX_VOLUME);
+ final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME);
+ final AudioAttributesCompat attrs = MediaUtils2.fromAudioAttributesBundle(
+ bundle.getBundle(KEY_AUDIO_ATTRIBUTES));
+ return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume,
+ currentVolume);
+ }
+ }
+
+ private final class ControllerCompatCallback extends MediaControllerCompat.Callback {
+ @Override
+ public void onSessionReady() {
+ sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (!mHandlerThread.isAlive()) {
+ return;
+ }
+ switch (resultCode) {
+ case CONNECT_RESULT_CONNECTED:
+ onConnectedNotLocked(resultData);
+ break;
+ case CONNECT_RESULT_DISCONNECTED:
+ mCallback.onDisconnected(MediaController2.this);
+ close();
+ break;
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ close();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ synchronized (mLock) {
+ mPlaybackStateCompat = state;
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ synchronized (mLock) {
+ mMediaMetadataCompat = metadata;
+ }
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ switch (event) {
+ case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: {
+ SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+ extras.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+ synchronized (mLock) {
+ mAllowedCommands = allowedCommands;
+ }
+ mCallback.onAllowedCommandsChanged(MediaController2.this, allowedCommands);
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: {
+ int playerState = extras.getInt(ARGUMENT_PLAYER_STATE);
+ synchronized (mLock) {
+ mPlayerState = playerState;
+ }
+ mCallback.onPlayerStateChanged(MediaController2.this, playerState);
+ break;
+ }
+ case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: {
+ MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ if (item == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mCurrentMediaItem = item;
+ }
+ mCallback.onCurrentMediaItemChanged(MediaController2.this, item);
+ break;
+ }
+ case SESSION_EVENT_ON_ERROR: {
+ int errorCode = extras.getInt(ARGUMENT_ERROR_CODE);
+ Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS);
+ mCallback.onError(MediaController2.this, errorCode, errorExtras);
+ break;
+ }
+ case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: {
+ List<Bundle> routes = MediaUtils2.toBundleList(
+ extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE));
+ mCallback.onRoutesInfoChanged(MediaController2.this, routes);
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYLIST_CHANGED: {
+ MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+ extras.getParcelableArray(ARGUMENT_PLAYLIST));
+ synchronized (mLock) {
+ mPlaylist = playlist;
+ mPlaylistMetadata = playlistMetadata;
+ }
+ mCallback.onPlaylistChanged(MediaController2.this, playlist, playlistMetadata);
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: {
+ MediaMetadata2 playlistMetadata = MediaMetadata2.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ synchronized (mLock) {
+ mPlaylistMetadata = playlistMetadata;
+ }
+ mCallback.onPlaylistMetadataChanged(MediaController2.this, playlistMetadata);
+ break;
+ }
+ case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: {
+ int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
+ synchronized (mLock) {
+ mRepeatMode = repeatMode;
+ }
+ mCallback.onRepeatModeChanged(MediaController2.this, repeatMode);
+ break;
+ }
+ case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: {
+ int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
+ synchronized (mLock) {
+ mShuffleMode = shuffleMode;
+ }
+ mCallback.onShuffleModeChanged(MediaController2.this, shuffleMode);
+ break;
+ }
+ case SESSION_EVENT_SEND_CUSTOM_COMMAND: {
+ Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND);
+ if (commandBundle == null) {
+ return;
+ }
+ SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
+ Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
+ ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER);
+ mCallback.onCustomCommand(MediaController2.this, command, args, receiver);
+ break;
+ }
+ case SESSION_EVENT_SET_CUSTOM_LAYOUT: {
+ List<CommandButton> layout = MediaUtils2.fromCommandButtonParcelableArray(
+ extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS));
+ if (layout == null) {
+ return;
+ }
+ mCallback.onCustomLayoutChanged(MediaController2.this, layout);
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: {
+ PlaybackInfo info = PlaybackInfo.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYBACK_INFO));
+ if (info == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mPlaybackInfo = info;
+ }
+ mCallback.onPlaybackInfoChanged(MediaController2.this, info);
+ break;
+ }
+ case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: {
+ PlaybackStateCompat state =
+ extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT);
+ if (state == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mPlaybackStateCompat = state;
+ }
+ mCallback.onPlaybackSpeedChanged(
+ MediaController2.this, state.getPlaybackSpeed());
+ break;
+ }
+ case SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED: {
+ MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE);
+ if (item == null) {
+ return;
+ }
+ synchronized (mLock) {
+ mBufferingState = bufferingState;
+ }
+ mCallback.onBufferingStateChanged(MediaController2.this, item, bufferingState);
+ break;
+ }
+ }
+ }
+ }
+
+ private static final String TAG = "MediaController2";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps
+ // the rootHints so it becomes non-null.
+ static final Bundle sDefaultRootExtras = new Bundle();
+ static {
+ sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true);
+ }
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+
+ private final SessionToken2 mToken;
+ private final ControllerCallback mCallback;
+ private final Executor mCallbackExecutor;
+ private final IBinder.DeathRecipient mDeathRecipient;
+
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+
+ @GuardedBy("mLock")
+ private MediaBrowserCompat mBrowserCompat;
+ @GuardedBy("mLock")
+ private boolean mIsReleased;
+ @GuardedBy("mLock")
+ private List<MediaItem2> mPlaylist;
+ @GuardedBy("mLock")
+ private MediaMetadata2 mPlaylistMetadata;
+ @GuardedBy("mLock")
+ private @RepeatMode int mRepeatMode;
+ @GuardedBy("mLock")
+ private @ShuffleMode int mShuffleMode;
+ @GuardedBy("mLock")
+ private int mPlayerState;
+ @GuardedBy("mLock")
+ private MediaItem2 mCurrentMediaItem;
+ @GuardedBy("mLock")
+ private int mBufferingState;
+ @GuardedBy("mLock")
+ private PlaybackInfo mPlaybackInfo;
+ @GuardedBy("mLock")
+ private SessionCommandGroup2 mAllowedCommands;
+
+ // Media 1.0 variables
+ @GuardedBy("mLock")
+ private MediaControllerCompat mControllerCompat;
+ @GuardedBy("mLock")
+ private ControllerCompatCallback mControllerCompatCallback;
+ @GuardedBy("mLock")
+ private PlaybackStateCompat mPlaybackStateCompat;
+ @GuardedBy("mLock")
+ private MediaMetadataCompat mMediaMetadataCompat;
+
+ // Assignment should be used with the lock hold, but should be used without a lock to prevent
+ // potential deadlock.
+ @GuardedBy("mLock")
+ private volatile boolean mConnected;
+
+ /**
+ * Create a {@link MediaController2} from the {@link SessionToken2}.
+ * This connects to the session and may wake up the service if it's not available.
+ *
+ * @param context Context
+ * @param token token to connect to
+ * @param executor executor to run callbacks on.
+ * @param callback controller callback to receive changes in
+ */
+ public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull Executor executor, @NonNull ControllerCallback callback) {
+ super();
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ mContext = context;
+ mHandlerThread = new HandlerThread("MediaController2_Thread");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ mToken = token;
+ mCallback = callback;
+ mCallbackExecutor = executor;
+ mDeathRecipient = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ MediaController2.this.close();
+ }
+ };
+
+ initialize();
+ }
+
+ /**
+ * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+ * received.
+ */
+ @Override
+ public void close() {
+ if (DEBUG) {
+ //Log.d(TAG, "release from " + mToken, new IllegalStateException());
+ }
+ synchronized (mLock) {
+ if (mIsReleased) {
+ // Prevent re-enterance from the ControllerCallback.onDisconnected()
+ return;
+ }
+ mHandler.removeCallbacksAndMessages(null);
+ mHandlerThread.quitSafely();
+
+ mIsReleased = true;
+
+ // Send command before the unregister callback to use mIControllerCallback in the
+ // callback.
+ sendCommand(CONTROLLER_COMMAND_DISCONNECT);
+ if (mControllerCompat != null) {
+ mControllerCompat.unregisterCallback(mControllerCompatCallback);
+ }
+ if (mBrowserCompat != null) {
+ mBrowserCompat.disconnect();
+ mBrowserCompat = null;
+ }
+ if (mControllerCompat != null) {
+ mControllerCompat.unregisterCallback(mControllerCompatCallback);
+ mControllerCompat = null;
+ }
+ mConnected = false;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onDisconnected(MediaController2.this);
+ }
+ });
+ }
+
+ /**
+ * @return token
+ */
+ public @NonNull SessionToken2 getSessionToken() {
+ return mToken;
+ }
+
+ /**
+ * Returns whether this class is connected to active {@link MediaSession2} or not.
+ */
+ public boolean isConnected() {
+ synchronized (mLock) {
+ return mConnected;
+ }
+ }
+
+ /**
+ * Requests that the player starts or resumes playback.
+ */
+ public void play() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_PLAY);
+ }
+ }
+
+ /**
+ * Requests that the player pauses playback.
+ */
+ public void pause() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_PAUSE);
+ }
+ }
+
+ /**
+ * Requests that the player be reset to its uninitialized state.
+ */
+ public void reset() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_RESET);
+ }
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+ * to start playback.
+ */
+ public void prepare() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_PLAYBACK_PREPARE);
+ }
+ }
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this
+ * may increase the rate.
+ */
+ public void fastForward() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD);
+ }
+ }
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase
+ * the rate.
+ */
+ public void rewind() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ sendCommand(COMMAND_CODE_SESSION_REWIND);
+ }
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public void seekTo(long pos) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putLong(ARGUMENT_SEEK_POSITION, pos);
+ sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ /**
+ * Request that the player start playback for a specific media id.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_MEDIA_ID, mediaId);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args);
+ }
+ }
+
+ /**
+ * Request that the player start playback for a specific search query.
+ *
+ * @param query The search query. Should not be an empty string.
+ * @param extras Optional extras that can include extra information about the query.
+ */
+ public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_QUERY, query);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args);
+ }
+ }
+
+ /**
+ * Request that the player start playback for a specific {@link Uri}.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(ARGUMENT_URI, uri);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args);
+ }
+ }
+
+ /**
+ * Request that the player prepare playback for a specific media id. In other words, other
+ * sessions can continue to play during the preparation of this session. This method can be
+ * used to speed up the start of the playback. Once the preparation is done, the session
+ * will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromMediaId} can be directly called without this method.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_MEDIA_ID, mediaId);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args);
+ }
+ }
+
+ /**
+ * Request that the player prepare playback for a specific search query.
+ * In other words, other sessions can continue to play during the preparation of this session.
+ * This method can be used to speed up the start of the playback.
+ * Once the preparation is done, the session will change its playback state to
+ * {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromSearch} can be directly called without this method.
+ *
+ * @param query The search query. Should not be an empty string.
+ * @param extras Optional extras that can include extra information about the query.
+ */
+ public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_QUERY, query);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args);
+ }
+ }
+
+ /**
+ * Request that the player prepare playback for a specific {@link Uri}. In other words,
+ * other sessions can continue to play during the preparation of this session. This method
+ * can be used to speed up the start of the playback. Once the preparation is done, the
+ * session will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}.
+ * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromUri} can be directly called without this method.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(ARGUMENT_URI, uri);
+ args.putBundle(ARGUMENT_EXTRAS, extras);
+ sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args);
+ }
+ }
+
+ /**
+ * 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}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @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 for local
+ * playback
+ */
+ public void setVolumeTo(int value, @VolumeFlags int flags) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_VOLUME, value);
+ args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+ sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args);
+ }
+ }
+
+ /**
+ * 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}.
+ * <p>
+ * The command will be ignored if the session does not support
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
+ * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param direction The direction to adjust the volume in.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_VOLUME_DIRECTION, direction);
+ args.putInt(ARGUMENT_VOLUME_FLAGS, flags);
+ sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args);
+ }
+ }
+
+ /**
+ * Get an intent for launching UI associated with this session if one exists.
+ *
+ * @return A {@link PendingIntent} to launch UI or null.
+ */
+ public @Nullable PendingIntent getSessionActivity() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return null;
+ }
+ return mControllerCompat.getSessionActivity();
+ }
+ }
+
+ /**
+ * Get the lastly cached player state from
+ * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
+ *
+ * @return player state
+ */
+ public int getPlayerState() {
+ synchronized (mLock) {
+ return mPlayerState;
+ }
+ }
+
+ /**
+ * Gets the duration of the current media item, or {@link MediaPlayerBase#UNKNOWN_TIME} if
+ * unknown.
+ * @return the duration in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
+ */
+ public long getDuration() {
+ synchronized (mLock) {
+ if (mMediaMetadataCompat != null
+ && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) {
+ return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION);
+ }
+ }
+ return MediaPlayerBase.UNKNOWN_TIME;
+ }
+
+ /**
+ * Gets the current playback position.
+ * <p>
+ * This returns the calculated value of the position, based on the difference between the
+ * update time and current time.
+ *
+ * @return position
+ */
+ public long getCurrentPosition() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return UNKNOWN_TIME;
+ }
+ if (mPlaybackStateCompat != null) {
+ long timeDiff = SystemClock.elapsedRealtime()
+ - mPlaybackStateCompat.getLastPositionUpdateTime();
+ long expectedPosition = mPlaybackStateCompat.getPosition()
+ + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff);
+ return Math.max(0, expectedPosition);
+ }
+ return UNKNOWN_TIME;
+ }
+ }
+
+ /**
+ * Get the lastly cached playback speed from
+ * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
+ *
+ * @return speed the lastly cached playback speed, or 0.0f if unknown.
+ */
+ public float getPlaybackSpeed() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return 0f;
+ }
+ return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed();
+ }
+ }
+
+ /**
+ * Set the playback speed.
+ */
+ public void setPlaybackSpeed(float speed) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed);
+ sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args);
+ }
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state.
+ */
+ public @MediaPlayerBase.BuffState int getBufferingState() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return BUFFERING_STATE_UNKNOWN;
+ }
+ return mBufferingState;
+ }
+ }
+
+ /**
+ * Gets the lastly cached buffered position from the session when
+ * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
+ * called.
+ *
+ * @return buffering position in millis, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
+ */
+ public long getBufferedPosition() {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return UNKNOWN_TIME;
+ }
+ return (mPlaybackStateCompat == null) ? UNKNOWN_TIME
+ : mPlaybackStateCompat.getBufferedPosition();
+ }
+ }
+
+ /**
+ * Get the current playback info for this session.
+ *
+ * @return The current playback info or null.
+ */
+ public @Nullable PlaybackInfo getPlaybackInfo() {
+ synchronized (mLock) {
+ return mPlaybackInfo;
+ }
+ }
+
+ /**
+ * Rate the media. This will cause the rating to be set for the current user.
+ * The rating style must follow the user rating style from the session.
+ * You can get the rating style from the session through the
+ * {@link MediaMetadata2#getRating(String)} with the key
+ * {@link MediaMetadata2#METADATA_KEY_USER_RATING}.
+ * <p>
+ * If the user rating was {@code null}, the media item does not accept setting user rating.
+ *
+ * @param mediaId The id of the media
+ * @param rating The rating to set
+ */
+ public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putString(ARGUMENT_MEDIA_ID, mediaId);
+ args.putBundle(ARGUMENT_RATING, rating.toBundle());
+ sendCommand(COMMAND_CODE_SESSION_SET_RATING, args);
+ }
+ }
+
+ /**
+ * Send custom command to the session
+ *
+ * @param command custom command
+ * @param args optional argument
+ * @param cb optional result receiver
+ */
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) {
+ synchronized (mLock) {
+ if (!mConnected) {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ return;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+ bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+ sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb);
+ }
+ }
+
+ /**
+ * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}.
+ * <p>
+ * This list may differ with the list that was specified with
+ * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+ * implementation. Use media items returned here for other playlist agent APIs such as
+ * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have
+ * enough permission.
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+ */
+ public @Nullable List<MediaItem2> getPlaylist() {
+ synchronized (mLock) {
+ return mPlaylist;
+ }
+ }
+
+ /**
+ * Sets the playlist.
+ * <p>
+ * Even when the playlist is successfully set, use the playlist returned from
+ * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
+ * Otherwise the session in the remote process can't distinguish between media items.
+ *
+ * @param list playlist
+ * @param metadata metadata of the playlist
+ * @see #getPlaylist()
+ * @see ControllerCallback#onPlaylistChanged
+ */
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ if (list == null) {
+ throw new IllegalArgumentException("list shouldn't be null");
+ }
+ Bundle args = new Bundle();
+ args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list));
+ args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args);
+ }
+
+ /**
+ * Updates the playlist metadata
+ *
+ * @param metadata metadata of the playlist
+ */
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args);
+ }
+
+ /**
+ * Gets the lastly cached playlist playlist metadata either from
+ * {@link ControllerCallback#onPlaylistMetadataChanged or
+ * {@link ControllerCallback#onPlaylistChanged}.
+ *
+ * @return metadata metadata of the playlist, or null if none is set
+ */
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ synchronized (mLock) {
+ return mPlaylistMetadata;
+ }
+ }
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the playlist,
+ * the current index of the playlist will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args);
+ }
+
+ /**
+ * Removes the media item at index in the playlist.
+ *<p>
+ * If the item is the currently playing item of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @param item the media item you want to add
+ */
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args);
+ }
+
+ /**
+ * Replace the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_PLAYLIST_INDEX, index);
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args);
+ }
+
+ /**
+ * Get the lastly cached current item from
+ * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
+ *
+ * @return the currently playing item, or null if unknown.
+ */
+ public MediaItem2 getCurrentMediaItem() {
+ synchronized (mLock) {
+ return mCurrentMediaItem;
+ }
+ }
+
+ /**
+ * Skips to the previous item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}.
+ */
+ public void skipToPreviousItem() {
+ sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM);
+ }
+
+ /**
+ * Skips to the next item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToNextItem()}.
+ */
+ public void skipToNextItem() {
+ sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM);
+ }
+
+ /**
+ * Skips to the item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @param item The item in the playlist you want to play
+ */
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args);
+ }
+
+ /**
+ * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}.
+ *
+ * @return repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public @RepeatMode int getRepeatMode() {
+ synchronized (mLock) {
+ return mRepeatMode;
+ }
+ }
+
+ /**
+ * Sets the repeat mode.
+ *
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args);
+ }
+
+ /**
+ * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}.
+ *
+ * @return The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public @ShuffleMode int getShuffleMode() {
+ synchronized (mLock) {
+ return mShuffleMode;
+ }
+ }
+
+ /**
+ * Sets the shuffle mode.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ Bundle args = new Bundle();
+ args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+ sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args);
+ }
+
+ /**
+ * Queries for information about the routes currently known.
+ */
+ public void subscribeRoutesInfo() {
+ sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO);
+ }
+
+ /**
+ * Unsubscribes for changes to the routes.
+ * <p>
+ * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for
+ * the routes once this method returns.
+ * </p>
+ */
+ public void unsubscribeRoutesInfo() {
+ sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO);
+ }
+
+ /**
+ * Selects the specified route.
+ *
+ * @param route The route to select.
+ */
+ public void selectRoute(@NonNull Bundle route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route shouldn't be null");
+ }
+ Bundle args = new Bundle();
+ args.putBundle(ARGUMENT_ROUTE_BUNDLE, route);
+ sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args);
+ }
+
+ // Should be used without a lock to prevent potential deadlock.
+ void onConnectedNotLocked(Bundle data) {
+ // is enough or should we pass it while connecting?
+ final SessionCommandGroup2 allowedCommands = SessionCommandGroup2.fromBundle(
+ data.getBundle(ARGUMENT_ALLOWED_COMMANDS));
+ final int playerState = data.getInt(ARGUMENT_PLAYER_STATE);
+ final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE);
+ final PlaybackStateCompat playbackStateCompat = data.getParcelable(
+ ARGUMENT_PLAYBACK_STATE_COMPAT);
+ final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE);
+ final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE);
+ final List<MediaItem2> playlist = MediaUtils2.fromMediaItem2ParcelableArray(
+ data.getParcelableArray(ARGUMENT_PLAYLIST));
+ final MediaItem2 currentMediaItem = MediaItem2.fromBundle(
+ data.getBundle(ARGUMENT_MEDIA_ITEM));
+ final PlaybackInfo playbackInfo =
+ PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO));
+ final MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+ data.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ if (DEBUG) {
+ Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken()
+ + ", allowedCommands=" + allowedCommands);
+ }
+ boolean close = false;
+ try {
+ synchronized (mLock) {
+ if (mIsReleased) {
+ return;
+ }
+ if (mConnected) {
+ Log.e(TAG, "Cannot be notified about the connection result many times."
+ + " Probably a bug or malicious app.");
+ close = true;
+ return;
+ }
+ mAllowedCommands = allowedCommands;
+ mPlayerState = playerState;
+ mBufferingState = bufferingState;
+ mPlaybackStateCompat = playbackStateCompat;
+ mRepeatMode = repeatMode;
+ mShuffleMode = shuffleMode;
+ mPlaylist = playlist;
+ mCurrentMediaItem = currentMediaItem;
+ mPlaylistMetadata = metadata;
+ mConnected = true;
+ mPlaybackInfo = playbackInfo;
+ }
+ mCallbackExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ // Note: We may trigger ControllerCallbacks with the initial values
+ // But it's hard to define the order of the controller callbacks
+ // Only notify about the
+ mCallback.onConnected(MediaController2.this, allowedCommands);
+ }
+ });
+ } finally {
+ if (close) {
+ // Trick to call release() without holding the lock, to prevent potential deadlock
+ // with the developer's custom lock within the ControllerCallback.onDisconnected().
+ close();
+ }
+ }
+ }
+
+ private void initialize() {
+ if (mToken.getType() == SessionToken2.TYPE_SESSION) {
+ synchronized (mLock) {
+ mBrowserCompat = null;
+ }
+ connectToSession(mToken.getSessionCompatToken());
+ } else {
+ connectToService();
+ }
+ }
+
+ private void connectToSession(MediaSessionCompat.Token sessionCompatToken) {
+ MediaControllerCompat controllerCompat = null;
+ try {
+ controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ synchronized (mLock) {
+ mControllerCompat = controllerCompat;
+ mControllerCompatCallback = new ControllerCompatCallback();
+ mControllerCompat.registerCallback(mControllerCompatCallback, mHandler);
+ }
+
+ if (controllerCompat.isSessionReady()) {
+ sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (!mHandlerThread.isAlive()) {
+ return;
+ }
+ switch (resultCode) {
+ case CONNECT_RESULT_CONNECTED:
+ onConnectedNotLocked(resultData);
+ break;
+ case CONNECT_RESULT_DISCONNECTED:
+ mCallback.onDisconnected(MediaController2.this);
+ close();
+ break;
+ }
+ }
+ });
+ }
+ }
+
+ private void connectToService() {
+ synchronized (mLock) {
+ mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(),
+ new ConnectionCallback(), sDefaultRootExtras);
+ mBrowserCompat.connect();
+ }
+ }
+
+ private void sendCommand(int commandCode) {
+ sendCommand(commandCode, null);
+ }
+
+ private void sendCommand(int commandCode, Bundle args) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putInt(ARGUMENT_COMMAND_CODE, commandCode);
+ sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null);
+ }
+
+ private void sendCommand(String command) {
+ sendCommand(command, null, null);
+ }
+
+ private void sendCommand(String command, ResultReceiver receiver) {
+ sendCommand(command, null, receiver);
+ }
+
+ private void sendCommand(String command, Bundle args, ResultReceiver receiver) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ MediaControllerCompat controller;
+ ControllerCompatCallback callback;
+ synchronized (mLock) {
+ controller = mControllerCompat;
+ callback = mControllerCompatCallback;
+ }
+ args.putBinder(ARGUMENT_ICONTROLLER_CALLBACK, callback.getIControllerCallback().asBinder());
+ args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName());
+ args.putInt(ARGUMENT_UID, Process.myUid());
+ args.putInt(ARGUMENT_PID, Process.myPid());
+ controller.sendCommand(command, args, receiver);
+ }
+
+ @NonNull Context getContext() {
+ return mContext;
+ }
+
+ @NonNull ControllerCallback getCallback() {
+ return mCallback;
+ }
+
+ @NonNull Executor getCallbackExecutor() {
+ return mCallbackExecutor;
+ }
+
+ @Nullable MediaBrowserCompat getBrowserCompat() {
+ synchronized (mLock) {
+ return mBrowserCompat;
+ }
+ }
+
+ private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+ @Override
+ public void onConnected() {
+ MediaBrowserCompat browser = getBrowserCompat();
+ if (browser != null) {
+ connectToSession(browser.getSessionToken());
+ } else if (DEBUG) {
+ Log.d(TAG, "Controller is closed prematually", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended() {
+ close();
+ }
+
+ @Override
+ public void onConnectionFailed() {
+ close();
+ }
+ }
+}
diff --git a/androidx/media/MediaController2Test.java b/androidx/media/MediaController2Test.java
new file mode 100644
index 00000000..75c9e502
--- /dev/null
+++ b/androidx/media/MediaController2Test.java
@@ -0,0 +1,1306 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.TestServiceRegistry.SessionServiceCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+// TODO(jaeawn): Revisit create/close session in the sHandler. It's no longer necessary.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+public class MediaController2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaController2Test";
+
+ PendingIntent mIntent;
+ MediaSession2 mSession;
+ MediaController2 mController;
+ MockPlayer mPlayer;
+ MockPlaylistAgent mMockAgent;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ final Intent sessionActivity = new Intent(mContext, MockActivity.class);
+ // Create this test specific MediaSession2 to use our own Handler.
+ mIntent = PendingIntent.getActivity(mContext, 0, sessionActivity, 0);
+
+ mPlayer = new MockPlayer(1);
+ mMockAgent = new MockPlaylistAgent();
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ return super.onConnect(session, controller);
+ }
+ return null;
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaSession2 session,
+ MediaPlaylistAgent playlistAgent,
+ MediaMetadata2 metadata) {
+ super.onPlaylistMetadataChanged(session, playlistAgent, metadata);
+ }
+ })
+ .setSessionActivity(mIntent)
+ .setId(TAG).build();
+ mController = createController(mSession.getToken());
+ TestServiceRegistry.getInstance().setHandler(sHandler);
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ if (mSession != null) {
+ mSession.close();
+ }
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ /**
+ * Test if the {@link MediaSession2TestBase.TestControllerCallback} wraps the callback proxy
+ * without missing any method.
+ */
+ @Test
+ public void testTestControllerCallback() {
+ prepareLooper();
+ Method[] methods = TestControllerCallback.class.getMethods();
+ assertNotNull(methods);
+ for (int i = 0; i < methods.length; i++) {
+ // For any methods in the controller callback, TestControllerCallback should have
+ // overriden the method and call matching API in the callback proxy.
+ assertNotEquals("TestControllerCallback should override " + methods[i]
+ + " and call callback proxy",
+ ControllerCallback.class, methods[i].getDeclaringClass());
+ }
+ }
+
+ @Test
+ public void testPlay() {
+ prepareLooper();
+ mController.play();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPlayCalled);
+ }
+
+ @Test
+ public void testPause() {
+ prepareLooper();
+ mController.pause();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPauseCalled);
+ }
+
+ @Test
+ public void testReset() {
+ prepareLooper();
+ mController.reset();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mResetCalled);
+ }
+
+ @Test
+ public void testPrepare() {
+ prepareLooper();
+ mController.prepare();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPrepareCalled);
+ }
+
+ @Test
+ public void testSeekTo() {
+ prepareLooper();
+ final long seekPosition = 12125L;
+ mController.seekTo(seekPosition);
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSeekToCalled);
+ assertEquals(seekPosition, mPlayer.mSeekPosition);
+ }
+
+ @Test
+ public void testGettersAfterConnected() throws InterruptedException {
+ prepareLooper();
+ final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+ final long position = 150000;
+ final long bufferedPosition = 900000;
+ final float speed = 0.5f;
+ final MediaItem2 currentMediaItem = TestUtils.createMediaItemWithMetadata();
+
+ mPlayer.mLastPlayerState = state;
+ mPlayer.mLastBufferingState = bufferingState;
+ mPlayer.mCurrentPosition = position;
+ mPlayer.mBufferedPosition = bufferedPosition;
+ mPlayer.mPlaybackSpeed = speed;
+ mMockAgent.mCurrentMediaItem = currentMediaItem;
+
+ long time1 = System.currentTimeMillis();
+ MediaController2 controller = createController(mSession.getToken());
+ long time2 = System.currentTimeMillis();
+ assertEquals(state, controller.getPlayerState());
+ assertEquals(bufferedPosition, controller.getBufferedPosition());
+ assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+ long positionLowerBound = (long) (position + speed * (System.currentTimeMillis() - time2));
+ long currentPosition = controller.getCurrentPosition();
+ long positionUpperBound = (long) (position + speed * (System.currentTimeMillis() - time1));
+ assertTrue("curPos=" + currentPosition + ", lowerBound=" + positionLowerBound
+ + ", upperBound=" + positionUpperBound,
+ positionLowerBound <= currentPosition && currentPosition <= positionUpperBound);
+ assertEquals(currentMediaItem, controller.getCurrentMediaItem());
+ }
+
+ @Test
+ public void testGetSessionActivity() {
+ prepareLooper();
+ PendingIntent sessionActivity = mController.getSessionActivity();
+ assertEquals(mContext.getPackageName(), sessionActivity.getCreatorPackage());
+ assertEquals(Process.myUid(), sessionActivity.getCreatorUid());
+ }
+
+ @Test
+ public void testSetPlaylist() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mController.setPlaylist(list, null /* Metadata */);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetPlaylistCalled);
+ assertNull(mMockAgent.mMetadata);
+
+ assertNotNull(mMockAgent.mPlaylist);
+ assertEquals(list.size(), mMockAgent.mPlaylist.size());
+ for (int i = 0; i < list.size(); i++) {
+ // MediaController2.setPlaylist does not ensure the equality of the items.
+ assertEquals(list.get(i).getMediaId(), mMockAgent.mPlaylist.get(i).getMediaId());
+ }
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onPlaylistChanged(
+ * MediaController2, List, MediaMetadata2)}.
+ */
+ @Test
+ public void testGetPlaylist() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> testList = TestUtils.createPlaylist(2);
+ final AtomicReference<List<MediaItem2>> listFromCallback = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> playlist, MediaMetadata2 metadata) {
+ assertNotNull(playlist);
+ assertEquals(testList.size(), playlist.size());
+ for (int i = 0; i < playlist.size(); i++) {
+ assertEquals(testList.get(i).getMediaId(), playlist.get(i).getMediaId());
+ }
+ listFromCallback.set(playlist);
+ latch.countDown();
+ }
+ };
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return testList;
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testControllerCallback_onPlaylistChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(
+ session.getToken(), true, callback);
+ agent.notifyPlaylistChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(listFromCallback.get(), controller.getPlaylist());
+ }
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() throws InterruptedException {
+ prepareLooper();
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mController.updatePlaylistMetadata(testMetadata);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+ assertNotNull(mMockAgent.mMetadata);
+ assertEquals(testMetadata.getMediaId(), mMockAgent.mMetadata.getMediaId());
+ }
+
+ @Test
+ public void testGetPlaylistMetadata() throws InterruptedException {
+ prepareLooper();
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ final AtomicReference<MediaMetadata2> metadataFromCallback = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ assertNotNull(testMetadata);
+ assertEquals(testMetadata.getMediaId(), metadata.getMediaId());
+ metadataFromCallback.set(metadata);
+ latch.countDown();
+ }
+ };
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return testMetadata;
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testGetPlaylistMetadata")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(session.getToken(), true, callback);
+ agent.notifyPlaylistMetadataChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(metadataFromCallback.get().getMediaId(),
+ controller.getPlaylistMetadata().getMediaId());
+ }
+ }
+
+ @Test
+ public void testSetPlaybackSpeed() throws Exception {
+ prepareLooper();
+ final float speed = 1.5f;
+ mController.setPlaybackSpeed(speed);
+ assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+ }
+
+ /**
+ * Test whether {@link MediaSession2#setPlaylist(List, MediaMetadata2)} is notified
+ * through the
+ * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2, MediaMetadata2)}
+ * if the controller doesn't have {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST} but
+ * {@link SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA}.
+ */
+ @Test
+ public void testControllerCallback_onPlaylistMetadataChanged() throws InterruptedException {
+ prepareLooper();
+ final MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ assertNotNull(metadata);
+ assertEquals(item.getMediaId(), metadata.getMediaId());
+ latch.countDown();
+ }
+ };
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(
+ SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA));
+ return commands;
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return item.getMetadata();
+ }
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return list;
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testControllerCallback_onPlaylistMetadataChanged")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .setPlaylistAgent(agent)
+ .build()) {
+ MediaController2 controller = createController(session.getToken(), true, callback);
+ agent.notifyPlaylistMetadataChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testAddPlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mController.addPlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mAddPlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ // MediaController2.addPlaylistItem does not ensure the equality of the items.
+ assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+ }
+
+ @Test
+ public void testRemovePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ mMockAgent.mPlaylist = TestUtils.createPlaylist(2);
+
+ // Recreate controller for sending removePlaylistItem.
+ // It's easier to ensure that MediaController2.getPlaylist() returns the playlist from the
+ // agent.
+ MediaController2 controller = createController(mSession.getToken());
+ MediaItem2 targetItem = controller.getPlaylist().get(0);
+ controller.removePlaylistItem(targetItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+ assertEquals(targetItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mController.replacePlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+ // MediaController2.replacePlaylistItem does not ensure the equality of the items.
+ assertEquals(testMediaItem.getMediaId(), mMockAgent.mItem.getMediaId());
+ }
+
+ @Test
+ public void testSkipToPreviousItem() throws InterruptedException {
+ prepareLooper();
+ mController.skipToPreviousItem();
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+ }
+
+ @Test
+ public void testSkipToNextItem() throws InterruptedException {
+ prepareLooper();
+ mController.skipToNextItem();
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mMockAgent.mSkipToNextItemCalled);
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws InterruptedException {
+ prepareLooper();
+ MediaController2 controller = createController(mSession.getToken());
+ MediaItem2 targetItem = TestUtils.createMediaItemWithMetadata();
+ controller.skipToPlaylistItem(targetItem);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+ assertEquals(targetItem, mMockAgent.mItem);
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onShuffleModeChanged(MediaController2, int)}.
+ */
+ @Test
+ public void testGetShuffleMode() throws InterruptedException {
+ prepareLooper();
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public int getShuffleMode() {
+ return testShuffleMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ assertEquals(testShuffleMode, shuffleMode);
+ latch.countDown();
+ }
+ };
+ mSession.updatePlayer(mPlayer, agent, null);
+ MediaController2 controller = createController(mSession.getToken(), true, callback);
+ agent.notifyShuffleModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(testShuffleMode, controller.getShuffleMode());
+ }
+
+ @Test
+ public void testSetShuffleMode() throws InterruptedException {
+ prepareLooper();
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ mController.setShuffleMode(testShuffleMode);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetShuffleModeCalled);
+ assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onRepeatModeChanged(MediaController2, int)}.
+ */
+ @Test
+ public void testGetRepeatMode() throws InterruptedException {
+ prepareLooper();
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ final MediaPlaylistAgent agent = new MockPlaylistAgent() {
+ @Override
+ public int getRepeatMode() {
+ return testRepeatMode;
+ }
+ };
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ assertEquals(testRepeatMode, repeatMode);
+ latch.countDown();
+ }
+ };
+ mSession.updatePlayer(mPlayer, agent, null);
+ MediaController2 controller = createController(mSession.getToken(), true, callback);
+ agent.notifyRepeatModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertEquals(testRepeatMode, controller.getRepeatMode());
+ }
+
+ @Test
+ public void testSetRepeatMode() throws InterruptedException {
+ prepareLooper();
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ mController.setRepeatMode(testRepeatMode);
+ assertTrue(mMockAgent.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ assertTrue(mMockAgent.mSetRepeatModeCalled);
+ assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+ }
+
+ @Test
+ public void testSetVolumeTo() throws Exception {
+ // TODO(jaewan): Also test with local volume.
+ prepareLooper();
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ TestVolumeProvider volumeProvider =
+ new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+ mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+ final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+ final int targetVolume = 50;
+ controller.setVolumeTo(targetVolume, 0 /* flags */);
+ assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(volumeProvider.mSetVolumeToCalled);
+ assertEquals(targetVolume, volumeProvider.mVolume);
+ }
+
+ @Test
+ public void testAdjustVolume() throws Exception {
+ // TODO(jaewan): Also test with local volume.
+ prepareLooper();
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ TestVolumeProvider volumeProvider =
+ new TestVolumeProvider(volumeControlType, maxVolume, currentVolume);
+
+ mSession.updatePlayer(new MockPlayer(0), null, volumeProvider);
+ final MediaController2 controller = createController(mSession.getToken(), true, null);
+
+ final int direction = AudioManager.ADJUST_RAISE;
+ controller.adjustVolume(direction, 0 /* flags */);
+ assertTrue(volumeProvider.mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(volumeProvider.mAdjustVolumeCalled);
+ assertEquals(direction, volumeProvider.mDirection);
+ }
+
+ @Test
+ public void testGetPackageName() {
+ prepareLooper();
+ assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+ }
+
+ @Test
+ public void testSendCustomCommand() throws InterruptedException {
+ prepareLooper();
+ // TODO(jaewan): Need to revisit with the permission.
+ final SessionCommand2 testCommand =
+ new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomCommand");
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onCustomCommand(MediaSession2 session, ControllerInfo controller,
+ SessionCommand2 customCommand, Bundle args, ResultReceiver cb) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(testCommand, customCommand);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(cb);
+ latch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+ controller.sendCustomCommand(testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testControllerCallback_onConnected() throws InterruptedException {
+ prepareLooper();
+ // createController() uses controller callback to wait until the controller becomes
+ // available.
+ MediaController2 controller = createController(mSession.getToken());
+ assertNotNull(controller);
+ }
+
+ @Test
+ public void testControllerCallback_sessionRejects() throws InterruptedException {
+ prepareLooper();
+ final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ return null;
+ }
+ };
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ }
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testControllerCallback_releaseSession() throws InterruptedException {
+ prepareLooper();
+ mSession.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testControllerCallback_close() throws InterruptedException {
+ prepareLooper();
+ mController.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testFastForward() throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onFastForward(MediaSession2 session, ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testFastForward").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.fastForward();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testRewind() throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onRewind(MediaSession2 session, ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testRewind").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.rewind();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromSearch() throws InterruptedException {
+ prepareLooper();
+ final String request = "random query";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ super.onPlayFromSearch(session, controller, query, extras);
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, query);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromSearch").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromSearch(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromUri() throws InterruptedException {
+ prepareLooper();
+ final Uri request = Uri.parse("foo://boo");
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+ Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, uri);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromUri").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromUri(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPlayFromMediaId() throws InterruptedException {
+ prepareLooper();
+ final String request = "media_id";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, mediaId);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPlayFromMediaId").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.playFromMediaId(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromSearch() throws InterruptedException {
+ prepareLooper();
+ final String request = "random query";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, query);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromSearch").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromSearch(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromUri() throws InterruptedException {
+ prepareLooper();
+ final Uri request = Uri.parse("foo://boo");
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller, Uri uri,
+ Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, uri);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromUri").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromUri(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testPrepareFromMediaId() throws InterruptedException {
+ prepareLooper();
+ final String request = "media_id";
+ final Bundle bundle = new Bundle();
+ bundle.putString("key", "value");
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(request, mediaId);
+ assertTrue(TestUtils.equals(bundle, extras));
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testPrepareFromMediaId").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.prepareFromMediaId(request, bundle);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetRating() throws InterruptedException {
+ prepareLooper();
+ final int ratingType = Rating2.RATING_5_STARS;
+ final float ratingValue = 3.5f;
+ final Rating2 rating = Rating2.newStarRating(ratingType, ratingValue);
+ final String mediaId = "media_id";
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback callback = new SessionCallback() {
+ @Override
+ public void onSetRating(MediaSession2 session, ControllerInfo controller,
+ String mediaIdOut, Rating2 ratingOut) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertEquals(mediaId, mediaIdOut);
+ assertEquals(rating, ratingOut);
+ latch.countDown();
+ }
+ };
+
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback)
+ .setId("testSetRating").build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.setRating(mediaId, rating);
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testIsConnected() throws InterruptedException {
+ prepareLooper();
+ assertTrue(mController.isConnected());
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ }
+ });
+ waitForDisconnect(mController, true);
+ assertFalse(mController.isConnected());
+ }
+
+ /**
+ * Test potential deadlock for calls between controller and session.
+ */
+ @Test
+ public void testDeadlock() throws InterruptedException {
+ prepareLooper();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = null;
+ }
+ });
+
+ // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+ final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+ final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+ sessionThread.start();
+ testThread.start();
+ final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+ final Handler testHandler = new Handler(testThread.getLooper());
+ final CountDownLatch latch = new CountDownLatch(1);
+ try {
+ final MockPlayer player = new MockPlayer(0);
+ sessionHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setId("testDeadlock").build();
+ }
+ });
+ final MediaController2 controller = createController(mSession.getToken());
+ testHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final int state = MediaPlayerBase.PLAYER_STATE_ERROR;
+ for (int i = 0; i < 100; i++) {
+ // triggers call from session to controller.
+ player.notifyPlaybackState(state);
+ // triggers call from controller to session.
+ controller.play();
+
+ // Repeat above
+ player.notifyPlaybackState(state);
+ controller.pause();
+ player.notifyPlaybackState(state);
+ controller.reset();
+ player.notifyPlaybackState(state);
+ controller.skipToNextItem();
+ player.notifyPlaybackState(state);
+ controller.skipToPreviousItem();
+ }
+ // This may hang if deadlock happens.
+ latch.countDown();
+ }
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ if (mSession != null) {
+ sessionHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ // Clean up here because sessionHandler will be removed afterwards.
+ mSession.close();
+ mSession = null;
+ }
+ });
+ }
+ if (sessionThread != null) {
+ sessionThread.quitSafely();
+ }
+ if (testThread != null) {
+ testThread.quitSafely();
+ }
+ }
+ }
+
+ @Test
+ public void testGetServiceToken() {
+ prepareLooper();
+ SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+ assertNotNull(token);
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(MockMediaSessionService2.ID, token.getId());
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ }
+
+ @Test
+ public void testConnectToService_sessionService() throws InterruptedException {
+ prepareLooper();
+ testConnectToService(MockMediaSessionService2.ID);
+ }
+
+ @Test
+ public void testConnectToService_libraryService() throws InterruptedException {
+ prepareLooper();
+ testConnectToService(MockMediaLibraryService2.ID);
+ }
+
+ public void testConnectToService(String id) throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaLibrarySessionCallback sessionCallback = new MediaLibrarySessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ if (mSession != null) {
+ mSession.close();
+ }
+ mSession = session;
+ mPlayer = (MockPlayer) session.getPlayer();
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertFalse(controller.isTrusted());
+ latch.countDown();
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+ TestServiceRegistry.getInstance().setSessionCallback(sessionCallback);
+
+ mController = createController(TestUtils.getServiceToken(mContext, id));
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ // Test command from controller to session service
+ // TODO: Re enable when transport control works
+ /*
+ mController.play();
+ assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mPlayCalled);
+ */
+
+ // Test command from session service to controller
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ mController.registerPlayerEventCallback((state) -> {
+ assertNotNull(state);
+ assertEquals(PlaybackState.STATE_REWINDING, state.getState());
+ latch.countDown();
+ }, sHandler);
+ mPlayer.notifyPlaybackState(
+ TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ @Test
+ public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+ prepareLooper();
+ testControllerAfterSessionIsClosed(mSession.getToken().getId());
+ }
+
+ // TODO(jaewan): Re-enable this test
+ @Ignore
+ @Test
+ public void testControllerAfterSessionIsClosed_sessionService() throws InterruptedException {
+ prepareLooper();
+ /*
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testControllerAfterSessionIsClosed(MockMediaSessionService2.ID);
+ */
+ }
+
+ @Test
+ public void testSubscribeRouteInfo() throws InterruptedException {
+ prepareLooper();
+ final TestSessionCallback callback = new TestSessionCallback() {
+ @Override
+ public void onSubscribeRoutesInfo(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ mLatch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+
+ callback.resetLatchCount(1);
+ controller.subscribeRoutesInfo();
+ assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ callback.resetLatchCount(1);
+ controller.unsubscribeRoutesInfo();
+ assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSelectRouteInfo() throws InterruptedException {
+ prepareLooper();
+ final Bundle testRoute = new Bundle();
+ testRoute.putString("id", "testRoute");
+ final TestSessionCallback callback = new TestSessionCallback() {
+ @Override
+ public void onSelectRoute(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Bundle route) {
+ assertEquals(mContext.getPackageName(), controller.getPackageName());
+ assertTrue(TestUtils.equals(route, testRoute));
+ mLatch.countDown();
+ }
+ };
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).setId(TAG).build();
+ final MediaController2 controller = createController(mSession.getToken());
+
+ callback.resetLatchCount(1);
+ controller.selectRoute(testRoute);
+ assertTrue(callback.mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testClose_beforeConnected() throws InterruptedException {
+ prepareLooper();
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ controller.close();
+ }
+
+ @Test
+ public void testClose_twice() {
+ prepareLooper();
+ mController.close();
+ mController.close();
+ }
+
+ @Test
+ public void testClose_session() throws InterruptedException {
+ prepareLooper();
+ final String id = mSession.getToken().getId();
+ mController.close();
+ // close is done immediately for session.
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsClosed(id);
+ }
+
+ @Ignore
+ @Test
+ public void testClose_sessionService() throws InterruptedException {
+ prepareLooper();
+ testCloseFromService(MockMediaSessionService2.ID);
+ }
+
+ @Ignore
+ @Test
+ public void testClose_libraryService() throws InterruptedException {
+ prepareLooper();
+ testCloseFromService(MockMediaLibraryService2.ID);
+ }
+
+ private void testCloseFromService(String id) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ TestServiceRegistry.getInstance().setSessionServiceCallback(new SessionServiceCallback() {
+ @Override
+ public void onCreated() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDestroyed() {
+ latch.countDown();
+ }
+ });
+ mController = createController(TestUtils.getServiceToken(mContext, id));
+ mController.close();
+ // Wait until close triggers onDestroy() of the session service.
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsClosed(id);
+ }
+
+ private void testControllerAfterSessionIsClosed(final String id) throws InterruptedException {
+ // This cause session service to be died.
+ mSession.close();
+ waitForDisconnect(mController, true);
+ testNoInteraction();
+
+ // Ensure that the controller cannot use newly create session with the same ID.
+ // Recreated session has different session stub, so previously created controller
+ // shouldn't be available.
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {})
+ .setId(id).build();
+ testNoInteraction();
+ }
+
+ // Test that mSession and mController doesn't interact.
+ // Note that this method can be called after the mSession is died, so mSession may not have
+ // valid player.
+ private void testNoInteraction() throws InterruptedException {
+ // TODO: check that calls from the controller to session shouldn't be delivered.
+
+ // Calls from the session to controller shouldn't be delivered.
+ final CountDownLatch latch = new CountDownLatch(1);
+ setRunnableForOnCustomCommand(mController, new Runnable() {
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+ });
+ SessionCommand2 customCommand = new SessionCommand2("testNoInteraction", null);
+ mSession.sendCustomCommand(customCommand, null);
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ setRunnableForOnCustomCommand(mController, null);
+ }
+
+ // TODO(jaewan): Add test for service connect rejection, when we differentiate session
+ // active/inactive and connection accept/refuse
+
+ class TestVolumeProvider extends VolumeProviderCompat {
+ final CountDownLatch mLatch = new CountDownLatch(1);
+ boolean mSetVolumeToCalled;
+ boolean mAdjustVolumeCalled;
+ int mVolume;
+ int mDirection;
+
+ TestVolumeProvider(int controlType, int maxVolume, int currentVolume) {
+ super(controlType, maxVolume, currentVolume);
+ }
+
+ @Override
+ public void onSetVolumeTo(int volume) {
+ mSetVolumeToCalled = true;
+ mVolume = volume;
+ mLatch.countDown();
+ }
+
+ @Override
+ public void onAdjustVolume(int direction) {
+ mAdjustVolumeCalled = true;
+ mDirection = direction;
+ mLatch.countDown();
+ }
+ }
+
+ class TestSessionCallback extends SessionCallback {
+ CountDownLatch mLatch;
+
+ void resetLatchCount(int count) {
+ mLatch = new CountDownLatch(count);
+ }
+ }
+}
diff --git a/androidx/media/MediaInterface2.java b/androidx/media/MediaInterface2.java
new file mode 100644
index 00000000..93047a1a
--- /dev/null
+++ b/androidx/media/MediaInterface2.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import java.util.List;
+
+class MediaInterface2 {
+ private MediaInterface2() {
+ }
+
+ // TODO: relocate methods among different interfaces and classes.
+ interface SessionPlaybackControl {
+ void prepare();
+ void play();
+ void pause();
+ void reset();
+
+ void seekTo(long pos);
+
+ int getPlayerState();
+ long getCurrentPosition();
+ long getDuration();
+
+ long getBufferedPosition();
+ int getBufferingState();
+
+ float getPlaybackSpeed();
+ void setPlaybackSpeed(float speed);
+ }
+
+ interface SessionPlaylistControl {
+ void setOnDataSourceMissingHelper(MediaSession2.OnDataSourceMissingHelper helper);
+ void clearOnDataSourceMissingHelper();
+
+ List<MediaItem2> getPlaylist();
+ MediaMetadata2 getPlaylistMetadata();
+ void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata);
+ void updatePlaylistMetadata(MediaMetadata2 metadata);
+
+ MediaItem2 getCurrentMediaItem();
+ void skipToPlaylistItem(MediaItem2 item);
+ void skipToPreviousItem();
+ void skipToNextItem();
+
+ void addPlaylistItem(int index, MediaItem2 item);
+ void removePlaylistItem(MediaItem2 item);
+ void replacePlaylistItem(int index, MediaItem2 item);
+
+ int getRepeatMode();
+ void setRepeatMode(int repeatMode);
+ int getShuffleMode();
+ void setShuffleMode(int shuffleMode);
+ }
+
+ // Common interface for session2 and controller2
+ // TODO: consider to add fastForward, rewind.
+ abstract static class SessionPlayer implements SessionPlaybackControl, SessionPlaylistControl {
+ abstract void skipForward();
+ abstract void skipBackward();
+ abstract void notifyError(@MediaSession2.ErrorCode int errorCode, @Nullable Bundle extras);
+ }
+}
diff --git a/androidx/media/MediaItem2.java b/androidx/media/MediaItem2.java
new file mode 100644
index 00000000..b8c44c18
--- /dev/null
+++ b/androidx/media/MediaItem2.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.UUID;
+
+/**
+ * A class with information on a single media item with the metadata information.
+ * Media item are application dependent so we cannot guarantee that they contain the right values.
+ * <p>
+ * When it's sent to a controller or browser, it's anonymized and data descriptor wouldn't be sent.
+ * <p>
+ * This object isn't a thread safe.
+ */
+public class MediaItem2 {
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+ public @interface Flags { }
+
+ /**
+ * Flag: Indicates that the item has children of its own.
+ */
+ public static final int FLAG_BROWSABLE = 1 << 0;
+
+ /**
+ * Flag: Indicates that the item is playable.
+ * <p>
+ * The id of this item may be passed to
+ * {@link MediaController2#playFromMediaId(String, Bundle)}
+ */
+ public static final int FLAG_PLAYABLE = 1 << 1;
+
+ private static final String KEY_ID = "android.media.mediaitem2.id";
+ private static final String KEY_FLAGS = "android.media.mediaitem2.flags";
+ private static final String KEY_METADATA = "android.media.mediaitem2.metadata";
+ private static final String KEY_UUID = "android.media.mediaitem2.uuid";
+
+ private final String mId;
+ private final int mFlags;
+ private final UUID mUUID;
+ private MediaMetadata2 mMetadata;
+ private DataSourceDesc mDataSourceDesc;
+
+ private MediaItem2(@NonNull String mediaId, @Nullable DataSourceDesc dsd,
+ @Nullable MediaMetadata2 metadata, @Flags int flags) {
+ this(mediaId, dsd, metadata, flags, null);
+ }
+
+ private MediaItem2(@NonNull String mediaId, @Nullable DataSourceDesc dsd,
+ @Nullable MediaMetadata2 metadata, @Flags int flags, @Nullable UUID uuid) {
+ if (mediaId == null) {
+ throw new IllegalArgumentException("mediaId shouldn't be null");
+ }
+ if (metadata != null && !TextUtils.equals(mediaId, metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata's id should be matched with the mediaid");
+ }
+
+ mId = mediaId;
+ mDataSourceDesc = dsd;
+ mMetadata = metadata;
+ mFlags = flags;
+ mUUID = (uuid == null) ? UUID.randomUUID() : uuid;
+ }
+ /**
+ * Return this object as a bundle to share between processes.
+ *
+ * @return a new bundle instance
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_FLAGS, mFlags);
+ if (mMetadata != null) {
+ bundle.putBundle(KEY_METADATA, mMetadata.toBundle());
+ }
+ bundle.putString(KEY_UUID, mUUID.toString());
+ return bundle;
+ }
+
+ /**
+ * Create a MediaItem2 from the {@link Bundle}.
+ *
+ * @param bundle The bundle which was published by {@link MediaItem2#toBundle()}.
+ * @return The newly created MediaItem2
+ */
+ public static MediaItem2 fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final String uuidString = bundle.getString(KEY_UUID);
+ return fromBundle(bundle, UUID.fromString(uuidString));
+ }
+
+ /**
+ * Create a MediaItem2 from the {@link Bundle} with the specified {@link UUID}.
+ * If {@link UUID}
+ * can be null for creating new.
+ *
+ * @param bundle The bundle which was published by {@link MediaItem2#toBundle()}.
+ * @param uuid A {@link UUID} to override. Can be {@link null} for override.
+ * @return The newly created MediaItem2
+ */
+ static MediaItem2 fromBundle(@NonNull Bundle bundle, @Nullable UUID uuid) {
+ if (bundle == null) {
+ return null;
+ }
+ final String id = bundle.getString(KEY_ID);
+ final Bundle metadataBundle = bundle.getBundle(KEY_METADATA);
+ final MediaMetadata2 metadata = metadataBundle != null
+ ? MediaMetadata2.fromBundle(metadataBundle) : null;
+ final int flags = bundle.getInt(KEY_FLAGS);
+ return new MediaItem2(id, null, metadata, flags, uuid);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("MediaItem2{");
+ sb.append("mFlags=").append(mFlags);
+ sb.append(", mMetadata=").append(mMetadata);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * Gets the flags of the item.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether this item is browsable.
+ * @see #FLAG_BROWSABLE
+ */
+ public boolean isBrowsable() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ /**
+ * Returns whether this item is playable.
+ * @see #FLAG_PLAYABLE
+ */
+ public boolean isPlayable() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ /**
+ * Set a metadata. If the metadata is not null, its id should be matched with this instance's
+ * media id.
+ *
+ * @param metadata metadata to update
+ */
+ public void setMetadata(@Nullable MediaMetadata2 metadata) {
+ if (metadata != null && !TextUtils.equals(mId, metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata's id should be matched with the mediaId");
+ }
+ mMetadata = metadata;
+ }
+
+ /**
+ * Returns the metadata of the media.
+ */
+ public @Nullable MediaMetadata2 getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Returns the media id for this item.
+ */
+ public /*@NonNull*/ String getMediaId() {
+ return mId;
+ }
+
+ /**
+ * Return the {@link DataSourceDesc}
+ * <p>
+ * Can be {@code null} if the MediaItem2 came from another process and anonymized
+ *
+ * @return data source descriptor
+ */
+ public @Nullable DataSourceDesc getDataSourceDesc() {
+ return mDataSourceDesc;
+ }
+
+ @Override
+ public int hashCode() {
+ return mUUID.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof MediaItem2)) {
+ return false;
+ }
+ MediaItem2 other = (MediaItem2) obj;
+ return mUUID.equals(other.mUUID);
+ }
+
+ /**
+ * Build {@link MediaItem2}
+ */
+ public static final class Builder {
+ private @Flags int mFlags;
+ private String mMediaId;
+ private MediaMetadata2 mMetadata;
+ private DataSourceDesc mDataSourceDesc;
+
+ /**
+ * Constructor for {@link Builder}
+ *
+ * @param flags
+ */
+ public Builder(@Flags int flags) {
+ mFlags = flags;
+ }
+
+ /**
+ * Set the media id of this instance. {@code null} for unset.
+ * <p>
+ * Media id is used to identify a media contents between session and controller.
+ * <p>
+ * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+ * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+ * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+ * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+ *
+ * @param mediaId media id
+ * @return this instance for chaining
+ */
+ public Builder setMediaId(@Nullable String mediaId) {
+ mMediaId = mediaId;
+ return this;
+ }
+
+ /**
+ * Set the metadata of this instance. {@code null} for unset.
+ * <p>
+ * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+ * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+ * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+ * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+ *
+ * @param metadata metadata
+ * @return this instance for chaining
+ */
+ public Builder setMetadata(@Nullable MediaMetadata2 metadata) {
+ mMetadata = metadata;
+ return this;
+ }
+
+ /**
+ * Set the data source descriptor for this instance. {@code null} for unset.
+ *
+ * @param dataSourceDesc data source descriptor
+ * @return this instance for chaining
+ */
+ public Builder setDataSourceDesc(@Nullable DataSourceDesc dataSourceDesc) {
+ mDataSourceDesc = dataSourceDesc;
+ return this;
+ }
+
+ /**
+ * Build {@link MediaItem2}.
+ *
+ * @return a new {@link MediaItem2}.
+ */
+ public MediaItem2 build() {
+ String id = (mMetadata != null)
+ ? mMetadata.getString(MediaMetadata2.METADATA_KEY_MEDIA_ID) : null;
+ if (id == null) {
+ id = (mMediaId != null) ? mMediaId : toString();
+ }
+ return new MediaItem2(id, mDataSourceDesc, mMetadata, mFlags);
+ }
+ }
+}
diff --git a/androidx/media/MediaLibraryService2.java b/androidx/media/MediaLibraryService2.java
new file mode 100644
index 00000000..edd97c3a
--- /dev/null
+++ b/androidx/media/MediaLibraryService2.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE;
+import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.Builder;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ * Base class for media library services.
+ * <p>
+ * Media library services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession2}.
+ * <p>
+ * When extending this class, also add the following to your {@code AndroidManifest.xml}.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaLibraryService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * The {@link MediaLibraryService2} class derives from {@link MediaSessionService2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ *
+ * @see MediaSessionService2
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MediaLibraryService2 extends MediaSessionService2 {
+ /**
+ * This is the interface name that a service implementing a session service should say that it
+ * support -- that is, this is the action it uses for its intent filter.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
+
+ // TODO: Revisit this value.
+
+ /**
+ * Session for the {@link MediaLibraryService2}. Build this object with
+ * {@link Builder} and return in {@link #onCreateSession(String)}.
+ */
+ public static final class MediaLibrarySession extends MediaSession2 {
+ /**
+ * Callback for the {@link MediaLibrarySession}.
+ */
+ public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
+ /**
+ * Called to get the root information for browsing by a particular client.
+ * <p>
+ * The implementation should verify that the client package has permission
+ * to access browse media information before returning the root id; it
+ * should return null if the client is not allowed to access this
+ * information.
+ * <p>
+ * Note: this callback may be called on the main thread, regardless of the callback
+ * executor.
+ *
+ * @param session the session for this event
+ * @param controllerInfo information of the controller requesting access to browse
+ * media.
+ * @param extras An optional bundle of service-specific arguments to send
+ * to the media library service when connecting and retrieving the
+ * root id for browsing, or null if none. The contents of this
+ * bundle may affect the information returned when browsing.
+ * @return The {@link LibraryRoot} for accessing this app's content or null.
+ * @see LibraryRoot#EXTRA_RECENT
+ * @see LibraryRoot#EXTRA_OFFLINE
+ * @see LibraryRoot#EXTRA_SUGGESTED
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT
+ */
+ public @Nullable LibraryRoot onGetLibraryRoot(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controllerInfo, @Nullable Bundle extras) {
+ return null;
+ }
+
+ /**
+ * Called to get an item. Return result here for the browser.
+ * <p>
+ * Return {@code null} for no result or error.
+ *
+ * @param session the session for this event
+ * @param mediaId item id to get media item.
+ * @return a media item. {@code null} for no result or error.
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_ITEM
+ */
+ public @Nullable MediaItem2 onGetItem(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controllerInfo, @NonNull String mediaId) {
+ return null;
+ }
+
+ /**
+ * Called to get children of given parent id. Return the children here for the browser.
+ * <p>
+ * Return an empty list for no children, and return {@code null} for the error.
+ *
+ * @param session the session for this event
+ * @param parentId parent id to get children
+ * @param page number of page
+ * @param pageSize size of the page
+ * @param extras extra bundle
+ * @return list of children. Can be {@code null}.
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_CHILDREN
+ */
+ public @Nullable List<MediaItem2> onGetChildren(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId, int page,
+ int pageSize, @Nullable Bundle extras) {
+ return null;
+ }
+
+ /**
+ * Called when a controller subscribes to the parent.
+ * <p>
+ * It's your responsibility to keep subscriptions by your own and call
+ * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)}
+ * when the parent is changed.
+ *
+ * @param session the session for this event
+ * @param controller controller
+ * @param parentId parent id
+ * @param extras extra bundle
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_SUBSCRIBE
+ */
+ public void onSubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId,
+ @Nullable Bundle extras) {
+ }
+
+ /**
+ * Called when a controller unsubscribes to the parent.
+ *
+ * @param session the session for this event
+ * @param controller controller
+ * @param parentId parent id
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_UNSUBSCRIBE
+ */
+ // TODO: Make this to be called.
+ public void onUnsubscribe(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controller, @NonNull String parentId) {
+ }
+
+ /**
+ * Called when a controller requests search.
+ *
+ * @param session the session for this event
+ * @param query The search query sent from the media browser. It contains keywords
+ * separated by space.
+ * @param extras The bundle of service-specific arguments sent from the media browser.
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_SEARCH
+ */
+ public void onSearch(@NonNull MediaLibrarySession session,
+ @NonNull ControllerInfo controllerInfo, @NonNull String query,
+ @Nullable Bundle extras) {
+ }
+
+ /**
+ * Called to get the search result. Return search result here for the browser which has
+ * requested search previously.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param session the session for this event
+ * @param controllerInfo Information of the controller requesting the search result.
+ * @param query The search query which was previously sent through
+ * {@link #onSearch(MediaLibrarySession, ControllerInfo, String, Bundle)}.
+ * @param page page number. Starts from {@code 1}.
+ * @param pageSize page size. Should be greater or equal to {@code 1}.
+ * @param extras The bundle of service-specific arguments sent from the media browser.
+ * @return search result. {@code null} for error.
+ * @see SessionCommand2#COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT
+ */
+ public @Nullable List<MediaItem2> onGetSearchResult(
+ @NonNull MediaLibrarySession session, @NonNull ControllerInfo controllerInfo,
+ @NonNull String query, int page, int pageSize, @Nullable Bundle extras) {
+ return null;
+ }
+ }
+
+ /**
+ * Builder for {@link MediaLibrarySession}.
+ */
+ // Override all methods just to show them with the type instead of generics in Javadoc.
+ // This workarounds javadoc issue described in the MediaSession2.BuilderBase.
+ public static final class Builder extends MediaSession2.BuilderBase<MediaLibrarySession,
+ Builder, MediaLibrarySessionCallback> {
+ private MediaLibrarySessionImplBase.Builder mImpl;
+
+ // Builder requires MediaLibraryService2 instead of Context just to ensure that the
+ // builder can be only instantiated within the MediaLibraryService2.
+ // Ideally it's better to make it inner class of service to enforce, it violates API
+ // guideline that Builders should be the inner class of the building target.
+ public Builder(@NonNull MediaLibraryService2 service,
+ @NonNull Executor callbackExecutor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ super(service);
+ mImpl = new MediaLibrarySessionImplBase.Builder(service);
+ setImpl(mImpl);
+ setSessionCallback(callbackExecutor, callback);
+ }
+
+ @Override
+ public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+ return super.setPlayer(player);
+ }
+
+ @Override
+ public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ return super.setPlaylistAgent(playlistAgent);
+ }
+
+ @Override
+ public @NonNull Builder setVolumeProvider(
+ @Nullable VolumeProviderCompat volumeProvider) {
+ return super.setVolumeProvider(volumeProvider);
+ }
+
+ @Override
+ public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) {
+ return super.setSessionActivity(pi);
+ }
+
+ @Override
+ public @NonNull Builder setId(@NonNull String id) {
+ return super.setId(id);
+ }
+
+ @Override
+ public @NonNull Builder setSessionCallback(@NonNull Executor executor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ return super.setSessionCallback(executor, callback);
+ }
+
+ @Override
+ public @NonNull MediaLibrarySession build() {
+ return super.build();
+ }
+ }
+
+ MediaLibrarySession(SupportLibraryImpl impl) {
+ super(impl);
+ }
+
+ /**
+ * Notify the controller of the change in a parent's children.
+ * <p>
+ * If the controller hasn't subscribed to the parent, the API will do nothing.
+ * <p>
+ * Controllers will use {@link MediaBrowser2#getChildren(String, int, int, Bundle)} to get
+ * the list of children.
+ *
+ * @param controller controller to notify
+ * @param parentId parent id with changes in its children
+ * @param itemCount number of children.
+ * @param extras extra information from session to controller
+ */
+ public void notifyChildrenChanged(@NonNull ControllerInfo controller,
+ @NonNull String parentId, int itemCount, @Nullable Bundle extras) {
+ Bundle options = new Bundle(extras);
+ options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
+ options.putBundle(MediaBrowser2.EXTRA_TARGET, controller.toBundle());
+ }
+
+ /**
+ * Notify all controllers that subscribed to the parent about change in the parent's
+ * children, regardless of the extra bundle supplied by
+ * {@link MediaBrowser2#subscribe(String, Bundle)}.
+ *
+ * @param parentId parent id
+ * @param itemCount number of children
+ * @param extras extra information from session to controller
+ */
+ // This is for the backward compatibility.
+ public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
+ @Nullable Bundle extras) {
+ Bundle options = new Bundle(extras);
+ options.putInt(MediaBrowser2.EXTRA_ITEM_COUNT, itemCount);
+ getServiceCompat().notifyChildrenChanged(parentId, options);
+ }
+
+ /**
+ * Notify controller about change in the search result.
+ *
+ * @param controller controller to notify
+ * @param query previously sent search query from the controller.
+ * @param itemCount the number of items that have been found in the search.
+ * @param extras extra bundle
+ */
+ public void notifySearchResultChanged(@NonNull ControllerInfo controller,
+ @NonNull String query, int itemCount, @NonNull Bundle extras) {
+ // TODO: Implement
+ }
+
+ private MediaLibraryService2 getService() {
+ return (MediaLibraryService2) getContext();
+ }
+
+ private MediaBrowserServiceCompat getServiceCompat() {
+ return getService().getServiceCompat();
+ }
+
+ @Override
+ MediaLibrarySessionCallback getCallback() {
+ return (MediaLibrarySessionCallback) super.getCallback();
+ }
+ }
+
+ @Override
+ MediaBrowserServiceCompat createBrowserServiceCompat() {
+ return new MyBrowserService();
+ }
+
+ @Override
+ int getSessionType() {
+ return SessionToken2.TYPE_LIBRARY_SERVICE;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ MediaSession2 session = getSession();
+ if (!(session instanceof MediaLibrarySession)) {
+ throw new RuntimeException("Expected MediaLibrarySession, but returned MediaSession2");
+ }
+ }
+
+ private MediaLibrarySession getLibrarySession() {
+ return (MediaLibrarySession) getSession();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return super.onBind(intent);
+ }
+
+ /**
+ * Called when another app requested to start this service.
+ * <p>
+ * Library service will accept or reject the connection with the
+ * {@link MediaLibrarySessionCallback} in the created session.
+ * <p>
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ * <p>
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new library session
+ * @see Builder
+ * @see #getSession()
+ * @throws RuntimeException if returned session is invalid
+ */
+ @Override
+ public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
+
+ /**
+ * Contains information that the library service needs to send to the client when
+ * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called.
+ */
+ public static final class LibraryRoot {
+ /**
+ * The lookup key for a boolean that indicates whether the library service should return a
+ * librar root for recently played media items.
+ *
+ * <p>When creating a media browser for a given media library service, this key can be
+ * supplied as a root hint for retrieving media items that are recently played.
+ * If the media library service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetLibraryRoot}
+ * is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_OFFLINE
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_RECENT = "android.media.extra.RECENT";
+
+ /**
+ * The lookup key for a boolean that indicates whether the library service should return a
+ * library root for offline media items.
+ *
+ * <p>When creating a media browser for a given media library service, this key can be
+ * supplied as a root hint for retrieving media items that are can be played without an
+ * internet connection.
+ * If the media library service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetLibraryRoot}
+ * is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE";
+
+ /**
+ * The lookup key for a boolean that indicates whether the library service should return a
+ * library root for suggested media items.
+ *
+ * <p>When creating a media browser for a given media library service, this key can be
+ * supplied as a root hint for retrieving the media items suggested by the media library
+ * service. The list of media items is considered ordered by relevance, first being the top
+ * suggestion.
+ * If the media library service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetLibraryRoot}
+ * is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_OFFLINE
+ */
+ public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED";
+
+ private final String mRootId;
+ private final Bundle mExtras;
+
+ //private final LibraryRootProvider mProvider;
+
+ /**
+ * Constructs a library root.
+ * @param rootId The root id for browsing.
+ * @param extras Any extras about the library service.
+ */
+ public LibraryRoot(@NonNull String rootId, @Nullable Bundle extras) {
+ if (rootId == null) {
+ throw new IllegalArgumentException("rootId shouldn't be null");
+ }
+ mRootId = rootId;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the root id for browsing.
+ */
+ public String getRootId() {
+ return mRootId;
+ }
+
+ /**
+ * Gets any extras about the library service.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+ }
+
+ private class MyBrowserService extends MediaBrowserServiceCompat {
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
+ final Bundle extras) {
+ if (MediaUtils2.isDefaultLibraryRootHint(extras)) {
+ // For connection request from the MediaController2. accept the connection from
+ // here, and let MediaLibrarySession decide whether to accept or reject the
+ // controller.
+ return sDefaultBrowserRoot;
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ // TODO: Revisit this when we support caller information.
+ final ControllerInfo info = new ControllerInfo(MediaLibraryService2.this, clientUid, -1,
+ clientPackageName, null);
+ MediaLibrarySession session = getLibrarySession();
+ // Call onGetLibraryRoot() directly instead of execute on the executor. Here's the
+ // reason.
+ // We need to return browser root here. So if we run the callback on the executor, we
+ // should wait for the completion.
+ // However, we cannot wait if the callback executor is the main executor, which posts
+ // the runnable to the main thread's. In that case, since this onGetRoot() always runs
+ // on the main thread, the posted runnable for calling onGetLibraryRoot() wouldn't run
+ // in here. Even worse, we cannot know whether it would be run on the main thread or
+ // not.
+ // Because of the reason, just call onGetLibraryRoot directly here. onGetLibraryRoot()
+ // has documentation that it may be called on the main thread.
+ LibraryRoot libraryRoot = session.getCallback().onGetLibraryRoot(
+ session, info, extras);
+ if (libraryRoot == null) {
+ return null;
+ }
+ return new BrowserRoot(libraryRoot.getRootId(), libraryRoot.getExtras());
+ }
+
+ @Override
+ public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
+ onLoadChildren(parentId, result, null);
+ }
+
+ @Override
+ public void onLoadChildren(final String parentId, final Result<List<MediaItem>> result,
+ final Bundle options) {
+ final ControllerInfo controller = getController();
+ getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ int page = options.getInt(EXTRA_PAGE, -1);
+ int pageSize = options.getInt(EXTRA_PAGE_SIZE, -1);
+ if (page >= 0 && pageSize >= 0) {
+ // Requesting the list of children through the pagenation.
+ List<MediaItem2> children = getLibrarySession().getCallback().onGetChildren(
+ getLibrarySession(), controller, parentId, page, pageSize, options);
+ if (children == null) {
+ result.sendError(null);
+ } else {
+ List<MediaItem> list = new ArrayList<>();
+ for (int i = 0; i < children.size(); i++) {
+ list.add(MediaUtils2.createMediaItem(children.get(i)));
+ }
+ result.sendResult(list);
+ }
+ } else {
+ // Only wants to register callbacks
+ getLibrarySession().getCallback().onSubscribe(getLibrarySession(),
+ controller, parentId, options);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onLoadItem(final String itemId, final Result<MediaItem> result) {
+ final ControllerInfo controller = getController();
+ getLibrarySession().getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ MediaItem2 item = getLibrarySession().getCallback().onGetItem(
+ getLibrarySession(), controller, itemId);
+ if (item == null) {
+ result.sendError(null);
+ } else {
+ result.sendResult(MediaUtils2.createMediaItem(item));
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+ // TODO: Implement
+ }
+
+ @Override
+ public void onCustomAction(String action, Bundle extras, Result<Bundle> result) {
+ // TODO: Implement
+ }
+
+ private ControllerInfo getController() {
+ // TODO: Implement, by using getBrowserRootHints() / getCurrentBrowserInfo() / ...
+ return null;
+ }
+ }
+}
diff --git a/androidx/media/MediaLibrarySessionImplBase.java b/androidx/media/MediaLibrarySessionImplBase.java
new file mode 100644
index 00000000..c21edd34
--- /dev/null
+++ b/androidx/media/MediaLibrarySessionImplBase.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import androidx.annotation.NonNull;
+import androidx.media.MediaLibraryService2.MediaLibrarySession;
+
+import java.util.concurrent.Executor;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaLibrarySessionImplBase extends MediaSession2ImplBase {
+ MediaLibrarySessionImplBase(Context context,
+ MediaSessionCompat sessionCompat, String id,
+ MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+ VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
+ Executor callbackExecutor,
+ MediaSession2.SessionCallback callback) {
+ super(context, sessionCompat, id, player, playlistAgent, volumeProvider, sessionActivity,
+ callbackExecutor, callback);
+ }
+
+ static final class Builder extends MediaSession2ImplBase.BuilderBase<
+ MediaLibrarySession, MediaLibrarySession.MediaLibrarySessionCallback> {
+ Builder(Context context) {
+ super(context);
+ }
+
+ @Override
+ public @NonNull MediaLibrarySession build() {
+ if (mCallbackExecutor == null) {
+ mCallbackExecutor = new MainHandlerExecutor(mContext);
+ }
+ if (mCallback == null) {
+ mCallback = new MediaLibrarySession.MediaLibrarySessionCallback() {};
+ }
+ return new MediaLibrarySession(new MediaLibrarySessionImplBase(mContext,
+ new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent,
+ mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback));
+ }
+ }
+}
diff --git a/androidx/media/MediaMetadata2.java b/androidx/media/MediaMetadata2.java
new file mode 100644
index 00000000..0cfd2375
--- /dev/null
+++ b/androidx/media/MediaMetadata2.java
@@ -0,0 +1,1097 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringDef;
+import androidx.collection.ArrayMap;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ */
+// New version of MediaMetadata with following changes
+// - Don't implement Parcelable for updatable support.
+// - Also support MediaDescription features. MediaDescription is deprecated instead because
+// it was insufficient for controller to display media contents.
+public final class MediaMetadata2 {
+ private static final String TAG = "MediaMetadata2";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the title of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the artist of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * duration of the media in ms. A negative duration indicates that the duration is unknown
+ * (or infinite).
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the album title for the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the author of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the writer of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the composer of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the compilation status of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the date the media was created or published.
+ * The format is unspecified but RFC 3339 is recommended.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the year
+ * the media was created or published.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the genre of the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * track number for the media.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * number of tracks in the media's original source.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * disc number for the media's original source.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the artist for the album of the media's original source.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+ /**
+ * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+ * artwork for the media.
+ * The artwork should be relatively small and may be scaled down if it is too large.
+ * For higher resolution artwork, {@link #METADATA_KEY_ART_URI} should be used instead.
+ *
+ * @see Builder#putBitmap(String, Bitmap)
+ * @see #getBitmap(String)
+ */
+ public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about Uri of the artwork for the media.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+ /**
+ * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+ * artwork for the album of the media's original source.
+ * The artwork should be relatively small and may be scaled down if it is too large.
+ * For higher resolution artwork, {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead.
+ *
+ * @see Builder#putBitmap(String, Bitmap)
+ * @see #getBitmap(String)
+ */
+ public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the Uri of the artwork for the album of the media's original source.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+ /**
+ * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+ * user's rating for the media.
+ *
+ * @see Builder#putRating(String, Rating2)
+ * @see #getRating(String)
+ */
+ public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+ /**
+ * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+ * overall rating for the media.
+ *
+ * @see Builder#putRating(String, Rating2)
+ * @see #getRating(String)
+ */
+ public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the title that is suitable for display to the user.
+ * It 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_DISPLAY_SUBTITLE =
+ "android.media.metadata.DISPLAY_SUBTITLE";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_DISPLAY_DESCRIPTION =
+ "android.media.metadata.DISPLAY_DESCRIPTION";
+
+ /**
+ * The metadata key for a {@link Bitmap} typed value to retrieve the information about the 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.
+ * <p>
+ * The icon should be relatively small and may be scaled down if it is too large.
+ * For higher resolution artwork, {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead.
+ *
+ * @see Builder#putBitmap(String, Bitmap)
+ * @see #getBitmap(String)
+ */
+ public static final String METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the Uri of 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.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_DISPLAY_ICON_URI =
+ "android.media.metadata.DISPLAY_ICON_URI";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the media ID of 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. It may be used with
+ * {@link MediaController2#playFromMediaId(String, Bundle)}
+ * to initiate playback.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
+
+ /**
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the Uri of the content. This value is specific to the service providing the
+ * content. It may be used with {@link MediaController2#playFromUri(Uri, Bundle)}
+ * to initiate playback.
+ *
+ * @see Builder#putText(String, CharSequence)
+ * @see Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
+
+ /**
+ * @hide
+ * The metadata key for a {@link Float} typed value to retrieve the information about the
+ * radio frequency if this metadata represents radio content.
+ *
+ * @see Builder#putFloat(String, float)
+ * @see #getFloat(String)
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final String METADATA_KEY_RADIO_FREQUENCY =
+ "android.media.metadata.RADIO_FREQUENCY";
+
+ /**
+ * @hide
+ * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+ * information about the radio program name if this metadata represents radio content.
+ *
+ * @see MediaMetadata2.Builder#putText(String, CharSequence)
+ * @see MediaMetadata2.Builder#putString(String, String)
+ * @see #getText(String)
+ * @see #getString(String)
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final String METADATA_KEY_RADIO_PROGRAM_NAME =
+ "android.media.metadata.RADIO_PROGRAM_NAME";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
+ * AVRCP 1.5. It should be one of the following:
+ * <ul>
+ * <li>{@link #BT_FOLDER_TYPE_MIXED}</li>
+ * <li>{@link #BT_FOLDER_TYPE_TITLES}</li>
+ * <li>{@link #BT_FOLDER_TYPE_ALBUMS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_ARTISTS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_GENRES}</li>
+ * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
+ * </ul>
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_BT_FOLDER_TYPE =
+ "android.media.metadata.BT_FOLDER_TYPE";
+
+ /**
+ * The type of folder that is unknown or contains media elements of mixed types as specified in
+ * the section 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_MIXED = 0;
+
+ /**
+ * The type of folder that contains media elements only as specified in the section 6.10.2.2 of
+ * the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_TITLES = 1;
+
+ /**
+ * The type of folder that contains folders categorized by album as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_ALBUMS = 2;
+
+ /**
+ * The type of folder that contains folders categorized by artist as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_ARTISTS = 3;
+
+ /**
+ * The type of folder that contains folders categorized by genre as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_GENRES = 4;
+
+ /**
+ * The type of folder that contains folders categorized by playlist as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_PLAYLISTS = 5;
+
+ /**
+ * The type of folder that contains folders categorized by year as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_YEARS = 6;
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about whether
+ * the media is an advertisement. A value of 0 indicates it is not an advertisement.
+ * A value of 1 or non-zero indicates it is an advertisement.
+ * If not specified, this value is set to 0 by default.
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+
+ /**
+ * The metadata key for a {@link Long} typed value to retrieve the information about the
+ * download status of the media which will be used for later offline playback. It should be
+ * one of the following:
+ *
+ * <ul>
+ * <li>{@link #STATUS_NOT_DOWNLOADED}</li>
+ * <li>{@link #STATUS_DOWNLOADING}</li>
+ * <li>{@link #STATUS_DOWNLOADED}</li>
+ * </ul>
+ *
+ * @see Builder#putLong(String, long)
+ * @see #getLong(String)
+ */
+ public static final String METADATA_KEY_DOWNLOAD_STATUS =
+ "android.media.metadata.DOWNLOAD_STATUS";
+
+ /**
+ * The status value to indicate the media item is not downloaded.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_NOT_DOWNLOADED = 0;
+
+ /**
+ * The status value to indicate the media item is being downloaded.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_DOWNLOADING = 1;
+
+ /**
+ * The status value to indicate the media item is downloaded for later offline playback.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_DOWNLOADED = 2;
+
+ /**
+ * A {@link Bundle} extra.
+ */
+ public static final String METADATA_KEY_EXTRAS = "android.media.metadata.EXTRAS";
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @StringDef({METADATA_KEY_TITLE, METADATA_KEY_ARTIST, METADATA_KEY_ALBUM, METADATA_KEY_AUTHOR,
+ METADATA_KEY_WRITER, METADATA_KEY_COMPOSER, METADATA_KEY_COMPILATION,
+ METADATA_KEY_DATE, METADATA_KEY_GENRE, METADATA_KEY_ALBUM_ARTIST, METADATA_KEY_ART_URI,
+ METADATA_KEY_ALBUM_ART_URI, METADATA_KEY_DISPLAY_TITLE, METADATA_KEY_DISPLAY_SUBTITLE,
+ METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_KEY_DISPLAY_ICON_URI,
+ METADATA_KEY_MEDIA_ID, METADATA_KEY_MEDIA_URI, METADATA_KEY_RADIO_PROGRAM_NAME})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TextKey {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @StringDef({METADATA_KEY_DURATION, METADATA_KEY_YEAR, METADATA_KEY_TRACK_NUMBER,
+ METADATA_KEY_NUM_TRACKS, METADATA_KEY_DISC_NUMBER, METADATA_KEY_BT_FOLDER_TYPE,
+ METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LongKey {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BitmapKey {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RatingKey {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @StringDef({METADATA_KEY_RADIO_FREQUENCY})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FloatKey {}
+
+ static final int METADATA_TYPE_LONG = 0;
+ static final int METADATA_TYPE_TEXT = 1;
+ static final int METADATA_TYPE_BITMAP = 2;
+ static final int METADATA_TYPE_RATING = 3;
+ static final int METADATA_TYPE_FLOAT = 4;
+ static final ArrayMap<String, Integer> 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);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_RADIO_FREQUENCY, METADATA_TYPE_FLOAT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_RADIO_PROGRAM_NAME, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
+ }
+
+ private static final @MediaMetadata2.TextKey
+ 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 @MediaMetadata2.BitmapKey
+ String[] PREFERRED_BITMAP_ORDER = {
+ METADATA_KEY_DISPLAY_ICON,
+ METADATA_KEY_ART,
+ METADATA_KEY_ALBUM_ART
+ };
+
+ private static final @MediaMetadata2.TextKey
+ String[] PREFERRED_URI_ORDER = {
+ METADATA_KEY_DISPLAY_ICON_URI,
+ METADATA_KEY_ART_URI,
+ METADATA_KEY_ALBUM_ART_URI
+ };
+
+ final Bundle mBundle;
+
+ MediaMetadata2(Bundle bundle) {
+ mBundle = new Bundle(bundle);
+ mBundle.setClassLoader(MediaMetadata2.class.getClassLoader());
+ }
+
+ /**
+ * 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(@NonNull String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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 @Nullable CharSequence getText(@NonNull @TextKey String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ return mBundle.getCharSequence(key);
+ }
+
+ /**
+ * Returns the media id, or {@code null} if the id doesn't exist.
+ *<p>
+ * This is equivalent to the {@link #getString(String)} with the {@link #METADATA_KEY_MEDIA_ID}.
+ *
+ * @return media id. Can be {@code null}
+ * @see #METADATA_KEY_MEDIA_ID
+ */
+ public @Nullable String getMediaId() {
+ return getString(METADATA_KEY_MEDIA_ID);
+ }
+
+ /**
+ * 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 @Nullable String getString(@NonNull @TextKey String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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(@NonNull @LongKey String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ return mBundle.getLong(key, 0);
+ }
+
+ /**
+ * Return a {@link Rating2} for the given key or null if no rating exists for
+ * the given key.
+ * <p>
+ * For the {@link #METADATA_KEY_USER_RATING}, A {@code null} return value means that user rating
+ * cannot be set by {@link MediaController2}.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link Rating2} or {@code null}
+ */
+ public @Nullable Rating2 getRating(@NonNull @RatingKey String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ Rating2 rating = null;
+ try {
+ rating = Rating2.fromBundle(mBundle.getBundle(key));
+ } catch (Exception e) {
+ // ignore, value was not a rating
+ Log.w(TAG, "Failed to retrieve a key as Rating.", e);
+ }
+ return rating;
+ }
+
+ /**
+ * Return the value associated with the given key, or 0.0f if no long exists
+ * for the given key.
+ *
+ * @param key The key the value is stored under
+ * @return a float value
+ */
+ public float getFloat(@NonNull @FloatKey String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ return mBundle.getFloat(key);
+ }
+
+ /**
+ * 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 @Nullable Bitmap getBitmap(@NonNull @BitmapKey String key) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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;
+ }
+
+ /**
+ * Get the extra {@link Bundle} from the metadata object.
+ *
+ * @return A {@link Bundle} or {@code null}
+ */
+ public @Nullable Bundle getExtras() {
+ try {
+ return mBundle.getBundle(METADATA_KEY_EXTRAS);
+ } catch (Exception e) {
+ // ignore, value was not an bundle
+ Log.w(TAG, "Failed to retrieve an extra");
+ }
+ return null;
+ }
+
+ /**
+ * 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 @NonNull Set<String> 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 @NonNull Bundle toBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates the {@link MediaMetadata2} from the bundle that previously returned by
+ * {@link #toBundle()}.
+ *
+ * @param bundle bundle for the metadata
+ * @return a new MediaMetadata2x
+ */
+ public static @NonNull MediaMetadata2 fromBundle(@Nullable Bundle bundle) {
+ return (bundle == null) ? null : new MediaMetadata2(bundle);
+ }
+
+ /**
+ * Use to build MediaMetadata2x objects. The system defined metadata keys must
+ * use the appropriate data type.
+ */
+ public static final class Builder {
+ final Bundle mBundle;
+
+ /**
+ * Create an empty Builder. Any field that should be included in the
+ * {@link MediaMetadata2} must be added.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadata2} 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(@NonNull MediaMetadata2 source) {
+ mBundle = new Bundle(source.toBundle());
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadata2} instance to set
+ * initial values, but replace bitmaps with a scaled down copy if they
+ * are larger than maxBitmapSize.
+ *
+ * @param source The original metadata to copy.
+ * @param maxBitmapSize The maximum height/width for bitmaps contained
+ * in the metadata.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public Builder(MediaMetadata2 source, int maxBitmapSize) {
+ this(source);
+ for (String key : mBundle.keySet()) {
+ Object value = mBundle.get(key);
+ if (value instanceof Bitmap) {
+ Bitmap bmp = (Bitmap) value;
+ if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
+ putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
+ }
+ }
+ }
+ }
+
+ /**
+ * 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:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_COMPILATION}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+ * <li>{@link #METADATA_KEY_MEDIA_ID}</li>
+ * <li>{@link #METADATA_KEY_MEDIA_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The CharSequence value to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putText(@NonNull @TextKey String key,
+ @Nullable CharSequence value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_COMPILATION}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+ * <li>{@link #METADATA_KEY_MEDIA_ID}</li>
+ * <li>{@link #METADATA_KEY_MEDIA_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putString(@NonNull @TextKey String key,
+ @Nullable String value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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:
+ * <ul>
+ * <li>{@link #METADATA_KEY_DURATION}</li>
+ * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+ * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_YEAR}</li>
+ * <li>{@link #METADATA_KEY_BT_FOLDER_TYPE}</li>
+ * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li>
+ * <li>{@link #METADATA_KEY_DOWNLOAD_STATUS}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putLong(@NonNull @LongKey String key, long value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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 Rating2} 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:
+ * <ul>
+ * <li>{@link #METADATA_KEY_RATING}</li>
+ * <li>{@link #METADATA_KEY_USER_RATING}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putRating(@NonNull @RatingKey String key,
+ @Nullable Rating2 value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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.putBundle(key, (value == null) ? null : value.toBundle());
+ 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:
+ * <ul>
+ * <li>{@link #METADATA_KEY_ART}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON}</li>
+ * </ul>
+ * Large bitmaps may be scaled down by the system when
+ * {@link android.media.session.MediaSession#setMetadata} is called.
+ * To pass full resolution images {@link Uri Uris} should be used with
+ * {@link #putString}.
+ *
+ * @param key The key for referencing this value
+ * @param value The Bitmap to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putBitmap(@NonNull @BitmapKey String key,
+ @Nullable Bitmap value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ 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;
+ }
+
+ /**
+ * Put a float value into the metadata. Custom keys may be used.
+ *
+ * @param key The key for referencing this value
+ * @param value The float value to store
+ * @return The Builder to allow chaining
+ */
+ public @NonNull Builder putFloat(@NonNull @LongKey String key, float value) {
+ if (key == null) {
+ throw new IllegalArgumentException("key shouldn't be null");
+ }
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_FLOAT) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a float");
+ }
+ }
+ mBundle.putFloat(key, value);
+ return this;
+ }
+
+ /**
+ * Set a bundle of extras.
+ *
+ * @param extras The extras to include with this description or null.
+ * @return The Builder to allow chaining
+ */
+ public Builder setExtras(@Nullable Bundle extras) {
+ mBundle.putBundle(METADATA_KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Creates a {@link MediaMetadata2} instance with the specified fields.
+ *
+ * @return The new MediaMetadata2x instance
+ */
+ public @NonNull MediaMetadata2 build() {
+ return new MediaMetadata2(mBundle);
+ }
+
+ private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
+ float maxSizeF = maxSize;
+ float widthScale = maxSizeF / bmp.getWidth();
+ float heightScale = maxSizeF / bmp.getHeight();
+ float scale = Math.min(widthScale, heightScale);
+ int height = (int) (bmp.getHeight() * scale);
+ int width = (int) (bmp.getWidth() * scale);
+ return Bitmap.createScaledBitmap(bmp, width, height, true);
+ }
+ }
+}
+
diff --git a/androidx/media/MediaMetadata2Test.java b/androidx/media/MediaMetadata2Test.java
new file mode 100644
index 00000000..f000f020
--- /dev/null
+++ b/androidx/media/MediaMetadata2Test.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.media.MediaMetadata2.Builder;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaMetadata2Test {
+ @Test
+ public void testBuilder() {
+ final Bundle extras = new Bundle();
+ extras.putString("MediaMetadata2Test", "testBuilder");
+ final String title = "title";
+ final long discNumber = 10;
+ final Rating2 rating = Rating2.newThumbRating(true);
+
+ Builder builder = new Builder();
+ builder.setExtras(extras);
+ builder.putString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE, title);
+ builder.putLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER, discNumber);
+ builder.putRating(MediaMetadata2.METADATA_KEY_USER_RATING, rating);
+
+ MediaMetadata2 metadata = builder.build();
+ assertTrue(TestUtils.equals(extras, metadata.getExtras()));
+ assertEquals(title, metadata.getString(MediaMetadata2.METADATA_KEY_DISPLAY_TITLE));
+ assertEquals(discNumber, metadata.getLong(MediaMetadata2.METADATA_KEY_DISC_NUMBER));
+ assertEquals(rating, metadata.getRating(MediaMetadata2.METADATA_KEY_USER_RATING));
+ }
+}
diff --git a/androidx/media/MediaPlayer2.java b/androidx/media/MediaPlayer2.java
new file mode 100644
index 00000000..1864d72d
--- /dev/null
+++ b/androidx/media/MediaPlayer2.java
@@ -0,0 +1,2011 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.media.AudioAttributes;
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.MediaFormat;
+import android.media.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.ResourceBusyException;
+import android.media.SyncParams;
+import android.media.TimedMetaData;
+import android.media.UnsupportedSchemeException;
+import android.os.Build;
+import android.os.PersistableBundle;
+import android.view.Surface;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+
+/**
+ * @hide
+ * MediaPlayer2 class can be used to control playback
+ * of audio/video files and streams. An example on how to use the methods in
+ * this class can be found in {@link android.widget.VideoView}.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#StateDiagram">State Diagram</a>
+ * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#Callbacks">Register informational and error callbacks</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use MediaPlayer2, read the
+ * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="StateDiagram"></a>
+ * <h3>State Diagram</h3>
+ *
+ * <p>Playback control of audio/video files and streams is managed as a state
+ * machine. The following diagram shows the life cycle and the states of a
+ * MediaPlayer2 object driven by the supported playback control operations.
+ * The ovals represent the states a MediaPlayer2 object may reside
+ * in. The arcs represent the playback control operations that drive the object
+ * state transition. There are two types of arcs. The arcs with a single arrow
+ * head represent synchronous method calls, while those with
+ * a double arrow head represent asynchronous method calls.</p>
+ *
+ * <p><img src="../../../images/mediaplayer_state_diagram.gif"
+ * alt="MediaPlayer State diagram"
+ * border="0" /></p>
+ *
+ * <p>From this state diagram, one can see that a MediaPlayer2 object has the
+ * following states:</p>
+ * <ul>
+ * <li>When a MediaPlayer2 object is just created using <code>create</code> or
+ * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
+ * {@link #close()} is called, it is in the <em>End</em> state. Between these
+ * two states is the life cycle of the MediaPlayer2 object.
+ * <ul>
+ * <li> It is a programming error to invoke methods such
+ * as {@link #getCurrentPosition()},
+ * {@link #getDuration()}, {@link #getVideoHeight()},
+ * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
+ * {@link #setPlayerVolume(float)}, {@link #pause()}, {@link #play()},
+ * {@link #seekTo(long, int)} or
+ * {@link #prepare()} in the <em>Idle</em> state.
+ * <li>It is also recommended that once
+ * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
+ * so that resources used by the internal player engine associated with the
+ * MediaPlayer2 object can be released immediately. Resource may include
+ * singleton resources such as hardware acceleration components and
+ * failure to call {@link #close()} may cause subsequent instances of
+ * MediaPlayer2 objects to fallback to software implementations or fail
+ * altogether. Once the MediaPlayer2
+ * object is in the <em>End</em> state, it can no longer be used and
+ * there is no way to bring it back to any other state. </li>
+ * <li>Furthermore,
+ * the MediaPlayer2 objects created using <code>new</code> is in the
+ * <em>Idle</em> state.
+ * </li>
+ * </ul>
+ * </li>
+ * <li>In general, some playback control operation may fail due to various
+ * reasons, such as unsupported audio/video format, poorly interleaved
+ * audio/video, resolution too high, streaming timeout, and the like.
+ * Thus, error reporting and recovery is an important concern under
+ * these circumstances. Sometimes, due to programming errors, invoking a playback
+ * control operation in an invalid state may also occur. Under all these
+ * error conditions, the internal player engine invokes a user supplied
+ * MediaPlayer2EventCallback.onError() method if an MediaPlayer2EventCallback has been
+ * registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ * <ul>
+ * <li>It is important to note that once an error occurs, the
+ * MediaPlayer2 object enters the <em>Error</em> state (except as noted
+ * above), even if an error listener has not been registered by the application.</li>
+ * <li>In order to reuse a MediaPlayer2 object that is in the <em>
+ * Error</em> state and recover from the error,
+ * {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ * state.</li>
+ * <li>It is good programming practice to have your application
+ * register a OnErrorListener to look out for error notifications from
+ * the internal player engine.</li>
+ * <li>IllegalStateException is
+ * thrown to prevent programming errors such as calling
+ * {@link #prepare()}, {@link #setDataSource(DataSourceDesc)}
+ * methods in an invalid state. </li>
+ * </ul>
+ * </li>
+ * <li>Calling
+ * {@link #setDataSource(DataSourceDesc)} transfers a
+ * MediaPlayer2 object in the <em>Idle</em> state to the
+ * <em>Initialized</em> state.
+ * <ul>
+ * <li>An IllegalStateException is thrown if
+ * setDataSource() is called in any other state.</li>
+ * <li>It is good programming
+ * practice to always look out for <code>IllegalArgumentException</code>
+ * and <code>IOException</code> that may be thrown from
+ * <code>setDataSource</code>.</li>
+ * </ul>
+ * </li>
+ * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ * before playback can be started.
+ * <ul>
+ * <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
+ * a call to {@link #prepare()} (asynchronous) which
+ * first transfers the object to the <em>Preparing</em> state after the
+ * call returns (which occurs almost right way) while the internal
+ * player engine continues working on the rest of preparation work
+ * until the preparation work completes. When the preparation completes,
+ * the internal player engine then calls a user supplied callback method,
+ * onInfo() of the MediaPlayer2EventCallback interface with {@link #MEDIA_INFO_PREPARED},
+ * if an MediaPlayer2EventCallback is registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
+ * <li>It is important to note that
+ * the <em>Preparing</em> state is a transient state, and the behavior
+ * of calling any method with side effect while a MediaPlayer2 object is
+ * in the <em>Preparing</em> state is undefined.</li>
+ * <li>An IllegalStateException is
+ * thrown if {@link #prepare()} is called in
+ * any other state.</li>
+ * <li>While in the <em>Prepared</em> state, properties
+ * such as audio/sound volume, screenOnWhilePlaying, looping can be
+ * adjusted by invoking the corresponding set methods.</li>
+ * </ul>
+ * </li>
+ * <li>To start the playback, {@link #play()} must be called. After
+ * {@link #play()} returns successfully, the MediaPlayer2 object is in the
+ * <em>Started</em> state. {@link #getPlayerState()} can be called to test
+ * whether the MediaPlayer2 object is in the <em>Started</em> state.
+ * <ul>
+ * <li>While in the <em>Started</em> state, the internal player engine calls
+ * a user supplied callback method MediaPlayer2EventCallback.onInfo() with
+ * {@link #MEDIA_INFO_BUFFERING_UPDATE} if an MediaPlayer2EventCallback has been
+ * registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ * This callback allows applications to keep track of the buffering status
+ * while streaming audio/video.</li>
+ * <li>Calling {@link #play()} has not effect
+ * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>Playback can be paused and stopped, and the current playback position
+ * can be adjusted. Playback can be paused via {@link #pause()}. When the call to
+ * {@link #pause()} returns, the MediaPlayer2 object enters the
+ * <em>Paused</em> state. Note that the transition from the <em>Started</em>
+ * state to the <em>Paused</em> state and vice versa happens
+ * asynchronously in the player engine. It may take some time before
+ * the state is updated in calls to {@link #getPlayerState()}, and it can be
+ * a number of seconds in the case of streamed content.
+ * <ul>
+ * <li>Calling {@link #play()} to resume playback for a paused
+ * MediaPlayer2 object, and the resumed playback
+ * position is the same as where it was paused. When the call to
+ * {@link #play()} returns, the paused MediaPlayer2 object goes back to
+ * the <em>Started</em> state.</li>
+ * <li>Calling {@link #pause()} has no effect on
+ * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>The playback position can be adjusted with a call to
+ * {@link #seekTo(long, int)}.
+ * <ul>
+ * <li>Although the asynchronuous {@link #seekTo(long, int)}
+ * call returns right away, the actual seek operation may take a while to
+ * finish, especially for audio/video being streamed. When the actual
+ * seek operation completes, the internal player engine calls a user
+ * supplied MediaPlayer2EventCallback.onCallCompleted() with
+ * {@link #CALL_COMPLETED_SEEK_TO}
+ * if an MediaPlayer2EventCallback has been registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
+ * <li>Please
+ * note that {@link #seekTo(long, int)} can also be called in the other states,
+ * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
+ * </em> state. When {@link #seekTo(long, int)} is called in those states,
+ * one video frame will be displayed if the stream has video and the requested
+ * position is valid.
+ * </li>
+ * <li>Furthermore, the actual current playback position
+ * can be retrieved with a call to {@link #getCurrentPosition()}, which
+ * is helpful for applications such as a Music player that need to keep
+ * track of the playback progress.</li>
+ * </ul>
+ * </li>
+ * <li>When the playback reaches the end of stream, the playback completes.
+ * <ul>
+ * <li>If current source is set to loop by {@link #loopCurrent(boolean)},
+ * the MediaPlayer2 object shall remain in the <em>Started</em> state.</li>
+ * <li>If the looping mode was set to <var>false
+ * </var>, the player engine calls a user supplied callback method,
+ * MediaPlayer2EventCallback.onCompletion(), if an MediaPlayer2EventCallback is
+ * registered beforehand via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
+ * The invoke of the callback signals that the object is now in the <em>
+ * PlaybackCompleted</em> state.</li>
+ * <li>While in the <em>PlaybackCompleted</em>
+ * state, calling {@link #play()} can restart the playback from the
+ * beginning of the audio/video source.</li>
+ * </ul>
+ *
+ *
+ * <a name="Valid_and_Invalid_States"></a>
+ * <h3>Valid and invalid states</h3>
+ *
+ * <table border="0" cellspacing="0" cellpadding="0">
+ * <tr><td>Method Name </p></td>
+ * <td>Valid Sates </p></td>
+ * <td>Invalid States </p></td>
+ * <td>Comments </p></td></tr>
+ * <tr><td>attachAuxEffect </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error} </p></td>
+ * <td>This method must be called after setDataSource.
+ * Calling it does not change the object state. </p></td></tr>
+ * <tr><td>getAudioSessionId </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>getCurrentPosition </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted} </p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getDuration </p></td>
+ * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoHeight </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoWidth </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getPlayerState </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>pause </p></td>
+ * <td>{Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Paused</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>prepare </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Preparing</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>release </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>After {@link #close()}, the object is no longer available. </p></td></tr>
+ * <tr><td>reset </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted, Error}</p></td>
+ * <td>{}</p></td>
+ * <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
+ * <tr><td>seekTo </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>setAudioAttributes </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio attributes type to become effective, this method must be called before
+ * prepare().</p></td></tr>
+ * <tr><td>setAudioSessionId </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>This method must be called in idle state as the audio session ID must be known before
+ * calling setDataSource. Calling it does not change the object
+ * state. </p></td></tr>
+ * <tr><td>setAudioStreamType (deprecated)</p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio stream type to become effective, this method must be called before
+ * prepare().</p></td></tr>
+ * <tr><td>setAuxEffectSendLevel </p></td>
+ * <td>any</p></td>
+ * <td>{} </p></td>
+ * <td>Calling this method does not change the object state. </p></td></tr>
+ * <tr><td>setDataSource </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setDisplay </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setSurface </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>loopCurrent </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>isLooping </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setDrmEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setMediaPlayer2EventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setPlaybackParams</p></td>
+ * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
+ * <td>{Idle, Stopped} </p></td>
+ * <td>This method will change state in some cases, depending on when it's called.
+ * </p></td></tr>
+ * <tr><td>setPlayerVolume </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.
+ * <tr><td>play </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Started</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>stop </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Stopped</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>getTrackInfo </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>selectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>deselectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ *
+ * </table>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
+ * android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element.
+ *
+ * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
+ * when used with network-based content.
+ *
+ * <a name="Callbacks"></a>
+ * <h3>Callbacks</h3>
+ * <p>Applications may want to register for informational and error
+ * events in order to be informed of some internal state update and
+ * possible runtime errors during playback or streaming. Registration for
+ * these events is done by properly setting the appropriate listeners (via calls
+ * to
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)},
+ * {@link #setDrmEventCallback(Executor, DrmEventCallback)}).
+ * In order to receive the respective callback
+ * associated with these listeners, applications are required to create
+ * MediaPlayer2 objects on a thread with its own Looper running (main UI
+ * thread by default has a Looper running).
+ *
+ */
+@TargetApi(Build.VERSION_CODES.P)
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MediaPlayer2 extends MediaPlayerBase {
+ /**
+ * Create a MediaPlayer2 object.
+ *
+ * @return A MediaPlayer2 object created
+ */
+ public static final MediaPlayer2 create() {
+ return new MediaPlayer2Impl();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public MediaPlayer2() { }
+
+ /**
+ * Releases the resources held by this {@code MediaPlayer2} object.
+ *
+ * It is considered good practice to call this method when you're
+ * done using the MediaPlayer2. In particular, whenever an Activity
+ * of an application is paused (its onPause() method is called),
+ * or stopped (its onStop() method is called), this method should be
+ * invoked to release the MediaPlayer2 object, unless the application
+ * has a special need to keep the object around. In addition to
+ * unnecessary resources (such as memory and instances of codecs)
+ * being held, failure to call this method immediately if a
+ * MediaPlayer2 object is no longer needed may also lead to
+ * continuous battery consumption for mobile devices, and playback
+ * failure for other applications if no multiple instances of the
+ * same codec are supported on a device. Even if multiple instances
+ * of the same codec are supported, some performance degradation
+ * may be expected when unnecessary multiple instances are used
+ * at the same time.
+ *
+ * {@code close()} may be safely called after a prior {@code close()}.
+ * This class implements the Java {@code AutoCloseable} interface and
+ * may be used with try-with-resources.
+ */
+ // This is a synchronous call.
+ @Override
+ public abstract void close();
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * reached end of stream and been paused, or never started before,
+ * playback will start at the beginning. If the source had not been
+ * prepared, the player will prepare the source and play.
+ *
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void play();
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to
+ * call prepare().
+ *
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void prepare();
+
+ /**
+ * Pauses playback. Call play() to resume.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void pause();
+
+ /**
+ * Tries to play next data source if applicable.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void skipToNext();
+
+ /**
+ * Moves the media to specified time position.
+ * Same as {@link #seekTo(long, int)} with {@code mode = SEEK_PREVIOUS_SYNC}.
+ *
+ * @param msec the offset in milliseconds from the start to seek to
+ */
+ // This is an asynchronous call.
+ @Override
+ public void seekTo(long msec) {
+ seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ @Override
+ public abstract long getCurrentPosition();
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ @Override
+ public abstract long getDuration();
+
+ /**
+ * Gets the current buffered media source position received through progressive downloading.
+ * The received buffering percentage indicates how much of the content has been buffered
+ * or played. For example a buffering update of 80 percent when half the content
+ * has already been played indicates that the next 30 percent of the
+ * content to play has been buffered.
+ *
+ * @return the current buffered media source position in milliseconds
+ */
+ @Override
+ public abstract long getBufferedPosition();
+
+ /**
+ * Gets the current player state.
+ *
+ * @return the current player state.
+ */
+ @Override
+ public abstract @PlayerState int getPlayerState();
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state, one of the following:
+ */
+ @Override
+ public abstract @BuffState int getBufferingState();
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepare()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
+
+ /**
+ * Gets the audio attributes for this MediaPlayer2.
+ * @return attributes a set of audio attributes
+ */
+ @Override
+ public abstract @Nullable AudioAttributesCompat getAudioAttributes();
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets a single data source as described by a DataSourceDesc which will be played
+ * after current data source is finished.
+ *
+ * @param dsd the descriptor of data source you want to play after current one
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets a list of data sources to be played sequentially after current data source is done.
+ *
+ * @param dsds the list of data sources you want to play after current one
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+ /**
+ * Gets the current data source as described by a DataSourceDesc.
+ *
+ * @return the current DataSourceDesc
+ */
+ @Override
+ public abstract @NonNull DataSourceDesc getCurrentDataSource();
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void loopCurrent(boolean loop);
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed the desired playback speed
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setPlaybackSpeed(float speed);
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return 1.0f;
+ }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ @Override
+ public boolean isReversePlaybackSupported() {
+ return false;
+ }
+
+ /**
+ * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+ * on the audio samples.
+ * Note that this volume is specific to the player, and is separate from stream volume
+ * used across the platform.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ // This is an asynchronous call.
+ @Override
+ public abstract void setPlayerVolume(float volume);
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ @Override
+ public abstract float getPlayerVolume();
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ @Override
+ public float getMaxPlayerVolume() {
+ return 1.0f;
+ }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ // This is a synchronous call.
+ @Override
+ public abstract void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb);
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ // This is a synchronous call.
+ @Override
+ public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+ /**
+ * Insert a task in the command queue to help the client to identify whether a batch
+ * of commands has been finished. When this command is processed, a notification
+ * {@code MediaPlayer2EventCallback.onCommandLabelReached} will be fired with the
+ * given {@code label}.
+ *
+ * @see MediaPlayer2EventCallback#onCommandLabelReached
+ *
+ * @param label An application specific Object used to help to identify the completeness
+ * of a batch of commands.
+ */
+ // This is an asynchronous call.
+ public void notifyWhenCommandLabelReached(@NonNull Object label) { }
+
+ /**
+ * Sets the {@link Surface} to be used as the sink for the video portion of
+ * the media. Setting a
+ * Surface will un-set any Surface or SurfaceHolder that was previously set.
+ * A null surface will result in only the audio track being played.
+ *
+ * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ // This is an asynchronous call.
+ public abstract void setSurface(Surface surface);
+
+ /* Do not change these video scaling mode values below without updating
+ * their counterparts in system/window.h! Please do not forget to update
+ * {@link #isVideoScalingModeSupported} when new video scaling modes
+ * are added.
+ */
+ /**
+ * Specifies a video scaling mode. The content is stretched to the
+ * surface rendering area. When the surface has the same aspect ratio
+ * as the content, the aspect ratio of the content is maintained;
+ * otherwise, the aspect ratio of the content is not maintained when video
+ * is being rendered.
+ * There is no content cropping with this video scaling mode.
+ */
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1;
+
+ /**
+ * Discards all pending commands.
+ */
+ // This is a synchronous call.
+ public abstract void clearPendingCommands();
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return the width of the video, or 0 if there is no video,
+ * no display surface was set, or the width has not been determined
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+ * is available.
+ */
+ public abstract int getVideoWidth();
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return the height of the video, or 0 if there is no video,
+ * no display surface was set, or the height has not been determined
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height is
+ * available.
+ */
+ public abstract int getVideoHeight();
+
+ /**
+ * Return Metrics data about the current player.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for the media being handled by this instance of MediaPlayer2
+ * The attributes are descibed in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ * @hide
+ * TODO: This method is not ready for public. Currently returns metrics data in MediaPlayer1.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public abstract PersistableBundle getMetrics();
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ */
+ // This is an asynchronous call.
+ public abstract void setPlaybackParams(@NonNull PlaybackParams params);
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ */
+ @NonNull
+ public abstract PlaybackParams getPlaybackParams();
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ */
+ // This is an asynchronous call.
+ public abstract void setSyncParams(@NonNull SyncParams params);
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ */
+ @NonNull
+ public abstract SyncParams getSyncParams();
+
+ /**
+ * Seek modes used in method seekTo(long, int) to move media position
+ * to a specified location.
+ *
+ * Do not change these mode values without updating their counterparts
+ * in include/media/IMediaSource.h!
+ */
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * right before or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_PREVIOUS_SYNC = 0x00;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * right after or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_NEXT_SYNC = 0x01;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * closest to (in time) or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_CLOSEST_SYNC = 0x02;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a frame (not necessarily a key frame) associated with a data source that
+ * is located closest to or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_CLOSEST = 0x03;
+
+ /** @hide */
+ @IntDef(flag = false, /*prefix = "SEEK",*/ value = {
+ SEEK_PREVIOUS_SYNC,
+ SEEK_NEXT_SYNC,
+ SEEK_CLOSEST_SYNC,
+ SEEK_CLOSEST,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface SeekMode {}
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * There is at most one active seekTo processed at any time. If there is a to-be-completed
+ * seekTo, new seekTo requests will be queued in such a way that only the last request
+ * is kept. When current seekTo is completed, the queued request will be processed if
+ * that request is different from just-finished seekTo operation, i.e., the requested
+ * position or mode is different.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * has a frame located at the position. When this happens, a frame nearby will be rendered.
+ * If msec is negative, time position zero will be used.
+ * If msec is larger than duration, duration will be used.
+ * @param mode the mode indicating where exactly to seek to.
+ */
+ // This is an asynchronous call.
+ public abstract void seekTo(long msec, @SeekMode int mode);
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ *
+ * @see MediaTimestamp
+ */
+ @Nullable
+ public abstract MediaTimestamp getTimestamp();
+
+ /**
+ * Resets the MediaPlayer2 to its uninitialized state. After calling
+ * this method, you will have to initialize it again by setting the
+ * data source and calling prepare().
+ */
+ // This is a synchronous call.
+ @Override
+ public abstract void reset();
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ */
+ // This is an asynchronous call.
+ public abstract void setAudioSessionId(int sessionId);
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return the audio session ID. {@see #setAudioSessionId(int)}
+ * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was
+ * contructed.
+ */
+ public abstract int getAudioSessionId();
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ // This is an asynchronous call.
+ public abstract void attachAuxEffect(int effectId);
+
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ // This is an asynchronous call.
+ public abstract void setAuxEffectSendLevel(float level);
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see MediaPlayer2#getTrackInfo
+ */
+ public abstract static class TrackInfo {
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ public abstract int getTrackType();
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ public abstract String getLanguage();
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ public abstract MediaFormat getFormat();
+
+ public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0;
+ public static final int MEDIA_TRACK_TYPE_VIDEO = 1;
+ public static final int MEDIA_TRACK_TYPE_AUDIO = 2;
+
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;
+
+ public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
+ public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+
+ @Override
+ public abstract String toString();
+ };
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return List of track info. The total number of tracks is the array length.
+ * Must be called again if an external timed text source has been added after
+ * addTimedTextSource method is called.
+ */
+ public abstract List<TrackInfo> getTrackInfo();
+
+ /**
+ * Returns the index of the audio, video, or subtitle track currently selected for playback,
+ * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ public abstract int getSelectedTrack(int trackType);
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ * @param index the index of the track to be selected. The valid range of the index
+ * is 0..total number of track - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see MediaPlayer2#getTrackInfo
+ */
+ // This is an asynchronous call.
+ public abstract void selectTrack(int index);
+
+ /**
+ * Deselect a track.
+ * <p>
+ * Currently, the track must be a timed text track and no audio or video tracks can be
+ * deselected. If the timed text track identified by index has not been
+ * selected before, it throws an exception.
+ * </p>
+ * @param index the index of the track to be deselected. The valid range of the index
+ * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see MediaPlayer2#getTrackInfo
+ */
+ // This is an asynchronous call.
+ public abstract void deselectTrack(int index);
+
+ /**
+ * Interface definition for callbacks to be invoked when the player has the corresponding
+ * events.
+ */
+ public abstract static class MediaPlayer2EventCallback {
+ /**
+ * Called to indicate the video size
+ *
+ * The video size (width and height) could be 0 if there was no video,
+ * no display surface was set, or the value was not determined yet.
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ * @param width the width of the video
+ * @param height the height of the video
+ */
+ public void onVideoSizeChanged(
+ MediaPlayer2 mp, DataSourceDesc dsd, int width, int height) { }
+
+ /**
+ * Called to indicate available timed metadata
+ * <p>
+ * This method will be called as timed metadata is extracted from the media,
+ * in the same order as it occurs in the media. The timing of this event is
+ * not controlled by the associated timestamp.
+ * <p>
+ * Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates
+ * {@link TimedMetaData}.
+ *
+ * @see MediaPlayer2#selectTrack(int)
+ * @see TimedMetaData
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ * @param data the timed metadata sample associated with this event
+ */
+ public void onTimedMetaDataAvailable(
+ MediaPlayer2 mp, DataSourceDesc dsd, TimedMetaData data) { }
+
+ /**
+ * Called to indicate an error.
+ *
+ * @param mp the MediaPlayer2 the error pertains to
+ * @param dsd the DataSourceDesc of this data source
+ * @param what the type of error that has occurred.
+ * @param extra an extra code, specific to the error. Typically
+ * implementation dependent.
+ */
+ public void onError(
+ MediaPlayer2 mp, DataSourceDesc dsd, @MediaError int what, int extra) { }
+
+ /**
+ * Called to indicate an info or a warning.
+ *
+ * @param mp the MediaPlayer2 the info pertains to.
+ * @param dsd the DataSourceDesc of this data source
+ * @param what the type of info or warning.
+ * @param extra an extra code, specific to the info. Typically
+ * implementation dependent.
+ */
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, @MediaInfo int what, int extra) { }
+
+ /**
+ * Called to acknowledge an API call.
+ *
+ * @param mp the MediaPlayer2 the call was made on.
+ * @param dsd the DataSourceDesc of this data source
+ * @param what the enum for the API call.
+ * @param status the returned status code for the call.
+ */
+ public void onCallCompleted(
+ MediaPlayer2 mp, DataSourceDesc dsd, @CallCompleted int what,
+ @CallStatus int status) { }
+
+ /**
+ * Called to indicate media clock has changed.
+ *
+ * @param mp the MediaPlayer2 the media time pertains to.
+ * @param dsd the DataSourceDesc of this data source
+ * @param timestamp the new media clock.
+ */
+ public void onMediaTimeChanged(
+ MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
+
+ /**
+ * Called to indicate {@link #notifyWhenCommandLabelReached(Object)} has been processed.
+ *
+ * @param mp the MediaPlayer2 {@link #notifyWhenCommandLabelReached(Object)} was called on.
+ * @param label the application specific Object given by
+ * {@link #notifyWhenCommandLabelReached(Object)}.
+ */
+ public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
+
+ /* TODO : uncomment below once API is available in supportlib.
+ * Called when when a player subtitle track has new subtitle data available.
+ * @param mp the player that reports the new subtitle data
+ * @param data the subtitle data
+ */
+ // public void onSubtitleData(MediaPlayer2 mp, @NonNull SubtitleData data) { }
+ }
+
+ /**
+ * Sets the callback to be invoked when the media source is ready for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ // This is a synchronous call.
+ public abstract void setMediaPlayer2EventCallback(
+ @NonNull Executor executor, @NonNull MediaPlayer2EventCallback eventCallback);
+
+ /**
+ * Clears the {@link MediaPlayer2EventCallback}.
+ */
+ // This is a synchronous call.
+ public abstract void clearMediaPlayer2EventCallback();
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ /** Unspecified media player error.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onError
+ */
+ public static final int MEDIA_ERROR_UNKNOWN = 1;
+
+ /** The video is streamed and its container is not valid for progressive
+ * playback i.e the video's index (e.g moov atom) is not at the start of the
+ * file.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onError
+ */
+ public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;
+
+ /** File or network related operation errors. */
+ public static final int MEDIA_ERROR_IO = -1004;
+ /** Bitstream is not conforming to the related coding standard or file spec. */
+ public static final int MEDIA_ERROR_MALFORMED = -1007;
+ /** Bitstream is conforming to the related coding standard or file spec, but
+ * the media framework does not support the feature. */
+ public static final int MEDIA_ERROR_UNSUPPORTED = -1010;
+ /** Some operation takes too long to complete, usually more than 3-5 seconds. */
+ public static final int MEDIA_ERROR_TIMED_OUT = -110;
+
+ /** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in
+ * system/core/include/utils/Errors.h
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onError
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int MEDIA_ERROR_SYSTEM = -2147483648;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, /*prefix = "MEDIA_ERROR",*/ value = {
+ MEDIA_ERROR_UNKNOWN,
+ MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+ MEDIA_ERROR_IO,
+ MEDIA_ERROR_MALFORMED,
+ MEDIA_ERROR_UNSUPPORTED,
+ MEDIA_ERROR_TIMED_OUT,
+ MEDIA_ERROR_SYSTEM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface MediaError {}
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ /** Unspecified media player info.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_UNKNOWN = 1;
+
+ /** The player switched to this datas source because it is the
+ * next-to-be-played in the playlist.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
+
+ /** The player just pushed the very first video frame for rendering.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3;
+
+ /** The player just rendered the very first audio sample.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4;
+
+ /** The player just completed the playback of this data source.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5;
+
+ /** The player just completed the playback of the full playlist.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_PLAYLIST_END = 6;
+
+ /** The player just prepared a data source.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_PREPARED = 100;
+
+ /** The video is too complex for the decoder: it can't decode frames fast
+ * enough. Possibly only the audio plays fine at this stage.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;
+
+ /** MediaPlayer2 is temporarily pausing playback internally in order to
+ * buffer more data.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_START = 701;
+
+ /** MediaPlayer2 is resuming playback after filling buffers.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_END = 702;
+
+ /** Estimated network bandwidth information (kbps) is available; currently this event fires
+ * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END}
+ * when playing network files.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703;
+
+ /**
+ * Update status in buffering a media source received through progressive downloading.
+ * The received buffering percentage indicates how much of the content has been buffered
+ * or played. For example a buffering update of 80 percent when half the content
+ * has already been played indicates that the next 30 percent of the
+ * content to play has been buffered.
+ *
+ * The {@code extra} parameter in {@code MediaPlayer2EventCallback.onInfo} is the
+ * percentage (0-100) of the content that has been buffered or played thus far.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_UPDATE = 704;
+
+ /** Bad interleaving means that a media has been improperly interleaved or
+ * not interleaved at all, e.g has all the video samples first then all the
+ * audio ones. Video is playing but a lot of disk seeks may be happening.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_BAD_INTERLEAVING = 800;
+
+ /** The media cannot be seeked (e.g live stream)
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
+
+ /** A new set of metadata is available.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_METADATA_UPDATE = 802;
+
+ /** A new set of external-only metadata is available. Used by
+ * JAVA framework to avoid triggering track scanning.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int MEDIA_INFO_EXTERNAL_METADATA_UPDATE = 803;
+
+ /** Informs that audio is not playing. Note that playback of the video
+ * is not interrupted.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804;
+
+ /** Informs that video is not playing. Note that playback of the audio
+ * is not interrupted.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805;
+
+ /** Failed to handle timed text track properly.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ * {@hide}
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int MEDIA_INFO_TIMED_TEXT_ERROR = 900;
+
+ /** Subtitle track was not supported by the media framework.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
+
+ /** Reading the subtitle track takes too long.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onInfo
+ */
+ public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, /*prefix = "MEDIA_INFO",*/ value = {
+ MEDIA_INFO_UNKNOWN,
+ MEDIA_INFO_STARTED_AS_NEXT,
+ MEDIA_INFO_VIDEO_RENDERING_START,
+ MEDIA_INFO_AUDIO_RENDERING_START,
+ MEDIA_INFO_PLAYBACK_COMPLETE,
+ MEDIA_INFO_PLAYLIST_END,
+ MEDIA_INFO_PREPARED,
+ MEDIA_INFO_VIDEO_TRACK_LAGGING,
+ MEDIA_INFO_BUFFERING_START,
+ MEDIA_INFO_BUFFERING_END,
+ MEDIA_INFO_NETWORK_BANDWIDTH,
+ MEDIA_INFO_BUFFERING_UPDATE,
+ MEDIA_INFO_BAD_INTERLEAVING,
+ MEDIA_INFO_NOT_SEEKABLE,
+ MEDIA_INFO_METADATA_UPDATE,
+ MEDIA_INFO_EXTERNAL_METADATA_UPDATE,
+ MEDIA_INFO_AUDIO_NOT_PLAYING,
+ MEDIA_INFO_VIDEO_NOT_PLAYING,
+ MEDIA_INFO_TIMED_TEXT_ERROR,
+ MEDIA_INFO_UNSUPPORTED_SUBTITLE,
+ MEDIA_INFO_SUBTITLE_TIMED_OUT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface MediaInfo {}
+
+ //--------------------------------------------------------------------------
+ /** The player just completed a call {@link #attachAuxEffect}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_ATTACH_AUX_EFFECT = 1;
+
+ /** The player just completed a call {@link #deselectTrack}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_DESELECT_TRACK = 2;
+
+ /** The player just completed a call {@link #loopCurrent}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_LOOP_CURRENT = 3;
+
+ /** The player just completed a call {@link #pause}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_PAUSE = 4;
+
+ /** The player just completed a call {@link #play}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_PLAY = 5;
+
+ /** The player just completed a call {@link #prepare}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_PREPARE = 6;
+
+ /** The player just completed a call {@link #releaseDrm}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_RELEASE_DRM = 12;
+
+ /** The player just completed a call {@link #restoreDrmKeys}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
+
+ /** The player just completed a call {@link #seekTo}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SEEK_TO = 14;
+
+ /** The player just completed a call {@link #selectTrack}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SELECT_TRACK = 15;
+
+ /** The player just completed a call {@link #setAudioAttributes}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_AUDIO_ATTRIBUTES = 16;
+
+ /** The player just completed a call {@link #setAudioSessionId}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_AUDIO_SESSION_ID = 17;
+
+ /** The player just completed a call {@link #setAuxEffectSendLevel}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL = 18;
+
+ /** The player just completed a call {@link #setDataSource}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_DATA_SOURCE = 19;
+
+ /** The player just completed a call {@link #setNextDataSource}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCE = 22;
+
+ /** The player just completed a call {@link #setNextDataSources}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCES = 23;
+
+ /** The player just completed a call {@link #setPlaybackParams}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_PLAYBACK_PARAMS = 24;
+
+ /** The player just completed a call {@link #setPlaybackSpeed}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_PLAYBACK_SPEED = 25;
+
+ /** The player just completed a call {@link #setPlayerVolume}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_PLAYER_VOLUME = 26;
+
+ /** The player just completed a call {@link #setSurface}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_SURFACE = 27;
+
+ /** The player just completed a call {@link #setSyncParams}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SET_SYNC_PARAMS = 28;
+
+ /** The player just completed a call {@link #skipToNext}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_COMPLETED_SKIP_TO_NEXT = 29;
+
+ /** The player just completed a call {@code notifyWhenCommandLabelReached}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCommandLabelReached
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED = 1003;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, /*prefix = "CALL_COMPLETED",*/ value = {
+ CALL_COMPLETED_ATTACH_AUX_EFFECT,
+ CALL_COMPLETED_DESELECT_TRACK,
+ CALL_COMPLETED_LOOP_CURRENT,
+ CALL_COMPLETED_PAUSE,
+ CALL_COMPLETED_PLAY,
+ CALL_COMPLETED_PREPARE,
+ CALL_COMPLETED_RELEASE_DRM,
+ CALL_COMPLETED_RESTORE_DRM_KEYS,
+ CALL_COMPLETED_SEEK_TO,
+ CALL_COMPLETED_SELECT_TRACK,
+ CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
+ CALL_COMPLETED_SET_AUDIO_SESSION_ID,
+ CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL,
+ CALL_COMPLETED_SET_DATA_SOURCE,
+ CALL_COMPLETED_SET_NEXT_DATA_SOURCE,
+ CALL_COMPLETED_SET_NEXT_DATA_SOURCES,
+ CALL_COMPLETED_SET_PLAYBACK_PARAMS,
+ CALL_COMPLETED_SET_PLAYBACK_SPEED,
+ CALL_COMPLETED_SET_PLAYER_VOLUME,
+ CALL_COMPLETED_SET_SURFACE,
+ CALL_COMPLETED_SET_SYNC_PARAMS,
+ CALL_COMPLETED_SKIP_TO_NEXT,
+ CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface CallCompleted {}
+
+ /** Status code represents that call is completed without an error.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_NO_ERROR = 0;
+
+ /** Status code represents that call is ended with an unknown error.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_ERROR_UNKNOWN = Integer.MIN_VALUE;
+
+ /** Status code represents that the player is not in valid state for the operation.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_INVALID_OPERATION = 1;
+
+ /** Status code represents that the argument is illegal.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_BAD_VALUE = 2;
+
+ /** Status code represents that the operation is not allowed.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_PERMISSION_DENIED = 3;
+
+ /** Status code represents a file or network related operation error.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_ERROR_IO = 4;
+
+ /** Status code represents that DRM operation is called before preparing a DRM scheme through
+ * {@link #prepareDrm}.
+ * @see MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+ */
+ public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = false, /*prefix = "CALL_STATUS",*/ value = {
+ CALL_STATUS_NO_ERROR,
+ CALL_STATUS_ERROR_UNKNOWN,
+ CALL_STATUS_INVALID_OPERATION,
+ CALL_STATUS_BAD_VALUE,
+ CALL_STATUS_PERMISSION_DENIED,
+ CALL_STATUS_ERROR_IO,
+ CALL_STATUS_NO_DRM_SCHEME})
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface CallStatus {}
+
+ // Modular DRM begin
+
+ /**
+ * Interface definition of a callback to be invoked when the app
+ * can do DRM configuration (get/set properties) before the session
+ * is opened. This facilitates configuration of the properties, like
+ * 'securityLevel', which has to be set after DRM scheme creation but
+ * before the DRM session is opened.
+ *
+ * The only allowed DRM calls in this listener are {@link #getDrmPropertyString}
+ * and {@link #setDrmPropertyString}.
+ */
+ public interface OnDrmConfigHelper {
+ /**
+ * Called to give the app the opportunity to configure DRM before the session is created
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ */
+ void onDrmConfig(MediaPlayer2 mp, DataSourceDesc dsd);
+ }
+
+ /**
+ * Register a callback to be invoked for configuration of the DRM object before
+ * the session is created.
+ * The callback will be invoked synchronously during the execution
+ * of {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ // This is a synchronous call.
+ public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener);
+
+ /**
+ * Interface definition for callbacks to be invoked when the player has the corresponding
+ * DRM events.
+ */
+ public abstract static class DrmEventCallback {
+ /**
+ * Called to indicate DRM info is available
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ * @param drmInfo DRM info of the source including PSSH, and subset
+ * of crypto schemes supported by this device
+ */
+ public void onDrmInfo(MediaPlayer2 mp, DataSourceDesc dsd, DrmInfo drmInfo) { }
+
+ /**
+ * Called to notify the client that {@link #prepareDrm} is finished and ready for
+ * key request/response.
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param dsd the DataSourceDesc of this data source
+ * @param status the result of DRM preparation.
+ */
+ public void onDrmPrepared(
+ MediaPlayer2 mp, DataSourceDesc dsd, @PrepareDrmStatusCode int status) { }
+ }
+
+ /**
+ * Sets the callback to be invoked when the media source is ready for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ // This is a synchronous call.
+ public abstract void setDrmEventCallback(@NonNull Executor executor,
+ @NonNull DrmEventCallback eventCallback);
+
+ /**
+ * Clears the {@link DrmEventCallback}.
+ */
+ // This is a synchronous call.
+ public abstract void clearDrmEventCallback();
+
+ /**
+ * The status codes for {@link DrmEventCallback#onDrmPrepared} listener.
+ * <p>
+ *
+ * DRM preparation has succeeded.
+ */
+ public static final int PREPARE_DRM_STATUS_SUCCESS = 0;
+
+ /**
+ * The device required DRM provisioning but couldn't reach the provisioning server.
+ */
+ public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1;
+
+ /**
+ * The device required DRM provisioning but the provisioning server denied the request.
+ */
+ public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2;
+
+ /**
+ * The DRM preparation has failed .
+ */
+ public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3;
+
+
+ /** @hide */
+ @IntDef(flag = false, /*prefix = "PREPARE_DRM_STATUS",*/ value = {
+ PREPARE_DRM_STATUS_SUCCESS,
+ PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PREPARATION_ERROR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface PrepareDrmStatusCode {}
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before being prepared
+ */
+ public abstract DrmInfo getDrmInfo();
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@link OnDrmConfigHelper} is registered, it will be called during
+ * preparation to allow configuration of the DRM properties before opening the
+ * DRM session. Note that the callback is called synchronously in the thread that called
+ * {@link #prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ *
+ * @throws IllegalStateException if called before being prepared or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ // This is a synchronous call.
+ public abstract void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException;
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ // This is an asynchronous call.
+ public abstract void releaseDrm() throws NoDrmSchemeException;
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideDrmKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @NonNull
+ public abstract MediaDrm.KeyRequest getDrmKeyRequest(
+ @Nullable byte[] keySetId, @Nullable byte[] initData,
+ @Nullable String mimeType, int keyType,
+ @Nullable Map<String, String> optionalParameters)
+ throws NoDrmSchemeException;
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideDrmKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreDrmKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link # getDrmKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ // This is a synchronous call.
+ public abstract byte[] provideDrmKeyResponse(
+ @Nullable byte[] keySetId, @NonNull byte[] response)
+ throws NoDrmSchemeException, DeniedByServerException;
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ // This is an asynchronous call.
+ public abstract void restoreDrmKeys(@NonNull byte[] keySetId)
+ throws NoDrmSchemeException;
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @NonNull
+ public abstract String getDrmPropertyString(
+ @NonNull String propertyName)
+ throws NoDrmSchemeException;
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ // This is a synchronous call.
+ public abstract void setDrmPropertyString(
+ @NonNull String propertyName, @NonNull String value)
+ throws NoDrmSchemeException;
+
+ /**
+ * Encapsulates the DRM properties of the source.
+ */
+ public abstract static class DrmInfo {
+ /**
+ * Returns the PSSH info of the data source for each supported DRM scheme.
+ */
+ public abstract Map<UUID, byte[]> getPssh();
+
+ /**
+ * Returns the intersection of the data source and the device DRM schemes.
+ * It effectively identifies the subset of the source's DRM schemes which
+ * are supported by the device too.
+ */
+ public abstract List<UUID> getSupportedSchemes();
+ }; // DrmInfo
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static class NoDrmSchemeException extends MediaDrmException {
+ public NoDrmSchemeException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static class ProvisioningNetworkErrorException extends MediaDrmException {
+ public ProvisioningNetworkErrorException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static class ProvisioningServerErrorException extends MediaDrmException {
+ public ProvisioningServerErrorException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+}
diff --git a/androidx/media/MediaPlayer2Impl.java b/androidx/media/MediaPlayer2Impl.java
new file mode 100644
index 00000000..3b3e119e
--- /dev/null
+++ b/androidx/media/MediaPlayer2Impl.java
@@ -0,0 +1,1982 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.media.AudioAttributes;
+import android.media.DeniedByServerException;
+import android.media.MediaDataSource;
+import android.media.MediaDrm;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.ResourceBusyException;
+import android.media.SyncParams;
+import android.media.TimedMetaData;
+import android.media.UnsupportedSchemeException;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Surface;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.io.IOException;
+import java.nio.ByteOrder;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @hide
+ */
+@TargetApi(Build.VERSION_CODES.P)
+@RestrictTo(LIBRARY_GROUP)
+public final class MediaPlayer2Impl extends MediaPlayer2 {
+
+ private static final String TAG = "MediaPlayer2Impl";
+
+ private static final int NEXT_SOURCE_STATE_ERROR = -1;
+ private static final int NEXT_SOURCE_STATE_INIT = 0;
+ private static final int NEXT_SOURCE_STATE_PREPARING = 1;
+ private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+
+ private static ArrayMap<Integer, Integer> sInfoEventMap;
+ private static ArrayMap<Integer, Integer> sErrorEventMap;
+
+ static {
+ sInfoEventMap = new ArrayMap<>();
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
+ sInfoEventMap.put(2 /*MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT*/, MEDIA_INFO_STARTED_AS_NEXT);
+ sInfoEventMap.put(
+ MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START);
+ sInfoEventMap.put(
+ MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_START);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_END);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING);
+ sInfoEventMap.put(
+ MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_UNSUPPORTED_SUBTITLE);
+ sInfoEventMap.put(MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_SUBTITLE_TIMED_OUT);
+
+ sErrorEventMap = new ArrayMap<>();
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNKNOWN);
+ sErrorEventMap.put(
+ MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+ MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
+ sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
+ }
+
+ private MediaPlayer mPlayer; // MediaPlayer is thread-safe.
+
+ private final Object mSrcLock = new Object();
+ //--- guarded by |mSrcLock| start
+ private long mSrcIdGenerator = 0;
+ private DataSourceDesc mCurrentDSD;
+ private long mCurrentSrcId = mSrcIdGenerator++;
+ private List<DataSourceDesc> mNextDSDs;
+ private long mNextSrcId = mSrcIdGenerator++;
+ private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ private boolean mNextSourcePlayPending = false;
+ //--- guarded by |mSrcLock| end
+
+ private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
+ private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
+ private volatile float mVolume = 1.0f;
+
+ private HandlerThread mHandlerThread;
+ private final Handler mTaskHandler;
+ private final Object mTaskLock = new Object();
+ @GuardedBy("mTaskLock")
+ private final ArrayDeque<Task> mPendingTasks = new ArrayDeque<>();
+ @GuardedBy("mTaskLock")
+ private Task mCurrentTask;
+
+ private final Object mLock = new Object();
+ //--- guarded by |mLock| start
+ @PlayerState private int mPlayerState;
+ @BuffState private int mBufferingState;
+ private AudioAttributesCompat mAudioAttributes;
+ private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
+ new ArrayList<>();
+ private ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
+ new ArrayMap<>();
+ private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
+ new ArrayList<>();
+ //--- guarded by |mLock| end
+
+ /**
+ * Default constructor.
+ * <p>When done with the MediaPlayer2Impl, you should call {@link #close()},
+ * to free the resources. If not released, too many MediaPlayer2Impl instances may
+ * result in an exception.</p>
+ */
+ public MediaPlayer2Impl() {
+ mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
+ mHandlerThread.start();
+ Looper looper = mHandlerThread.getLooper();
+ mTaskHandler = new Handler(looper);
+ mPlayer = new MediaPlayer();
+ mPlayerState = PLAYER_STATE_IDLE;
+ mBufferingState = BUFFERING_STATE_UNKNOWN;
+ setUpListeners();
+ }
+
+ /**
+ * Releases the resources held by this {@code MediaPlayer2} object.
+ *
+ * It is considered good practice to call this method when you're
+ * done using the MediaPlayer2. In particular, whenever an Activity
+ * of an application is paused (its onPause() method is called),
+ * or stopped (its onStop() method is called), this method should be
+ * invoked to release the MediaPlayer2 object, unless the application
+ * has a special need to keep the object around. In addition to
+ * unnecessary resources (such as memory and instances of codecs)
+ * being held, failure to call this method immediately if a
+ * MediaPlayer2 object is no longer needed may also lead to
+ * continuous battery consumption for mobile devices, and playback
+ * failure for other applications if no multiple instances of the
+ * same codec are supported on a device. Even if multiple instances
+ * of the same codec are supported, some performance degradation
+ * may be expected when unnecessary multiple instances are used
+ * at the same time.
+ *
+ * {@code close()} may be safely called after a prior {@code close()}.
+ * This class implements the Java {@code AutoCloseable} interface and
+ * may be used with try-with-resources.
+ */
+ @Override
+ public void close() {
+ mPlayer.release();
+ }
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * been stopped, or never started before, playback will start at the
+ * beginning.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void play() {
+ addTask(new Task(CALL_COMPLETED_PLAY, false) {
+ @Override
+ void process() {
+ mPlayer.start();
+ setPlayerState(PLAYER_STATE_PLAYING);
+ }
+ });
+ }
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare(). For streams, you should call prepare(),
+ * which returns immediately, rather than blocking until enough data has been
+ * buffered.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void prepare() {
+ addTask(new Task(CALL_COMPLETED_PREPARE, true) {
+ @Override
+ void process() throws IOException {
+ mPlayer.prepareAsync();
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+ }
+ });
+ }
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been initialized.
+ */
+ @Override
+ public void pause() {
+ addTask(new Task(CALL_COMPLETED_PAUSE, false) {
+ @Override
+ void process() {
+ mPlayer.pause();
+ setPlayerState(PLAYER_STATE_PAUSED);
+ }
+ });
+ }
+
+ /**
+ * Tries to play next data source if applicable.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void skipToNext() {
+ addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
+ @Override
+ void process() {
+ // TODO: switch to next data source and play
+ }
+ });
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ @Override
+ public long getCurrentPosition() {
+ return mPlayer.getCurrentPosition();
+ }
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ @Override
+ public long getDuration() {
+ return mPlayer.getDuration();
+ }
+
+ /**
+ * Gets the current buffered media source position received through progressive downloading.
+ * The received buffering percentage indicates how much of the content has been buffered
+ * or played. For example a buffering update of 80 percent when half the content
+ * has already been played indicates that the next 30 percent of the
+ * content to play has been buffered.
+ *
+ * @return the current buffered media source position in milliseconds
+ */
+ @Override
+ public long getBufferedPosition() {
+ // Use cached buffered percent for now.
+ return getDuration() * mBufferedPercentageCurrent.get() / 100;
+ }
+
+ @Override
+ public @PlayerState int getPlayerState() {
+ synchronized (mLock) {
+ return mPlayerState;
+ }
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ */
+ @Override
+ public @BuffState int getBufferingState() {
+ synchronized (mLock) {
+ return mBufferingState;
+ }
+ }
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepare()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ * @throws IllegalArgumentException if the attributes are null or invalid.
+ */
+ @Override
+ public void setAudioAttributes(@NonNull final AudioAttributesCompat attributes) {
+ addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
+ @Override
+ void process() {
+ AudioAttributes attr;
+ synchronized (mLock) {
+ mAudioAttributes = attributes;
+ attr = (AudioAttributes) mAudioAttributes.unwrap();
+ }
+ mPlayer.setAudioAttributes(attr);
+ }
+ });
+ }
+
+ @Override
+ public @NonNull AudioAttributesCompat getAudioAttributes() {
+ synchronized (mLock) {
+ return mAudioAttributes;
+ }
+ }
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setDataSource(@NonNull final DataSourceDesc dsd) {
+ addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ // TODO: setDataSource could update exist data source
+ synchronized (mSrcLock) {
+ mCurrentDSD = dsd;
+ mCurrentSrcId = mSrcIdGenerator++;
+ try {
+ handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
+ } catch (IOException e) {
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets a single data source as described by a DataSourceDesc which will be played
+ * after current data source is finished.
+ *
+ * @param dsd the descriptor of data source you want to play after current one
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setNextDataSource(@NonNull final DataSourceDesc dsd) {
+ addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
+ @Override
+ void process() {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ synchronized (mSrcLock) {
+ mNextDSDs = new ArrayList<DataSourceDesc>(1);
+ mNextDSDs.add(dsd);
+ mNextSrcId = mSrcIdGenerator++;
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
+ }
+ /* FIXME : define and handle state.
+ int state = getMediaPlayer2State();
+ if (state != MEDIAPLAYER2_STATE_IDLE) {
+ synchronized (mSrcLock) {
+ prepareNextDataSource_l();
+ }
+ }
+ */
+ }
+ });
+ }
+
+ /**
+ * Sets a list of data sources to be played sequentially after current data source is done.
+ *
+ * @param dsds the list of data sources you want to play after current one
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if dsds is null or empty, or contains null DataSourceDesc
+ */
+ @Override
+ public void setNextDataSources(@NonNull final List<DataSourceDesc> dsds) {
+ addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
+ @Override
+ void process() {
+ if (dsds == null || dsds.size() == 0) {
+ throw new IllegalArgumentException("data source list cannot be null or empty.");
+ }
+ for (DataSourceDesc dsd : dsds) {
+ if (dsd == null) {
+ throw new IllegalArgumentException(
+ "DataSourceDesc in the source list cannot be null.");
+ }
+ }
+
+ synchronized (mSrcLock) {
+ mNextDSDs = new ArrayList(dsds);
+ mNextSrcId = mSrcIdGenerator++;
+ mNextSourceState = NEXT_SOURCE_STATE_INIT;
+ mNextSourcePlayPending = false;
+ }
+ /* FIXME : define and handle state.
+ int state = getMediaPlayer2State();
+ if (state != MEDIAPLAYER2_STATE_IDLE) {
+ synchronized (mSrcLock) {
+ prepareNextDataSource_l();
+ }
+ }
+ */
+ }
+ });
+ }
+
+ @Override
+ public @NonNull DataSourceDesc getCurrentDataSource() {
+ synchronized (mSrcLock) {
+ return mCurrentDSD;
+ }
+ }
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ @Override
+ public void loopCurrent(final boolean loop) {
+ addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
+ @Override
+ void process() {
+ mPlayer.setLooping(loop);
+ }
+ });
+ }
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed the desired playback speed
+ */
+ @Override
+ public void setPlaybackSpeed(final float speed) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
+ @Override
+ void process() {
+ setPlaybackParamsInternal(getPlaybackParams().setSpeed(speed));
+ }
+ });
+ }
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return getPlaybackParams().getSpeed();
+ }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ @Override
+ public boolean isReversePlaybackSupported() {
+ return false;
+ }
+
+ /**
+ * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+ * on the audio samples.
+ * Note that this volume is specific to the player, and is separate from stream volume
+ * used across the platform.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ @Override
+ public void setPlayerVolume(final float volume) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
+ @Override
+ void process() {
+ mVolume = volume;
+ mPlayer.setVolume(volume, volume);
+ }
+ });
+ }
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ @Override
+ public float getPlayerVolume() {
+ return mVolume;
+ }
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ @Override
+ public float getMaxPlayerVolume() {
+ return 1.0f;
+ }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ @Override
+ public void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb) {
+ if (cb == null) {
+ throw new IllegalArgumentException("Illegal null PlayerEventCallback");
+ }
+ if (e == null) {
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the PlayerEventCallback");
+ }
+ synchronized (mLock) {
+ mPlayerEventCallbackMap.put(cb, e);
+ }
+ }
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ @Override
+ public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+ if (cb == null) {
+ throw new IllegalArgumentException("Illegal null PlayerEventCallback");
+ }
+ synchronized (mLock) {
+ mPlayerEventCallbackMap.remove(cb);
+ }
+ }
+
+ @Override
+ public void notifyWhenCommandLabelReached(final Object label) {
+ addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
+ @Override
+ void process() {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Sets the {@link Surface} to be used as the sink for the video portion of
+ * the media. Setting a Surface will un-set any Surface or SurfaceHolder that
+ * was previously set. A null surface will result in only the audio track
+ * being played.
+ *
+ * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ @Override
+ public void setSurface(final Surface surface) {
+ addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
+ @Override
+ void process() {
+ mPlayer.setSurface(surface);
+ }
+ });
+ }
+
+ /**
+ * Discards all pending commands.
+ */
+ @Override
+ public void clearPendingCommands() {
+ // TODO: implement this.
+ }
+
+ private void addTask(Task task) {
+ synchronized (mTaskLock) {
+ mPendingTasks.add(task);
+ processPendingTask_l();
+ }
+ }
+
+ @GuardedBy("mTaskLock")
+ private void processPendingTask_l() {
+ if (mCurrentTask != null) {
+ return;
+ }
+ if (!mPendingTasks.isEmpty()) {
+ Task task = mPendingTasks.removeFirst();
+ mCurrentTask = task;
+ mTaskHandler.post(task);
+ }
+ }
+
+ private void handleDataSource(boolean isCurrent, @NonNull final DataSourceDesc dsd, long srcId)
+ throws IOException {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+ // TODO: handle the case isCurrent is false.
+ switch (dsd.getType()) {
+ case DataSourceDesc.TYPE_CALLBACK:
+ mPlayer.setDataSource(new MediaDataSource() {
+ Media2DataSource mDataSource = dsd.getMedia2DataSource();
+ @Override
+ public int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException {
+ return mDataSource.readAt(position, buffer, offset, size);
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return mDataSource.getSize();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mDataSource.close();
+ }
+ });
+ break;
+
+ case DataSourceDesc.TYPE_FD:
+ mPlayer.setDataSource(
+ dsd.getFileDescriptor(),
+ dsd.getFileDescriptorOffset(),
+ dsd.getFileDescriptorLength());
+ break;
+
+ case DataSourceDesc.TYPE_URI:
+ mPlayer.setDataSource(
+ dsd.getUriContext(),
+ dsd.getUri(),
+ dsd.getUriHeaders(),
+ dsd.getUriCookies());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return the width of the video, or 0 if there is no video,
+ * no display surface was set, or the width has not been determined
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+ * is available.
+ */
+ @Override
+ public int getVideoWidth() {
+ return mPlayer.getVideoWidth();
+ }
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return the height of the video, or 0 if there is no video,
+ * no display surface was set, or the height has not been determined
+ * yet. The {@code MediaPlayer2EventCallback} can be registered via
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+ * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height
+ * is available.
+ */
+ @Override
+ public int getVideoHeight() {
+ return mPlayer.getVideoHeight();
+ }
+
+ @Override
+ public PersistableBundle getMetrics() {
+ return mPlayer.getMetrics();
+ }
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @throws IllegalArgumentException if params is not supported.
+ */
+ @Override
+ public void setPlaybackParams(@NonNull final PlaybackParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
+ @Override
+ void process() {
+ setPlaybackParamsInternal(params);
+ }
+ });
+ }
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public PlaybackParams getPlaybackParams() {
+ return mPlayer.getPlaybackParams();
+ }
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if params are not supported.
+ */
+ @Override
+ public void setSyncParams(@NonNull final SyncParams params) {
+ addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
+ @Override
+ void process() {
+ mPlayer.setSyncParams(params);
+ }
+ });
+ }
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public SyncParams getSyncParams() {
+ return mPlayer.getSyncParams();
+ }
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * There is at most one active seekTo processed at any time. If there is a to-be-completed
+ * seekTo, new seekTo requests will be queued in such a way that only the last request
+ * is kept. When current seekTo is completed, the queued request will be processed if
+ * that request is different from just-finished seekTo operation, i.e., the requested
+ * position or mode is different.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * has a frame located at the position. When this happens, a frame nearby will be rendered.
+ * If msec is negative, time position zero will be used.
+ * If msec is larger than duration, duration will be used.
+ * @param mode the mode indicating where exactly to seek to.
+ * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp earlier than or the same as msec. Use
+ * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp later than or the same as msec. Use
+ * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp closest to or the same as msec. Use
+ * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+ * or may not be a sync frame but is closest to or the same as msec.
+ * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+ * to the other options if there is no sync frame located at msec.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ @Override
+ public void seekTo(final long msec, @SeekMode final int mode) {
+ addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
+ @Override
+ void process() {
+ mPlayer.seekTo(msec, mode);
+ }
+ });
+ }
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ * @see MediaTimestamp
+ */
+ @Override
+ @Nullable
+ public MediaTimestamp getTimestamp() {
+ return mPlayer.getTimestamp();
+ }
+
+ /**
+ * Resets the MediaPlayer2 to its uninitialized state. After calling
+ * this method, you will have to initialize it again by setting the
+ * data source and calling prepare().
+ */
+ @Override
+ public void reset() {
+ mPlayer.reset();
+ setPlayerState(PLAYER_STATE_IDLE);
+ setBufferingState(BUFFERING_STATE_UNKNOWN);
+ /* FIXME: reset other internal variables. */
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the sessionId is invalid.
+ */
+ @Override
+ public void setAudioSessionId(final int sessionId) {
+ addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
+ @Override
+ void process() {
+ mPlayer.setAudioSessionId(sessionId);
+ }
+ });
+ }
+
+ @Override
+ public int getAudioSessionId() {
+ return mPlayer.getAudioSessionId();
+ }
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ @Override
+ public void attachAuxEffect(final int effectId) {
+ addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
+ @Override
+ void process() {
+ mPlayer.attachAuxEffect(effectId);
+ }
+ });
+ }
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ @Override
+ public void setAuxEffectSendLevel(final float level) {
+ addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
+ @Override
+ void process() {
+ mPlayer.setAuxEffectSendLevel(level);
+ }
+ });
+ }
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see MediaPlayer2#getTrackInfo
+ */
+ public static final class TrackInfoImpl extends TrackInfo {
+ final int mTrackType;
+ final MediaFormat mFormat;
+
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ @Override
+ public int getTrackType() {
+ return mTrackType;
+ }
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ @Override
+ public String getLanguage() {
+ String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
+ return language == null ? "und" : language;
+ }
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ @Override
+ public MediaFormat getFormat() {
+ if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
+ || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ return mFormat;
+ }
+ return null;
+ }
+
+ TrackInfoImpl(Parcel in) {
+ mTrackType = in.readInt();
+ // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
+ // even for audio/video tracks, meaning we only set the mime and language.
+ String mime = in.readString();
+ String language = in.readString();
+ mFormat = MediaFormat.createSubtitleFormat(mime, language);
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
+ }
+ }
+
+ TrackInfoImpl(int type, MediaFormat format) {
+ mTrackType = type;
+ mFormat = format;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ /* package private */ void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTrackType);
+ dest.writeString(getLanguage());
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder(128);
+ out.append(getClass().getName());
+ out.append('{');
+ switch (mTrackType) {
+ case MEDIA_TRACK_TYPE_VIDEO:
+ out.append("VIDEO");
+ break;
+ case MEDIA_TRACK_TYPE_AUDIO:
+ out.append("AUDIO");
+ break;
+ case MEDIA_TRACK_TYPE_TIMEDTEXT:
+ out.append("TIMEDTEXT");
+ break;
+ case MEDIA_TRACK_TYPE_SUBTITLE:
+ out.append("SUBTITLE");
+ break;
+ default:
+ out.append("UNKNOWN");
+ break;
+ }
+ out.append(", " + mFormat.toString());
+ out.append("}");
+ return out.toString();
+ }
+
+ /**
+ * Used to read a TrackInfoImpl from a Parcel.
+ */
+ /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR =
+ new Parcelable.Creator<TrackInfoImpl>() {
+ @Override
+ public TrackInfoImpl createFromParcel(Parcel in) {
+ return new TrackInfoImpl(in);
+ }
+
+ @Override
+ public TrackInfoImpl[] newArray(int size) {
+ return new TrackInfoImpl[size];
+ }
+ };
+
+ };
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return List of track info. The total number of tracks is the array length.
+ * Must be called again if an external timed text source has been added after
+ * addTimedTextSource method is called.
+ * @throws IllegalStateException if it is called in an invalid state.
+ */
+ @Override
+ public List<TrackInfo> getTrackInfo() {
+ MediaPlayer.TrackInfo[] list = mPlayer.getTrackInfo();
+ List<TrackInfo> trackList = new ArrayList<>();
+ for (MediaPlayer.TrackInfo info : list) {
+ trackList.add(new TrackInfoImpl(info.getTrackType(), info.getFormat()));
+ }
+ return trackList;
+ }
+
+ /**
+ * Returns the index of the audio, video, or subtitle track currently selected for playback,
+ * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ @Override
+ public int getSelectedTrack(int trackType) {
+ return mPlayer.getSelectedTrack(trackType);
+ }
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ *
+ * @param index the index of the track to be selected. The valid range of the index
+ * is 0..total number of track - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ * @see MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void selectTrack(final int index) {
+ addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
+ @Override
+ void process() {
+ mPlayer.selectTrack(index);
+ }
+ });
+ }
+
+ /**
+ * Deselect a track.
+ * <p>
+ * Currently, the track must be a timed text track and no audio or video tracks can be
+ * deselected. If the timed text track identified by index has not been
+ * selected before, it throws an exception.
+ * </p>
+ *
+ * @param index the index of the track to be deselected. The valid range of the index
+ * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ * @see MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void deselectTrack(final int index) {
+ addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
+ @Override
+ void process() {
+ mPlayer.deselectTrack(index);
+ }
+ });
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void setMediaPlayer2EventCallback(@NonNull Executor executor,
+ @NonNull MediaPlayer2EventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the MediaPlayer2EventCallback");
+ }
+ synchronized (mLock) {
+ mMp2EventCallbackRecords.add(new Pair(executor, eventCallback));
+ }
+ }
+
+ /**
+ * Clears the {@link MediaPlayer2EventCallback}.
+ */
+ @Override
+ public void clearMediaPlayer2EventCallback() {
+ synchronized (mLock) {
+ mMp2EventCallbackRecords.clear();
+ }
+ }
+
+ // Modular DRM begin
+
+ /**
+ * Register a callback to be invoked for configuration of the DRM object before
+ * the session is created.
+ * The callback will be invoked synchronously during the execution
+ * of {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ @Override
+ public void setOnDrmConfigHelper(final OnDrmConfigHelper listener) {
+ mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
+ @Override
+ public void onDrmConfig(MediaPlayer mp) {
+ /** FIXME: pass the right DSD. */
+ listener.onDrmConfig(MediaPlayer2Impl.this, null);
+ }
+ });
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void setDrmEventCallback(@NonNull Executor executor,
+ @NonNull DrmEventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException(
+ "Illegal null Executor for the MediaPlayer2EventCallback");
+ }
+ synchronized (mLock) {
+ mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
+ }
+ }
+
+ /**
+ * Clears the {@link DrmEventCallback}.
+ */
+ @Override
+ public void clearDrmEventCallback() {
+ synchronized (mLock) {
+ mDrmEventCallbackRecords.clear();
+ }
+ }
+
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before prepare()
+ */
+ @Override
+ public DrmInfo getDrmInfo() {
+ MediaPlayer.DrmInfo info = mPlayer.getDrmInfo();
+ return info == null ? null : new DrmInfoImpl(info.getPssh(), info.getSupportedSchemes());
+ }
+
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@code OnDrmConfigHelper} is registered, it will be called during
+ * preparation to allow configuration of the DRM properties before opening the
+ * DRM session. Note that the callback is called synchronously in the thread that called
+ * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ * @throws IllegalStateException if called before prepare(), or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ @Override
+ public void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException {
+ try {
+ mPlayer.prepareDrm(uuid);
+ } catch (MediaPlayer.ProvisioningNetworkErrorException e) {
+ throw new ProvisioningNetworkErrorException(e.getMessage());
+ } catch (MediaPlayer.ProvisioningServerErrorException e) {
+ throw new ProvisioningServerErrorException(e.getMessage());
+ }
+ }
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ @Override
+ public void releaseDrm() throws NoDrmSchemeException {
+ addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
+ @Override
+ void process() throws NoDrmSchemeException {
+ try {
+ mPlayer.releaseDrm();
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+ });
+ }
+
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideDrmKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @Override
+ @NonNull
+ public MediaDrm.KeyRequest getDrmKeyRequest(@Nullable byte[] keySetId,
+ @Nullable byte[] initData, @Nullable String mimeType, int keyType,
+ @Nullable Map<String, String> optionalParameters)
+ throws NoDrmSchemeException {
+ try {
+ return mPlayer.getKeyRequest(keySetId, initData, mimeType, keyType, optionalParameters);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideDrmKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreDrmKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link #getDrmKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ @Override
+ public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ throws NoDrmSchemeException, DeniedByServerException {
+ try {
+ return mPlayer.provideKeyResponse(keySetId, response);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ @Override
+ public void restoreDrmKeys(@NonNull final byte[] keySetId)
+ throws NoDrmSchemeException {
+ addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
+ @Override
+ void process() throws NoDrmSchemeException {
+ try {
+ mPlayer.restoreKeys(keySetId);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+ });
+ }
+
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ *
+
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ @NonNull
+ public String getDrmPropertyString(@NonNull String propertyName)
+ throws NoDrmSchemeException {
+ try {
+ return mPlayer.getDrmPropertyString(propertyName);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ *
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ public void setDrmPropertyString(@NonNull String propertyName,
+ @NonNull String value)
+ throws NoDrmSchemeException {
+ try {
+ mPlayer.setDrmPropertyString(propertyName, value);
+ } catch (MediaPlayer.NoDrmSchemeException e) {
+ throw new NoDrmSchemeException(e.getMessage());
+ }
+ }
+
+ private void setPlaybackParamsInternal(final PlaybackParams params) {
+ PlaybackParams current = mPlayer.getPlaybackParams();
+ mPlayer.setPlaybackParams(params);
+ if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
+ }
+ });
+ }
+ }
+
+ private void setPlayerState(@PlayerState final int state) {
+ synchronized (mLock) {
+ if (mPlayerState == state) {
+ return;
+ }
+ mPlayerState = state;
+ }
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
+ }
+ });
+ }
+
+ private void setBufferingState(@BuffState final int state) {
+ synchronized (mLock) {
+ if (mBufferingState == state) {
+ return;
+ }
+ mBufferingState = state;
+ }
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
+ }
+ });
+ }
+
+ private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
+ List<Pair<Executor, MediaPlayer2EventCallback>> records;
+ synchronized (mLock) {
+ records = new ArrayList<>(mMp2EventCallbackRecords);
+ }
+ for (final Pair<Executor, MediaPlayer2EventCallback> record : records) {
+ record.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(record.second);
+ }
+ });
+ }
+ }
+
+ private void notifyPlayerEvent(final PlayerEventNotifier notifier) {
+ ArrayMap<PlayerEventCallback, Executor> map;
+ synchronized (mLock) {
+ map = new ArrayMap<>(mPlayerEventCallbackMap);
+ }
+ final int callbackCount = map.size();
+ for (int i = 0; i < callbackCount; i++) {
+ final Executor executor = map.valueAt(i);
+ final PlayerEventCallback cb = map.keyAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ notifier.notify(cb);
+ }
+ });
+ }
+ }
+
+ private interface Mp2EventNotifier {
+ void notify(MediaPlayer2EventCallback callback);
+ }
+
+ private interface PlayerEventNotifier {
+ void notify(PlayerEventCallback callback);
+ }
+
+ private void setUpListeners() {
+ mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ setPlayerState(PLAYER_STATE_PAUSED);
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback callback) {
+ callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
+ }
+ });
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
+ }
+ });
+ synchronized (mTaskLock) {
+ if (mCurrentTask != null
+ && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
+ && mCurrentTask.mDSD == mCurrentDSD
+ && mCurrentTask.mNeedToWaitForEventToComplete) {
+ mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ }
+ });
+ mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
+ @Override
+ public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
+ }
+ });
+ }
+ });
+ mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ switch (what) {
+ case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ MEDIA_INFO_VIDEO_RENDERING_START, 0);
+ }
+ });
+ break;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_START:
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
+ break;
+ case MediaPlayer.MEDIA_INFO_BUFFERING_END:
+ setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+ break;
+ }
+ return false;
+ }
+ });
+ mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ setPlayerState(PLAYER_STATE_PAUSED);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
+ 0);
+ }
+ });
+ }
+ });
+ mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, final int what, final int extra) {
+ setPlayerState(PLAYER_STATE_ERROR);
+ setBufferingState(BUFFERING_STATE_UNKNOWN);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
+ cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+ }
+ });
+ return true;
+ }
+ });
+ mPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ synchronized (mTaskLock) {
+ if (mCurrentTask != null
+ && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
+ && mCurrentTask.mNeedToWaitForEventToComplete) {
+ mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ final long seekPos = getCurrentPosition();
+ notifyPlayerEvent(new PlayerEventNotifier() {
+ @Override
+ public void notify(PlayerEventCallback cb) {
+ // TODO: The actual seeked position might be different from the
+ // requested position. Clarify which one is expected here.
+ cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
+ }
+ });
+ }
+ });
+ mPlayer.setOnTimedMetaDataAvailableListener(
+ new MediaPlayer.OnTimedMetaDataAvailableListener() {
+ @Override
+ public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onTimedMetaDataAvailable(
+ MediaPlayer2Impl.this, mCurrentDSD, data);
+ }
+ });
+ }
+ });
+ mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
+ }
+ });
+ return true;
+ }
+ });
+ mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, final int percent) {
+ if (percent >= 100) {
+ setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
+ }
+ mBufferedPercentageCurrent.set(percent);
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
+ MEDIA_INFO_BUFFERING_UPDATE, percent);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Encapsulates the DRM properties of the source.
+ */
+ public static final class DrmInfoImpl extends DrmInfo {
+ private Map<UUID, byte[]> mMapPssh;
+ private UUID[] mSupportedSchemes;
+
+ /**
+ * Returns the PSSH info of the data source for each supported DRM scheme.
+ */
+ @Override
+ public Map<UUID, byte[]> getPssh() {
+ return mMapPssh;
+ }
+
+ /**
+ * Returns the intersection of the data source and the device DRM schemes.
+ * It effectively identifies the subset of the source's DRM schemes which
+ * are supported by the device too.
+ */
+ @Override
+ public List<UUID> getSupportedSchemes() {
+ return Arrays.asList(mSupportedSchemes);
+ }
+
+ private DrmInfoImpl(Map<UUID, byte[]> pssh, UUID[] supportedSchemes) {
+ mMapPssh = pssh;
+ mSupportedSchemes = supportedSchemes;
+ }
+
+ private DrmInfoImpl(Parcel parcel) {
+ Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize());
+
+ int psshsize = parcel.readInt();
+ byte[] pssh = new byte[psshsize];
+ parcel.readByteArray(pssh);
+
+ Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh));
+ mMapPssh = parsePSSH(pssh, psshsize);
+ Log.v(TAG, "DrmInfoImpl() PSSH: " + mMapPssh);
+
+ int supportedDRMsCount = parcel.readInt();
+ mSupportedSchemes = new UUID[supportedDRMsCount];
+ for (int i = 0; i < supportedDRMsCount; i++) {
+ byte[] uuid = new byte[16];
+ parcel.readByteArray(uuid);
+
+ mSupportedSchemes[i] = bytesToUUID(uuid);
+
+ Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: "
+ + mSupportedSchemes[i]);
+ }
+
+ Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize
+ + " supportedDRMsCount: " + supportedDRMsCount);
+ }
+
+ private DrmInfoImpl makeCopy() {
+ return new DrmInfoImpl(this.mMapPssh, this.mSupportedSchemes);
+ }
+
+ private String arrToHex(byte[] bytes) {
+ String out = "0x";
+ for (int i = 0; i < bytes.length; i++) {
+ out += String.format("%02x", bytes[i]);
+ }
+
+ return out;
+ }
+
+ private UUID bytesToUUID(byte[] uuid) {
+ long msb = 0, lsb = 0;
+ for (int i = 0; i < 8; i++) {
+ msb |= (((long) uuid[i] & 0xff) << (8 * (7 - i)));
+ lsb |= (((long) uuid[i + 8] & 0xff) << (8 * (7 - i)));
+ }
+
+ return new UUID(msb, lsb);
+ }
+
+ private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) {
+ Map<UUID, byte[]> result = new HashMap<UUID, byte[]>();
+
+ final int uuidSize = 16;
+ final int dataLenSize = 4;
+
+ int len = psshsize;
+ int numentries = 0;
+ int i = 0;
+
+ while (len > 0) {
+ if (len < uuidSize) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse "
+ + "UUID: (%d < 16) pssh: %d", len, psshsize));
+ return null;
+ }
+
+ byte[] subset = Arrays.copyOfRange(pssh, i, i + uuidSize);
+ UUID uuid = bytesToUUID(subset);
+ i += uuidSize;
+ len -= uuidSize;
+
+ // get data length
+ if (len < 4) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse "
+ + "datalen: (%d < 4) pssh: %d", len, psshsize));
+ return null;
+ }
+
+ subset = Arrays.copyOfRange(pssh, i, i + dataLenSize);
+ int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN)
+ ? ((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16)
+ | ((subset[1] & 0xff) << 8) | (subset[0] & 0xff)
+ : ((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16)
+ | ((subset[2] & 0xff) << 8) | (subset[3] & 0xff);
+ i += dataLenSize;
+ len -= dataLenSize;
+
+ if (len < datalen) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse "
+ + "data: (%d < %d) pssh: %d", len, datalen, psshsize));
+ return null;
+ }
+
+ byte[] data = Arrays.copyOfRange(pssh, i, i + datalen);
+
+ // skip the data
+ i += datalen;
+ len -= datalen;
+
+ Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d",
+ numentries, uuid, arrToHex(data), psshsize));
+ numentries++;
+ result.put(uuid, data);
+ }
+
+ return result;
+ }
+
+ }; // DrmInfoImpl
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException {
+ public NoDrmSchemeExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningNetworkErrorExceptionImpl
+ extends ProvisioningNetworkErrorException {
+ public ProvisioningNetworkErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningServerErrorExceptionImpl
+ extends ProvisioningServerErrorException {
+ public ProvisioningServerErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private abstract class Task implements Runnable {
+ private final int mMediaCallType;
+ private final boolean mNeedToWaitForEventToComplete;
+ private DataSourceDesc mDSD;
+
+ Task(int mediaCallType, boolean needToWaitForEventToComplete) {
+ mMediaCallType = mediaCallType;
+ mNeedToWaitForEventToComplete = needToWaitForEventToComplete;
+ }
+
+ abstract void process() throws IOException, NoDrmSchemeException;
+
+ @Override
+ public void run() {
+ int status = CALL_STATUS_NO_ERROR;
+ try {
+ process();
+ } catch (IllegalStateException e) {
+ status = CALL_STATUS_INVALID_OPERATION;
+ } catch (IllegalArgumentException e) {
+ status = CALL_STATUS_BAD_VALUE;
+ } catch (SecurityException e) {
+ status = CALL_STATUS_PERMISSION_DENIED;
+ } catch (IOException e) {
+ status = CALL_STATUS_ERROR_IO;
+ } catch (NoDrmSchemeException e) {
+ status = CALL_STATUS_NO_DRM_SCHEME;
+ } catch (Exception e) {
+ status = CALL_STATUS_ERROR_UNKNOWN;
+ }
+ synchronized (mSrcLock) {
+ mDSD = mCurrentDSD;
+ }
+
+ if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
+
+ sendCompleteNotification(status);
+
+ synchronized (mTaskLock) {
+ mCurrentTask = null;
+ processPendingTask_l();
+ }
+ }
+ }
+
+ private void sendCompleteNotification(final int status) {
+ // In {@link #notifyWhenCommandLabelReached} case, a separate callback
+ // {#link #onCommandLabelReached} is already called in {@code process()}.
+ if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
+ return;
+ }
+ notifyMediaPlayer2Event(new Mp2EventNotifier() {
+ @Override
+ public void notify(MediaPlayer2EventCallback cb) {
+ cb.onCallCompleted(
+ MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
+ }
+ });
+ }
+ };
+}
diff --git a/androidx/media/MediaPlayer2Test.java b/androidx/media/MediaPlayer2Test.java
new file mode 100644
index 00000000..f565e8a3
--- /dev/null
+++ b/androidx/media/MediaPlayer2Test.java
@@ -0,0 +1,2262 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.hardware.Camera;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaRecorder;
+import android.media.MediaTimestamp;
+import android.media.PlaybackParams;
+import android.media.SyncParams;
+import android.media.audiofx.AudioEffect;
+import android.media.audiofx.Visualizer;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import androidx.media.test.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+public class MediaPlayer2Test extends MediaPlayer2TestBase {
+
+ private static final String LOG_TAG = "MediaPlayer2Test";
+
+ private static final int RECORDED_VIDEO_WIDTH = 176;
+ private static final int RECORDED_VIDEO_HEIGHT = 144;
+ private static final long RECORDED_DURATION_MS = 3000;
+ private static final float FLOAT_TOLERANCE = .0001f;
+
+ private String mRecordedFilePath;
+ private final Vector<Integer> mSubtitleTrackIndex = new Vector<>();
+ private final Monitor mOnSubtitleDataCalled = new Monitor();
+ private int mSelectedSubtitleIndex;
+
+ private File mOutFile;
+ private Camera mCamera;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mRecordedFilePath = new File(Environment.getExternalStorageDirectory(),
+ "mediaplayer_record.out").getAbsolutePath();
+ mOutFile = new File(mRecordedFilePath);
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ if (mOutFile != null && mOutFile.exists()) {
+ mOutFile.delete();
+ }
+ }
+
+ // Bug 13652927
+ public void testVorbisCrash() throws Exception {
+ MediaPlayer2 mp = mPlayer;
+ MediaPlayer2 mp2 = mPlayer2;
+ AssetFileDescriptor afd2 = mResources.openRawResourceFd(R.raw.testmp3_2);
+ mp2.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(afd2.getFileDescriptor(), afd2.getStartOffset(), afd2.getLength())
+ .build());
+ final Monitor onPrepareCalled = new Monitor();
+ final Monitor onErrorCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ onPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ onErrorCalled.signal();
+ }
+ };
+ mp2.setMediaPlayer2EventCallback(mExecutor, ecb);
+ mp2.prepare();
+ onPrepareCalled.waitForSignal();
+ afd2.close();
+ mp2.clearMediaPlayer2EventCallback();
+
+ mp2.loopCurrent(true);
+ mp2.play();
+
+ for (int i = 0; i < 20; i++) {
+ try {
+ AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.bug13652927);
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
+ afd.getLength())
+ .build());
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+ onPrepareCalled.reset();
+ mp.prepare();
+ onErrorCalled.waitForSignal();
+ afd.close();
+ } catch (Exception e) {
+ // expected to fail
+ Log.i("@@@", "failed: " + e);
+ }
+ Thread.sleep(500);
+ assertTrue("media player died",
+ mp2.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ mp.reset();
+ }
+ }
+
+ public void testPlayNullSourcePath() throws Exception {
+ final Monitor onSetDataSourceCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
+ assertTrue(status != MediaPlayer2.CALL_STATUS_NO_ERROR);
+ onSetDataSourceCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ onSetDataSourceCalled.reset();
+ mPlayer.setDataSource((DataSourceDesc) null);
+ onSetDataSourceCalled.waitForSignal();
+ }
+
+ public void testPlayAudioFromDataURI() throws Exception {
+ final int mp3Duration = 34909;
+ final int tolerance = 70;
+ final int seekDuration = 100;
+
+ // This is "R.raw.testmp3_2", base64-encoded.
+ final int resid = R.raw.testmp3_3;
+
+ InputStream is = mContext.getResources().openRawResource(resid);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("data:;base64,");
+ builder.append(reader.readLine());
+ Uri uri = Uri.parse(builder.toString());
+
+ MediaPlayer2 mp = createMediaPlayer2(mContext, uri);
+
+ final Monitor onPrepareCalled = new Monitor();
+ final Monitor onPlayCalled = new Monitor();
+ final Monitor onSeekToCalled = new Monitor();
+ final Monitor onLoopCurrentCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ onPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ onPlayCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+ onLoopCurrentCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ onSeekToCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ try {
+ AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mp.setAudioAttributes(attributes);
+ /* FIXME: ensure screen is on while testing.
+ mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
+ */
+
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ /* FIXME: what's API for checking loop state?
+ assertFalse(mp.isLooping());
+ */
+ onLoopCurrentCalled.reset();
+ mp.loopCurrent(true);
+ onLoopCurrentCalled.waitForSignal();
+ /* FIXME: what's API for checking loop state?
+ assertTrue(mp.isLooping());
+ */
+
+ assertEquals(mp3Duration, mp.getDuration(), tolerance);
+ long pos = mp.getCurrentPosition();
+ assertTrue(pos >= 0);
+ assertTrue(pos < mp3Duration - seekDuration);
+
+ onSeekToCalled.reset();
+ mp.seekTo(pos + seekDuration, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ onSeekToCalled.waitForSignal();
+ assertEquals(pos + seekDuration, mp.getCurrentPosition(), tolerance);
+
+ // test pause and restart
+ mp.pause();
+ Thread.sleep(SLEEP_TIME);
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // test stop and restart
+ mp.reset();
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(mContext, uri)
+ .build());
+ onPrepareCalled.reset();
+ mp.prepare();
+ onPrepareCalled.waitForSignal();
+
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // waiting to complete
+ while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Thread.sleep(SLEEP_TIME);
+ }
+ } finally {
+ mp.close();
+ }
+ }
+
+ public void testPlayAudio() throws Exception {
+ final int resid = R.raw.testmp3_2;
+ final int mp3Duration = 34909;
+ final int tolerance = 70;
+ final int seekDuration = 100;
+
+ MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+
+ final Monitor onPrepareCalled = new Monitor();
+ final Monitor onPlayCalled = new Monitor();
+ final Monitor onSeekToCalled = new Monitor();
+ final Monitor onLoopCurrentCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ onPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ onPlayCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+ onLoopCurrentCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ onSeekToCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ try {
+ AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mp.setAudioAttributes(attributes);
+
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ //assertFalse(mp.isLooping());
+ onLoopCurrentCalled.reset();
+ mp.loopCurrent(true);
+ onLoopCurrentCalled.waitForSignal();
+ //assertTrue(mp.isLooping());
+
+ assertEquals(mp3Duration, mp.getDuration(), tolerance);
+ long pos = mp.getCurrentPosition();
+ assertTrue(pos >= 0);
+ assertTrue(pos < mp3Duration - seekDuration);
+
+ onSeekToCalled.reset();
+ mp.seekTo(pos + seekDuration, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ onSeekToCalled.waitForSignal();
+ assertEquals(pos + seekDuration, mp.getCurrentPosition(), tolerance);
+
+ // test pause and restart
+ mp.pause();
+ Thread.sleep(SLEEP_TIME);
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // test stop and restart
+ mp.reset();
+ AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+ .build());
+
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+ onPrepareCalled.reset();
+ mp.prepare();
+ onPrepareCalled.waitForSignal();
+ afd.close();
+
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // waiting to complete
+ while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Thread.sleep(SLEEP_TIME);
+ }
+ } catch (Exception e) {
+ throw e;
+ } finally {
+ mp.close();
+ }
+ }
+
+ /*
+ public void testConcurentPlayAudio() throws Exception {
+ final int resid = R.raw.test1m1s; // MP3 longer than 1m are usualy offloaded
+ final int tolerance = 70;
+
+ List<MediaPlayer2> mps = Stream.generate(() -> createMediaPlayer2(mContext, resid))
+ .limit(5).collect(Collectors.toList());
+
+ try {
+ for (MediaPlayer2 mp : mps) {
+ Monitor onPlayCalled = new Monitor();
+ Monitor onLoopCurrentCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb =
+ new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+ int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ onPlayCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+ onLoopCurrentCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ AudioAttributes attributes = new AudioAttributes.Builder()
+ .setInternalLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mp.setAudioAttributes(attributes);
+ mp.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);
+
+ assertFalse(mp.isPlaying());
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.isPlaying());
+
+ assertFalse(mp.isLooping());
+ onLoopCurrentCalled.reset();
+ mp.loopCurrent(true);
+ onLoopCurrentCalled.waitForSignal();
+ assertTrue(mp.isLooping());
+
+ long pos = mp.getCurrentPosition();
+ assertTrue(pos >= 0);
+
+ Thread.sleep(SLEEP_TIME); // Delay each track to be able to ear them
+ }
+ // Check that all mp3 are playing concurrently here
+ for (MediaPlayer2 mp : mps) {
+ long pos = mp.getCurrentPosition();
+ Thread.sleep(SLEEP_TIME);
+ assertEquals(pos + SLEEP_TIME, mp.getCurrentPosition(), tolerance);
+ }
+ } finally {
+ mps.forEach(MediaPlayer2::close);
+ }
+ }
+ */
+
+ public void testPlayAudioLooping() throws Exception {
+ final int resid = R.raw.testmp3;
+
+ MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+ try {
+ AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mp.setAudioAttributes(attributes);
+ mp.loopCurrent(true);
+ final Monitor onCompletionCalled = new Monitor();
+ final Monitor onPlayCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb =
+ new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd,
+ int what, int extra) {
+ Log.i("@@@", "got oncompletion");
+ onCompletionCalled.signal();
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+ int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ onPlayCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ assertFalse(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ onPlayCalled.reset();
+ mp.play();
+ onPlayCalled.waitForSignal();
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ long duration = mp.getDuration();
+ Thread.sleep(duration * 4); // allow for several loops
+ assertTrue(mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertEquals("wrong number of completion signals", 0,
+ onCompletionCalled.getNumSignal());
+ mp.loopCurrent(false);
+
+ // wait for playback to finish
+ while (mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Thread.sleep(SLEEP_TIME);
+ }
+ assertEquals("wrong number of completion signals", 1,
+ onCompletionCalled.getNumSignal());
+ } finally {
+ mp.close();
+ }
+ }
+
+ public void testPlayMidi() throws Exception {
+ final int resid = R.raw.midi8sec;
+ final int midiDuration = 8000;
+ final int tolerance = 70;
+ final int seekDuration = 1000;
+
+ MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+
+ final Monitor onPrepareCalled = new Monitor();
+ final Monitor onSeekToCalled = new Monitor();
+ final Monitor onLoopCurrentCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb =
+ new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ onPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+ int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_LOOP_CURRENT) {
+ onLoopCurrentCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ onSeekToCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ try {
+ AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mp.setAudioAttributes(attributes);
+
+ mp.play();
+
+ /* FIXME: what's API for checking loop state?
+ assertFalse(mp.isLooping());
+ */
+ onLoopCurrentCalled.reset();
+ mp.loopCurrent(true);
+ onLoopCurrentCalled.waitForSignal();
+ /* FIXME: what's API for checking loop state?
+ assertTrue(mp.isLooping());
+ */
+
+ assertEquals(midiDuration, mp.getDuration(), tolerance);
+ long pos = mp.getCurrentPosition();
+ assertTrue(pos >= 0);
+ assertTrue(pos < midiDuration - seekDuration);
+
+ onSeekToCalled.reset();
+ mp.seekTo(pos + seekDuration, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ onSeekToCalled.waitForSignal();
+ assertEquals(pos + seekDuration, mp.getCurrentPosition(), tolerance);
+
+ // test stop and restart
+ mp.reset();
+ AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+ .build());
+
+ mp.setMediaPlayer2EventCallback(mExecutor, ecb);
+ onPrepareCalled.reset();
+ mp.prepare();
+ onPrepareCalled.waitForSignal();
+ afd.close();
+
+ mp.play();
+
+ Thread.sleep(SLEEP_TIME);
+ } finally {
+ mp.close();
+ }
+ }
+
+ static class OutputListener {
+ int mSession;
+ AudioEffect mVc;
+ Visualizer mVis;
+ byte [] mVisData;
+ boolean mSoundDetected;
+ OutputListener(int session) {
+ mSession = session;
+ /* FIXME: find out a public API for replacing AudioEffect contructor.
+ // creating a volume controller on output mix ensures that ro.audio.silent mutes
+ // audio after the effects and not before
+ mVc = new AudioEffect(
+ AudioEffect.EFFECT_TYPE_NULL,
+ UUID.fromString("119341a0-8469-11df-81f9-0002a5d5c51b"),
+ 0,
+ session);
+ mVc.setEnabled(true);
+ */
+ mVis = new Visualizer(session);
+ int size = 256;
+ int[] range = Visualizer.getCaptureSizeRange();
+ if (size < range[0]) {
+ size = range[0];
+ }
+ if (size > range[1]) {
+ size = range[1];
+ }
+ assertTrue(mVis.setCaptureSize(size) == Visualizer.SUCCESS);
+
+ mVis.setDataCaptureListener(new Visualizer.OnDataCaptureListener() {
+ @Override
+ public void onWaveFormDataCapture(Visualizer visualizer,
+ byte[] waveform, int samplingRate) {
+ if (!mSoundDetected) {
+ for (int i = 0; i < waveform.length; i++) {
+ // 8 bit unsigned PCM, zero level is at 128, which is -128 when
+ // seen as a signed byte
+ if (waveform[i] != -128) {
+ mSoundDetected = true;
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate) {
+ }
+ }, 10000 /* milliHertz */, true /* PCM */, false /* FFT */);
+ assertTrue(mVis.setEnabled(true) == Visualizer.SUCCESS);
+ }
+
+ void reset() {
+ mSoundDetected = false;
+ }
+
+ boolean heardSound() {
+ return mSoundDetected;
+ }
+
+ void release() {
+ mVis.release();
+ /* FIXME: find out a public API for replacing AudioEffect contructor.
+ mVc.release();
+ */
+ }
+ }
+
+ public void testPlayAudioTwice() throws Exception {
+
+ final int resid = R.raw.camera_click;
+
+ MediaPlayer2 mp = createMediaPlayer2(mContext, resid);
+ try {
+ AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mp.setAudioAttributes(attributes);
+
+ OutputListener listener = new OutputListener(mp.getAudioSessionId());
+
+ Thread.sleep(SLEEP_TIME);
+ assertFalse("noise heard before test started", listener.heardSound());
+
+ mp.play();
+ Thread.sleep(SLEEP_TIME);
+ assertFalse("player was still playing after " + SLEEP_TIME + " ms",
+ mp.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue("nothing heard while test ran", listener.heardSound());
+ listener.reset();
+ mp.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mp.play();
+ Thread.sleep(SLEEP_TIME);
+ assertTrue("nothing heard when sound was replayed", listener.heardSound());
+ listener.release();
+ } finally {
+ mp.close();
+ }
+ }
+
+ @Test
+ @LargeTest
+ public void testPlayVideo() throws Exception {
+ playVideoTest(R.raw.testvideo, 352, 288);
+ }
+
+ /**
+ * Test for reseting a surface during video playback
+ * After reseting, the video should continue playing
+ * from the time setDisplay() was called
+ */
+ @Test
+ @LargeTest
+ public void testVideoSurfaceResetting() throws Exception {
+ final int tolerance = 150;
+ final int audioLatencyTolerance = 1000; /* covers audio path latency variability */
+ final int seekPos = 4760; // This is the I-frame position
+
+ final CountDownLatch seekDone = new CountDownLatch(1);
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ seekDone.countDown();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ if (!checkLoadResource(R.raw.testvideo)) {
+ return; // skip;
+ }
+ playLoadedVideo(352, 288, -1);
+
+ Thread.sleep(SLEEP_TIME);
+
+ long posBefore = mPlayer.getCurrentPosition();
+ mPlayer.setSurface(mActivity.getSurfaceHolder2().getSurface());
+ long posAfter = mPlayer.getCurrentPosition();
+
+ /* temporarily disable timestamp checking because MediaPlayer2 now seeks to I-frame
+ * position, instead of requested position. setDisplay invovles a seek operation
+ * internally.
+ */
+ // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
+ // assertEquals(posAfter, posBefore, tolerance);
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ Thread.sleep(SLEEP_TIME);
+
+ mPlayer.seekTo(seekPos, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ seekDone.await();
+ posAfter = mPlayer.getCurrentPosition();
+ assertEquals(seekPos, posAfter, tolerance + audioLatencyTolerance);
+
+ Thread.sleep(SLEEP_TIME / 2);
+ posBefore = mPlayer.getCurrentPosition();
+ mPlayer.setSurface(null);
+ posAfter = mPlayer.getCurrentPosition();
+ // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
+ // assertEquals(posAfter, posBefore, tolerance);
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ Thread.sleep(SLEEP_TIME);
+
+ posBefore = mPlayer.getCurrentPosition();
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+ posAfter = mPlayer.getCurrentPosition();
+
+ // TODO: uncomment out line below when MediaPlayer2 can seek to requested position.
+ // assertEquals(posAfter, posBefore, tolerance);
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ Thread.sleep(SLEEP_TIME);
+ }
+
+ public void testRecordedVideoPlayback0() throws Exception {
+ testRecordedVideoPlaybackWithAngle(0);
+ }
+
+ public void testRecordedVideoPlayback90() throws Exception {
+ testRecordedVideoPlaybackWithAngle(90);
+ }
+
+ public void testRecordedVideoPlayback180() throws Exception {
+ testRecordedVideoPlaybackWithAngle(180);
+ }
+
+ public void testRecordedVideoPlayback270() throws Exception {
+ testRecordedVideoPlaybackWithAngle(270);
+ }
+
+ private boolean hasCamera() {
+ return mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
+ }
+
+ private void testRecordedVideoPlaybackWithAngle(int angle) throws Exception {
+ int width = RECORDED_VIDEO_WIDTH;
+ int height = RECORDED_VIDEO_HEIGHT;
+ final String file = mRecordedFilePath;
+ final long durationMs = RECORDED_DURATION_MS;
+
+ if (!hasCamera()) {
+ return;
+ }
+
+ boolean isSupported = false;
+ mCamera = Camera.open(0);
+ Camera.Parameters parameters = mCamera.getParameters();
+ List<Camera.Size> videoSizes = parameters.getSupportedVideoSizes();
+ // getSupportedVideoSizes returns null when separate video/preview size
+ // is not supported.
+ if (videoSizes == null) {
+ videoSizes = parameters.getSupportedPreviewSizes();
+ }
+ for (Camera.Size size : videoSizes) {
+ if (size.width == width && size.height == height) {
+ isSupported = true;
+ break;
+ }
+ }
+ mCamera.release();
+ mCamera = null;
+ if (!isSupported) {
+ width = videoSizes.get(0).width;
+ height = videoSizes.get(0).height;
+ }
+ checkOrientation(angle);
+ recordVideo(width, height, angle, file, durationMs);
+ checkDisplayedVideoSize(width, height, angle, file);
+ checkVideoRotationAngle(angle, file);
+ }
+
+ private void checkOrientation(int angle) throws Exception {
+ assertTrue(angle >= 0);
+ assertTrue(angle < 360);
+ assertTrue((angle % 90) == 0);
+ }
+
+ private void recordVideo(
+ int w, int h, int angle, String file, long durationMs) throws Exception {
+
+ MediaRecorder recorder = new MediaRecorder();
+ recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+ recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
+ recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
+ recorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
+ recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
+ recorder.setOutputFile(file);
+ recorder.setOrientationHint(angle);
+ recorder.setVideoSize(w, h);
+ recorder.setPreviewDisplay(mActivity.getSurfaceHolder2().getSurface());
+ recorder.prepare();
+ recorder.start();
+ Thread.sleep(durationMs);
+ recorder.stop();
+ recorder.release();
+ recorder = null;
+ }
+
+ private void checkDisplayedVideoSize(
+ int w, int h, int angle, String file) throws Exception {
+
+ int displayWidth = w;
+ int displayHeight = h;
+ if ((angle % 180) != 0) {
+ displayWidth = h;
+ displayHeight = w;
+ }
+ playVideoTest(file, displayWidth, displayHeight);
+ }
+
+ private void checkVideoRotationAngle(int angle, String file) {
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(file);
+ String rotation = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+ retriever.release();
+ retriever = null;
+ assertNotNull(rotation);
+ assertEquals(Integer.parseInt(rotation), angle);
+ }
+
+ public void testPlaylist() throws Exception {
+ if (!checkLoadResource(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+ return; // skip
+ }
+ final DataSourceDesc dsd1 = createDataSourceDesc(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz);
+ final DataSourceDesc dsd2 = createDataSourceDesc(
+ R.raw.testvideo);
+ ArrayList<DataSourceDesc> nextDSDs = new ArrayList<DataSourceDesc>(2);
+ nextDSDs.add(dsd2);
+ nextDSDs.add(dsd1);
+
+ mPlayer.setNextDataSources(nextDSDs);
+
+ final Monitor onCompletion1Called = new Monitor();
+ final Monitor onCompletion2Called = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ Log.i(LOG_TAG, "testPlaylist: prepared dsd MediaId=" + dsd.getMediaId());
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ if (dsd == dsd1) {
+ onCompletion1Called.signal();
+ } else if (dsd == dsd2) {
+ onCompletion2Called.signal();
+ } else {
+ mOnCompletionCalled.signal();
+ }
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mOnCompletionCalled.reset();
+ onCompletion1Called.reset();
+ onCompletion2Called.reset();
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mPlayer.prepare();
+
+ mPlayer.play();
+
+ mOnCompletionCalled.waitForSignal();
+ onCompletion2Called.waitForSignal();
+ onCompletion1Called.waitForSignal();
+
+ mPlayer.reset();
+ }
+
+ // setPlaybackParams() with non-zero speed should NOT start playback.
+ // TODO: enable this test when MediaPlayer2.setPlaybackParams() is fixed
+ /*
+ public void testSetPlaybackParamsPositiveSpeed() throws Exception {
+ if (!checkLoadResource(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+ return; // skip
+ }
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ mOnCompletionCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ mOnSeekCompleteCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mOnCompletionCalled.reset();
+ mPlayer.setDisplay(mActivity.getSurfaceHolder());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mOnSeekCompleteCalled.reset();
+ mPlayer.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mOnSeekCompleteCalled.waitForSignal();
+
+ final float playbackRate = 1.0f;
+
+ int playTime = 2000; // The testing clip is about 10 second long.
+ mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+ assertTrue("MediaPlayer2 should be playing", mPlayer.isPlaying());
+ Thread.sleep(playTime);
+ assertTrue("MediaPlayer2 should still be playing",
+ mPlayer.getCurrentPosition() > 0);
+
+ long duration = mPlayer.getDuration();
+ mOnSeekCompleteCalled.reset();
+ mPlayer.seekTo(duration - 1000, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mOnSeekCompleteCalled.waitForSignal();
+
+ mOnCompletionCalled.waitForSignal();
+ assertFalse("MediaPlayer2 should not be playing", mPlayer.isPlaying());
+ long eosPosition = mPlayer.getCurrentPosition();
+
+ mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+ assertTrue("MediaPlayer2 should be playing after EOS", mPlayer.isPlaying());
+ Thread.sleep(playTime);
+ long position = mPlayer.getCurrentPosition();
+ assertTrue("MediaPlayer2 should still be playing after EOS",
+ position > 0 && position < eosPosition);
+
+ mPlayer.reset();
+ }
+ */
+
+ @Test
+ @LargeTest
+ public void testPlaybackRate() throws Exception {
+ final int toleranceMs = 1000;
+ if (!checkLoadResource(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+ return; // skip
+ }
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ SyncParams sync = new SyncParams().allowDefaults();
+ mPlayer.setSyncParams(sync);
+ sync = mPlayer.getSyncParams();
+
+ float[] rates = { 0.25f, 0.5f, 1.0f, 2.0f };
+ for (float playbackRate : rates) {
+ mPlayer.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ Thread.sleep(1000);
+ int playTime = 4000; // The testing clip is about 10 second long.
+ mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+ mPlayer.play();
+ Thread.sleep(playTime);
+ PlaybackParams pbp = mPlayer.getPlaybackParams();
+ assertEquals(
+ playbackRate, pbp.getSpeed(),
+ FLOAT_TOLERANCE + playbackRate * sync.getTolerance());
+ assertTrue("MediaPlayer2 should still be playing",
+ mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ long playedMediaDurationMs = mPlayer.getCurrentPosition();
+ int diff = Math.abs((int) (playedMediaDurationMs / playbackRate) - playTime);
+ if (diff > toleranceMs) {
+ fail("Media player had error in playback rate " + playbackRate
+ + ", play time is " + playTime + " vs expected " + playedMediaDurationMs);
+ }
+ mPlayer.pause();
+ pbp = mPlayer.getPlaybackParams();
+ // TODO: pause() should NOT change PlaybackParams.
+ // assertEquals(0.f, pbp.getSpeed(), FLOAT_TOLERANCE);
+ }
+ mPlayer.reset();
+ }
+
+ @Test
+ @LargeTest
+ public void testSeekModes() throws Exception {
+ // This clip has 2 I frames at 66687us and 4299687us.
+ if (!checkLoadResource(
+ R.raw.bbb_s1_320x240_mp4_h264_mp2_800kbps_30fps_aac_lc_5ch_240kbps_44100hz)) {
+ return; // skip
+ }
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ mOnSeekCompleteCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mOnSeekCompleteCalled.reset();
+ mPlayer.play();
+
+ final long seekPosMs = 3000;
+ final long timeToleranceMs = 100;
+ final long syncTime1Ms = 67;
+ final long syncTime2Ms = 4300;
+
+ // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+ // seek to previous sync or next sync.
+ long cp = runSeekMode(MediaPlayer2.SEEK_CLOSEST, seekPosMs);
+ assertTrue("MediaPlayer2 did not seek to closest position",
+ cp > seekPosMs && cp < syncTime2Ms);
+
+ // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+ // seek to closest position or next sync.
+ cp = runSeekMode(MediaPlayer2.SEEK_PREVIOUS_SYNC, seekPosMs);
+ assertTrue("MediaPlayer2 did not seek to preivous sync position",
+ cp < seekPosMs - timeToleranceMs);
+
+ // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+ // seek to closest position or previous sync.
+ cp = runSeekMode(MediaPlayer2.SEEK_NEXT_SYNC, seekPosMs);
+ assertTrue("MediaPlayer2 did not seek to next sync position",
+ cp > syncTime2Ms - timeToleranceMs);
+
+ // TODO: tighten checking range. For now, ensure mediaplayer doesn't
+ // seek to closest position or previous sync.
+ cp = runSeekMode(MediaPlayer2.SEEK_CLOSEST_SYNC, seekPosMs);
+ assertTrue("MediaPlayer2 did not seek to closest sync position",
+ cp > syncTime2Ms - timeToleranceMs);
+
+ mPlayer.reset();
+ }
+
+ private long runSeekMode(int seekMode, long seekPosMs) throws Exception {
+ final int sleepIntervalMs = 100;
+ int timeRemainedMs = 10000; // total time for testing
+ final int timeToleranceMs = 100;
+
+ mPlayer.seekTo(seekPosMs, seekMode);
+ mOnSeekCompleteCalled.waitForSignal();
+ mOnSeekCompleteCalled.reset();
+ long cp = -seekPosMs;
+ while (timeRemainedMs > 0) {
+ cp = mPlayer.getCurrentPosition();
+ // Wait till MediaPlayer2 starts rendering since MediaPlayer2 caches
+ // seek position as current position.
+ if (cp < seekPosMs - timeToleranceMs || cp > seekPosMs + timeToleranceMs) {
+ break;
+ }
+ timeRemainedMs -= sleepIntervalMs;
+ Thread.sleep(sleepIntervalMs);
+ }
+ assertTrue("MediaPlayer2 did not finish seeking in time for mode " + seekMode,
+ timeRemainedMs > 0);
+ return cp;
+ }
+
+ @Test
+ @LargeTest
+ public void testGetTimestamp() throws Exception {
+ final int toleranceUs = 100000;
+ final float playbackRate = 1.0f;
+ if (!checkLoadResource(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz)) {
+ return; // skip
+ }
+
+ final Monitor onPauseCalled = new Monitor();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PAUSE) {
+ onPauseCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mPlayer.play();
+ mPlayer.setPlaybackParams(new PlaybackParams().setSpeed(playbackRate));
+ Thread.sleep(SLEEP_TIME); // let player get into stable state.
+ long nt1 = System.nanoTime();
+ MediaTimestamp ts1 = mPlayer.getTimestamp();
+ long nt2 = System.nanoTime();
+ assertTrue("Media player should return a valid time stamp", ts1 != null);
+ assertEquals("MediaPlayer2 had error in clockRate " + ts1.getMediaClockRate(),
+ playbackRate, ts1.getMediaClockRate(), 0.001f);
+ assertTrue("The nanoTime of Media timestamp should be taken when getTimestamp is called.",
+ nt1 <= ts1.getAnchorSytemNanoTime() && ts1.getAnchorSytemNanoTime() <= nt2);
+
+ onPauseCalled.reset();
+ mPlayer.pause();
+ onPauseCalled.waitForSignal();
+ ts1 = mPlayer.getTimestamp();
+ assertTrue("Media player should return a valid time stamp", ts1 != null);
+ assertTrue("Media player should have play rate of 0.0f when paused",
+ ts1.getMediaClockRate() == 0.0f);
+
+ mPlayer.seekTo(0, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mPlayer.play();
+ Thread.sleep(SLEEP_TIME); // let player get into stable state.
+ int playTime = 4000; // The testing clip is about 10 second long.
+ ts1 = mPlayer.getTimestamp();
+ assertTrue("Media player should return a valid time stamp", ts1 != null);
+ Thread.sleep(playTime);
+ MediaTimestamp ts2 = mPlayer.getTimestamp();
+ assertTrue("Media player should return a valid time stamp", ts2 != null);
+ assertTrue("The clockRate should not be changed.",
+ ts1.getMediaClockRate() == ts2.getMediaClockRate());
+ assertEquals("MediaPlayer2 had error in timestamp.",
+ ts1.getAnchorMediaTimeUs() + (long) (playTime * ts1.getMediaClockRate() * 1000),
+ ts2.getAnchorMediaTimeUs(), toleranceUs);
+
+ mPlayer.reset();
+ }
+
+ public void testLocalVideo_MKV_H265_1280x720_500kbps_25fps_AAC_Stereo_128kbps_44100Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz, 1280, 720);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_500kbps_25fps_AAC_Stereo_128kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_500kbps_25fps_aac_stereo_128kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_500kbps_30fps_AAC_Stereo_128kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_1000kbps_25fps_AAC_Stereo_128kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_1000kbps_30fps_AAC_Stereo_128kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_1000kbps_30fps_aac_stereo_128kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_1350kbps_25fps_AAC_Stereo_128kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_1350kbps_25fps_aac_stereo_128kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_1350kbps_30fps_AAC_Stereo_128kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_128kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_1350kbps_30fps_AAC_Stereo_128kbps_44110Hz_frag()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_128kbps_44100hz_fragmented,
+ 480, 360);
+ }
+
+ public void testLocalVideo_MP4_H264_480x360_1350kbps_30fps_AAC_Stereo_192kbps_44110Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz, 480, 360);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Mono_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_mono_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Mono_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_mono_24kbps_22050hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_128kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_12fps_AAC_Stereo_128kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Mono_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_mono_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Mono_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_mono_24kbps_22050hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_128kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_56kbps_25fps_AAC_Stereo_128kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_56kbps_25fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Mono_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Mono_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_22050hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_128kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_12fps_AAC_Stereo_128kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Mono_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_mono_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Mono_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_mono_24kbps_22050hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_24kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_24kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_24kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_128kbps_11025Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz, 176, 144);
+ }
+
+ public void testLocalVideo_3gp_H263_176x144_300kbps_25fps_AAC_Stereo_128kbps_22050Hz()
+ throws Exception {
+ playVideoTest(
+ R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_22050hz, 176, 144);
+ }
+
+ private void readSubtitleTracks() throws Exception {
+ mSubtitleTrackIndex.clear();
+ List<MediaPlayer2.TrackInfo> trackInfos = mPlayer.getTrackInfo();
+ if (trackInfos == null || trackInfos.size() == 0) {
+ return;
+ }
+
+ Vector<Integer> subtitleTrackIndex = new Vector<>();
+ for (int i = 0; i < trackInfos.size(); ++i) {
+ assertTrue(trackInfos.get(i) != null);
+ if (trackInfos.get(i).getTrackType()
+ == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ subtitleTrackIndex.add(i);
+ }
+ }
+
+ mSubtitleTrackIndex.addAll(subtitleTrackIndex);
+ }
+
+ private void selectSubtitleTrack(int index) throws Exception {
+ int trackIndex = mSubtitleTrackIndex.get(index);
+ mPlayer.selectTrack(trackIndex);
+ mSelectedSubtitleIndex = index;
+ }
+
+ private void deselectSubtitleTrack(int index) throws Exception {
+ int trackIndex = mSubtitleTrackIndex.get(index);
+ mOnDeselectTrackCalled.reset();
+ mPlayer.deselectTrack(trackIndex);
+ mOnDeselectTrackCalled.waitForSignal();
+ if (mSelectedSubtitleIndex == index) {
+ mSelectedSubtitleIndex = -1;
+ }
+ }
+
+ public void testDeselectTrackForSubtitleTracks() throws Throwable {
+ if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
+ return; // skip;
+ }
+
+ /* FIXME: find out counter part of waitForIdleSync.
+ getInstrumentation().waitForIdleSync();
+ */
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+ mOnInfoCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ mOnSeekCompleteCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_DESELECT_TRACK) {
+ mCallStatus = status;
+ mOnDeselectTrackCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ /* TODO: uncomment once API is available in supportlib.
+ mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+ if (data != null && data.getData() != null) {
+ mOnSubtitleDataCalled.signal();
+ }
+ }
+ });
+ */
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // Closed caption tracks are in-band.
+ // So, those tracks will be found after processing a number of frames.
+ mOnInfoCalled.waitForSignal(1500);
+
+ mOnInfoCalled.reset();
+ mOnInfoCalled.waitForSignal(1500);
+
+ readSubtitleTracks();
+
+ // Run twice to check if repeated selection-deselection on the same track works well.
+ for (int i = 0; i < 2; i++) {
+ // Waits until at least one subtitle is fired. Timeout is 2.5 seconds.
+ selectSubtitleTrack(i);
+ mOnSubtitleDataCalled.reset();
+ assertTrue(mOnSubtitleDataCalled.waitForSignal(2500));
+
+ // Try deselecting track.
+ deselectSubtitleTrack(i);
+ mOnSubtitleDataCalled.reset();
+ assertFalse(mOnSubtitleDataCalled.waitForSignal(1500));
+ }
+
+ // Deselecting unselected track: expected error status
+ mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
+ deselectSubtitleTrack(0);
+ assertTrue(mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR);
+
+ mPlayer.reset();
+ }
+
+ public void testChangeSubtitleTrack() throws Throwable {
+ if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
+ return; // skip;
+ }
+
+ /* TODO: uncomment once API is available in supportlib.
+ mPlayer.setOnSubtitleDataListener(new MediaPlayer2.OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+ if (data != null && data.getData() != null) {
+ mOnSubtitleDataCalled.signal();
+ }
+ }
+ });
+ */
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+ mOnInfoCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // Closed caption tracks are in-band.
+ // So, those tracks will be found after processing a number of frames.
+ mOnInfoCalled.waitForSignal(1500);
+
+ mOnInfoCalled.reset();
+ mOnInfoCalled.waitForSignal(1500);
+
+ readSubtitleTracks();
+
+ // Waits until at least two captions are fired. Timeout is 2.5 sec.
+ selectSubtitleTrack(0);
+ assertTrue(mOnSubtitleDataCalled.waitForCountedSignals(2, 2500) >= 2);
+
+ mOnSubtitleDataCalled.reset();
+ selectSubtitleTrack(1);
+ assertTrue(mOnSubtitleDataCalled.waitForCountedSignals(2, 2500) >= 2);
+
+ mPlayer.reset();
+ }
+
+ @Test
+ @LargeTest
+ public void testGetTrackInfoForVideoWithSubtitleTracks() throws Throwable {
+ if (!checkLoadResource(R.raw.testvideo_with_2_subtitle_tracks)) {
+ return; // skip;
+ }
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) {
+ mOnInfoCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // The media metadata will be changed while playing since closed caption tracks are in-band
+ // and those tracks will be found after processing a number of frames. These tracks will be
+ // found within one second.
+ mOnInfoCalled.waitForSignal(1500);
+
+ mOnInfoCalled.reset();
+ mOnInfoCalled.waitForSignal(1500);
+
+ readSubtitleTracks();
+ assertEquals(2, mSubtitleTrackIndex.size());
+
+ mPlayer.reset();
+ }
+
+ /*
+ * This test assumes the resources being tested are between 8 and 14 seconds long
+ * The ones being used here are 10 seconds long.
+ */
+ public void testResumeAtEnd() throws Throwable {
+ int testsRun = testResumeAtEnd(R.raw.loudsoftmp3)
+ + testResumeAtEnd(R.raw.loudsoftwav)
+ + testResumeAtEnd(R.raw.loudsoftogg)
+ + testResumeAtEnd(R.raw.loudsoftitunes)
+ + testResumeAtEnd(R.raw.loudsoftfaac)
+ + testResumeAtEnd(R.raw.loudsoftaac);
+ }
+
+ // returns 1 if test was run, 0 otherwise
+ private int testResumeAtEnd(int res) throws Throwable {
+ if (!loadResource(res)) {
+ Log.i(LOG_TAG, "testResumeAtEnd: No decoder found for "
+ + mContext.getResources().getResourceEntryName(res) + " --- skipping.");
+ return 0; // skip
+ }
+ mOnCompletionCalled.reset();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ mOnCompletionCalled.signal();
+ mPlayer.play();
+ }
+ }
+ };
+ mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ // skip the first part of the file so we reach EOF sooner
+ mPlayer.seekTo(5000, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mPlayer.play();
+ // sleep long enough that we restart playback at least once, but no more
+ Thread.sleep(10000);
+ assertTrue("MediaPlayer2 should still be playing",
+ mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ mPlayer.reset();
+ assertEquals("wrong number of repetitions", 1, mOnCompletionCalled.getNumSignal());
+ return 1;
+ }
+
+ @Test
+ @LargeTest
+ public void testPositionAtEnd() throws Throwable {
+ int testsRun = testPositionAtEnd(R.raw.test1m1shighstereo)
+ + testPositionAtEnd(R.raw.loudsoftmp3)
+ + testPositionAtEnd(R.raw.loudsoftwav)
+ + testPositionAtEnd(R.raw.loudsoftogg)
+ + testPositionAtEnd(R.raw.loudsoftitunes)
+ + testPositionAtEnd(R.raw.loudsoftfaac)
+ + testPositionAtEnd(R.raw.loudsoftaac);
+ }
+
+ private int testPositionAtEnd(int res) throws Throwable {
+ if (!loadResource(res)) {
+ Log.i(LOG_TAG, "testPositionAtEnd: No decoder found for "
+ + mContext.getResources().getResourceEntryName(res) + " --- skipping.");
+ return 0; // skip
+ }
+ AudioAttributesCompat attributes = new AudioAttributesCompat.Builder()
+ .setLegacyStreamType(AudioManager.STREAM_MUSIC)
+ .build();
+ mPlayer.setAudioAttributes(attributes);
+
+ mOnCompletionCalled.reset();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ mOnCompletionCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ long duration = mPlayer.getDuration();
+ assertTrue("resource too short", duration > 6000);
+ mPlayer.seekTo(duration - 5000, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Log.i("@@@@", "position: " + mPlayer.getCurrentPosition());
+ Thread.sleep(500);
+ }
+ Log.i("@@@@", "final position: " + mPlayer.getCurrentPosition());
+ assertTrue(mPlayer.getCurrentPosition() > duration - 1000);
+ mPlayer.reset();
+ return 1;
+ }
+
+ @Test
+ @LargeTest
+ public void testMediaPlayer2Callback() throws Throwable {
+ final int mp4Duration = 8484;
+
+ if (!checkLoadResource(R.raw.testvideo)) {
+ return; // skip;
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ mOnCompletionCalled.reset();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+ int width, int height) {
+ mOnVideoSizeChangedCalled.signal();
+ }
+
+ @Override
+ public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ mOnErrorCalled.signal();
+ }
+
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ mOnInfoCalled.signal();
+
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) {
+ mOnCompletionCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO) {
+ mOnSeekCompleteCalled.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ assertFalse(mOnPrepareCalled.isSignalled());
+ assertFalse(mOnVideoSizeChangedCalled.isSignalled());
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+ mOnVideoSizeChangedCalled.waitForSignal();
+
+ mOnSeekCompleteCalled.reset();
+ mPlayer.seekTo(mp4Duration >> 1, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ mOnSeekCompleteCalled.waitForSignal();
+
+ assertFalse(mOnCompletionCalled.isSignalled());
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Thread.sleep(SLEEP_TIME);
+ }
+ assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+ mOnCompletionCalled.waitForSignal();
+ assertFalse(mOnErrorCalled.isSignalled());
+ mPlayer.reset();
+ }
+
+ @Test
+ @LargeTest
+ public void testPlayerStates() throws Throwable {
+ final int mp4Duration = 8484;
+
+ if (!checkLoadResource(R.raw.testvideo)) {
+ return; // skip;
+ }
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ final Monitor prepareCompleted = new Monitor();
+ final Monitor playCompleted = new Monitor();
+ final Monitor pauseCompleted = new Monitor();
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PREPARE) {
+ prepareCompleted.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ playCompleted.signal();
+ } else if (what == MediaPlayer2.CALL_COMPLETED_PAUSE) {
+ pauseCompleted.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
+ assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+ prepareCompleted.reset();
+ mPlayer.prepare();
+ prepareCompleted.waitForSignal();
+ assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ mPlayer.getBufferingState());
+ assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+
+ playCompleted.reset();
+ mPlayer.play();
+ playCompleted.waitForSignal();
+ assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ mPlayer.getBufferingState());
+ assertEquals(MediaPlayerBase.PLAYER_STATE_PLAYING, mPlayer.getPlayerState());
+
+ pauseCompleted.reset();
+ mPlayer.pause();
+ pauseCompleted.waitForSignal();
+ assertEquals(MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ mPlayer.getBufferingState());
+ assertEquals(MediaPlayerBase.PLAYER_STATE_PAUSED, mPlayer.getPlayerState());
+
+ mPlayer.reset();
+ assertEquals(MediaPlayerBase.BUFFERING_STATE_UNKNOWN, mPlayer.getBufferingState());
+ assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, mPlayer.getPlayerState());
+ }
+
+ @Test
+ @LargeTest
+ public void testPlayerEventCallback() throws Throwable {
+ final int mp4Duration = 8484;
+
+ if (!checkLoadResource(R.raw.testvideo)) {
+ return; // skip;
+ }
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+
+ final Monitor onPrepareCalled = new Monitor();
+ final Monitor onSeekCompleteCalled = new Monitor();
+ final Monitor onPlayerStateChangedCalled = new Monitor();
+ final AtomicInteger playerState = new AtomicInteger();
+ final Monitor onBufferingStateChangedCalled = new Monitor();
+ final AtomicInteger bufferingState = new AtomicInteger();
+ final Monitor onPlaybackSpeedChanged = new Monitor();
+ final AtomicReference<Float> playbackSpeed = new AtomicReference<>();
+
+ MediaPlayerBase.PlayerEventCallback callback = new MediaPlayerBase.PlayerEventCallback() {
+ // TODO: implement and add test case for onCurrentDataSourceChanged() callback.
+ @Override
+ public void onMediaPrepared(MediaPlayerBase mpb, DataSourceDesc dsd) {
+ onPrepareCalled.signal();
+ }
+
+ @Override
+ public void onPlayerStateChanged(MediaPlayerBase mpb, int state) {
+ playerState.set(state);
+ onPlayerStateChangedCalled.signal();
+ }
+
+ @Override
+ public void onBufferingStateChanged(MediaPlayerBase mpb, DataSourceDesc dsd,
+ int state) {
+ bufferingState.set(state);
+ onBufferingStateChangedCalled.signal();
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(MediaPlayerBase mpb, float speed) {
+ playbackSpeed.set(speed);
+ onPlaybackSpeedChanged.signal();
+ }
+
+ @Override
+ public void onSeekCompleted(MediaPlayerBase mpb, long position) {
+ onSeekCompleteCalled.signal();
+ }
+ };
+ ExecutorService executor = Executors.newFixedThreadPool(1);
+ mPlayer.registerPlayerEventCallback(executor, callback);
+
+ onPrepareCalled.reset();
+ onPlayerStateChangedCalled.reset();
+ onBufferingStateChangedCalled.reset();
+ mPlayer.prepare();
+ do {
+ assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
+ } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED);
+
+ assertTrue(onPrepareCalled.waitForSignal(1000));
+ do {
+ assertTrue(onPlayerStateChangedCalled.waitForSignal(1000));
+ } while (playerState.get() != MediaPlayerBase.PLAYER_STATE_PAUSED);
+ do {
+ assertTrue(onBufferingStateChangedCalled.waitForSignal(1000));
+ } while (bufferingState.get() != MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
+
+ onSeekCompleteCalled.reset();
+ mPlayer.seekTo(mp4Duration >> 1, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ onSeekCompleteCalled.waitForSignal();
+
+ onPlaybackSpeedChanged.reset();
+ mPlayer.setPlaybackSpeed(0.5f);
+ do {
+ assertTrue(onPlaybackSpeedChanged.waitForSignal(1000));
+ } while (Math.abs(playbackSpeed.get() - 0.5f) > FLOAT_TOLERANCE);
+
+ mPlayer.reset();
+
+ mPlayer.unregisterPlayerEventCallback(callback);
+ executor.shutdown();
+ }
+
+ public void testRecordAndPlay() throws Exception {
+ if (!hasMicrophone()) {
+ return;
+ }
+ /* FIXME: check the codec exists.
+ if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_AUDIO_AMR_NB)
+ || !MediaUtils.checkEncoder(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) {
+ return; // skip
+ }
+ */
+ File outputFile = new File(Environment.getExternalStorageDirectory(),
+ "record_and_play.3gp");
+ String outputFileLocation = outputFile.getAbsolutePath();
+ try {
+ recordMedia(outputFileLocation);
+
+ Uri uri = Uri.parse(outputFileLocation);
+ MediaPlayer2 mp = MediaPlayer2.create();
+ try {
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(mContext, uri)
+ .build());
+ mp.prepare();
+ Thread.sleep(SLEEP_TIME);
+ playAndStop(mp);
+ } finally {
+ mp.close();
+ }
+
+ try {
+ mp = createMediaPlayer2(mContext, uri);
+ playAndStop(mp);
+ } finally {
+ if (mp != null) {
+ mp.close();
+ }
+ }
+
+ try {
+ mp = createMediaPlayer2(mContext, uri, mActivity.getSurfaceHolder());
+ playAndStop(mp);
+ } finally {
+ if (mp != null) {
+ mp.close();
+ }
+ }
+ } finally {
+ outputFile.delete();
+ }
+ }
+
+ private void playAndStop(MediaPlayer2 mp) throws Exception {
+ mp.play();
+ Thread.sleep(SLEEP_TIME);
+ mp.reset();
+ }
+
+ private void recordMedia(String outputFile) throws Exception {
+ MediaRecorder mr = new MediaRecorder();
+ try {
+ mr.setAudioSource(MediaRecorder.AudioSource.MIC);
+ mr.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
+ mr.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
+ mr.setOutputFile(outputFile);
+
+ mr.prepare();
+ mr.start();
+ Thread.sleep(SLEEP_TIME);
+ mr.stop();
+ } finally {
+ mr.release();
+ }
+ }
+
+ private boolean hasMicrophone() {
+ return mActivity.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_MICROPHONE);
+ }
+
+ // Smoke test playback from a Media2DataSource.
+ @Test
+ @LargeTest
+ public void testPlaybackFromAMedia2DataSource() throws Exception {
+ final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+ final int duration = 10000;
+
+ /* FIXME: check the codec exists.
+ if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+ return;
+ }
+ */
+
+ TestMedia2DataSource dataSource =
+ TestMedia2DataSource.fromAssetFd(mResources.openRawResourceFd(resid));
+ // Test returning -1 from getSize() to indicate unknown size.
+ dataSource.returnFromGetSize(-1);
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(dataSource)
+ .build());
+ playLoadedVideo(null, null, -1);
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // Test pause and restart.
+ mPlayer.pause();
+ Thread.sleep(SLEEP_TIME);
+ assertFalse(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // Test reset.
+ mPlayer.reset();
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(dataSource)
+ .build());
+
+ mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ assertTrue(mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING);
+
+ // Test seek. Note: the seek position is cached and returned as the
+ // current position so there's no point in comparing them.
+ mPlayer.seekTo(duration - SLEEP_TIME, MediaPlayer2.SEEK_PREVIOUS_SYNC);
+ while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Thread.sleep(SLEEP_TIME);
+ }
+ }
+
+ @Test
+ @LargeTest
+ public void testNullMedia2DataSourceIsRejected() throws Exception {
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
+ mCallStatus = status;
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
+ mPlayer.setDataSource((DataSourceDesc) null);
+ mOnPlayCalled.waitForSignal();
+ assertTrue(mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR);
+ }
+
+ @Test
+ @LargeTest
+ public void testMedia2DataSourceIsClosedOnReset() throws Exception {
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
+ mCallStatus = status;
+ mOnPlayCalled.signal();
+ }
+ }
+ };
+ mPlayer.setMediaPlayer2EventCallback(mExecutor, ecb);
+
+ TestMedia2DataSource dataSource = new TestMedia2DataSource(new byte[0]);
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(dataSource)
+ .build());
+ mOnPlayCalled.waitForSignal();
+ mPlayer.reset();
+ assertTrue(dataSource.isClosed());
+ }
+
+ @Test
+ @LargeTest
+ public void testPlaybackFailsIfMedia2DataSourceThrows() throws Exception {
+ final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+ /* FIXME: check the codec exists.
+ if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+ return;
+ }
+ */
+
+ setOnErrorListener();
+ TestMedia2DataSource dataSource =
+ TestMedia2DataSource.fromAssetFd(mResources.openRawResourceFd(resid));
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(dataSource)
+ .build());
+
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ dataSource.throwFromReadAt();
+ mPlayer.play();
+ assertTrue(mOnErrorCalled.waitForSignal());
+ }
+
+ @Test
+ @LargeTest
+ public void testPlaybackFailsIfMedia2DataSourceReturnsAnError() throws Exception {
+ final int resid = R.raw.video_480x360_mp4_h264_1350kbps_30fps_aac_stereo_192kbps_44100hz;
+ /* FIXME: check the codec exists.
+ if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+ return;
+ }
+ */
+
+ TestMedia2DataSource dataSource =
+ TestMedia2DataSource.fromAssetFd(mResources.openRawResourceFd(resid));
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(dataSource)
+ .build());
+
+ setOnErrorListener();
+ MediaPlayer2.MediaPlayer2EventCallback ecb = new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+ };
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(ecb);
+ }
+
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+
+ dataSource.returnFromReadAt(-2);
+ mPlayer.play();
+ assertTrue(mOnErrorCalled.waitForSignal());
+ }
+}
diff --git a/androidx/media/MediaPlayer2TestBase.java b/androidx/media/MediaPlayer2TestBase.java
new file mode 100644
index 00000000..215993a3
--- /dev/null
+++ b/androidx/media/MediaPlayer2TestBase.java
@@ -0,0 +1,584 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.MediaTimestamp;
+import android.media.TimedMetaData;
+import android.net.Uri;
+import android.support.test.rule.ActivityTestRule;
+import android.view.SurfaceHolder;
+
+import androidx.annotation.CallSuper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+
+import java.io.IOException;
+import java.net.HttpCookie;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+
+/**
+ * Base class for tests which use MediaPlayer2 to play audio or video.
+ */
+public class MediaPlayer2TestBase {
+ private static final Logger LOG = Logger.getLogger(MediaPlayer2TestBase.class.getName());
+
+ protected static final int SLEEP_TIME = 1000;
+ protected static final int LONG_SLEEP_TIME = 6000;
+ protected static final int STREAM_RETRIES = 20;
+
+ protected Monitor mOnVideoSizeChangedCalled = new Monitor();
+ protected Monitor mOnVideoRenderingStartCalled = new Monitor();
+ protected Monitor mOnBufferingUpdateCalled = new Monitor();
+ protected Monitor mOnPrepareCalled = new Monitor();
+ protected Monitor mOnPlayCalled = new Monitor();
+ protected Monitor mOnDeselectTrackCalled = new Monitor();
+ protected Monitor mOnSeekCompleteCalled = new Monitor();
+ protected Monitor mOnCompletionCalled = new Monitor();
+ protected Monitor mOnInfoCalled = new Monitor();
+ protected Monitor mOnErrorCalled = new Monitor();
+ protected int mCallStatus;
+
+ protected Context mContext;
+ protected Resources mResources;
+
+ protected ExecutorService mExecutor;
+
+ protected MediaPlayer2 mPlayer = null;
+ protected MediaPlayer2 mPlayer2 = null;
+ protected MediaStubActivity mActivity;
+
+ protected final Object mEventCbLock = new Object();
+ protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks =
+ new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+ protected final Object mEventCbLock2 = new Object();
+ protected List<MediaPlayer2.MediaPlayer2EventCallback> mEventCallbacks2 =
+ new ArrayList<MediaPlayer2.MediaPlayer2EventCallback>();
+
+ @Rule
+ public ActivityTestRule<MediaStubActivity> mActivityRule =
+ new ActivityTestRule<>(MediaStubActivity.class);
+
+ // convenience functions to create MediaPlayer2
+ protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri) {
+ return createMediaPlayer2(context, uri, null);
+ }
+
+ protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri,
+ SurfaceHolder holder) {
+ AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ int s = am.generateAudioSessionId();
+ return createMediaPlayer2(context, uri, holder, null, s > 0 ? s : 0);
+ }
+
+ protected static MediaPlayer2 createMediaPlayer2(Context context, Uri uri, SurfaceHolder holder,
+ AudioAttributesCompat audioAttributes, int audioSessionId) {
+ try {
+ MediaPlayer2 mp = MediaPlayer2.create();
+ final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
+ new AudioAttributesCompat.Builder().build();
+ mp.setAudioAttributes(aa);
+ mp.setAudioSessionId(audioSessionId);
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(context, uri)
+ .build());
+ if (holder != null) {
+ mp.setSurface(holder.getSurface());
+ }
+ final Monitor onPrepareCalled = new Monitor();
+ ExecutorService executor = Executors.newFixedThreadPool(1);
+ MediaPlayer2.MediaPlayer2EventCallback ecb =
+ new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(
+ MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ onPrepareCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(executor, ecb);
+ mp.prepare();
+ onPrepareCalled.waitForSignal();
+ mp.clearMediaPlayer2EventCallback();
+ executor.shutdown();
+ return mp;
+ } catch (IllegalArgumentException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ } catch (SecurityException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ } catch (InterruptedException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ }
+ return null;
+ }
+
+ protected static MediaPlayer2 createMediaPlayer2(Context context, int resid) {
+ AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ int s = am.generateAudioSessionId();
+ return createMediaPlayer2(context, resid, null, s > 0 ? s : 0);
+ }
+
+ protected static MediaPlayer2 createMediaPlayer2(Context context, int resid,
+ AudioAttributesCompat audioAttributes, int audioSessionId) {
+ try {
+ AssetFileDescriptor afd = context.getResources().openRawResourceFd(resid);
+ if (afd == null) {
+ return null;
+ }
+
+ MediaPlayer2 mp = MediaPlayer2.create();
+
+ final AudioAttributesCompat aa = audioAttributes != null ? audioAttributes :
+ new AudioAttributesCompat.Builder().build();
+ mp.setAudioAttributes(aa);
+ mp.setAudioSessionId(audioSessionId);
+
+ mp.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+ .build());
+
+ final Monitor onPrepareCalled = new Monitor();
+ ExecutorService executor = Executors.newFixedThreadPool(1);
+ MediaPlayer2.MediaPlayer2EventCallback ecb =
+ new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onInfo(
+ MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ onPrepareCalled.signal();
+ }
+ }
+ };
+ mp.setMediaPlayer2EventCallback(executor, ecb);
+ mp.prepare();
+ onPrepareCalled.waitForSignal();
+ mp.clearMediaPlayer2EventCallback();
+ afd.close();
+ executor.shutdown();
+ return mp;
+ } catch (IOException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ } catch (IllegalArgumentException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ } catch (SecurityException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ } catch (InterruptedException ex) {
+ LOG.warning("create failed:" + ex);
+ // fall through
+ }
+ return null;
+ }
+
+ public static class Monitor {
+ private int mNumSignal;
+
+ public synchronized void reset() {
+ mNumSignal = 0;
+ }
+
+ public synchronized void signal() {
+ mNumSignal++;
+ notifyAll();
+ }
+
+ public synchronized boolean waitForSignal() throws InterruptedException {
+ return waitForCountedSignals(1) > 0;
+ }
+
+ public synchronized int waitForCountedSignals(int targetCount) throws InterruptedException {
+ while (mNumSignal < targetCount) {
+ wait();
+ }
+ return mNumSignal;
+ }
+
+ public synchronized boolean waitForSignal(long timeoutMs) throws InterruptedException {
+ return waitForCountedSignals(1, timeoutMs) > 0;
+ }
+
+ public synchronized int waitForCountedSignals(int targetCount, long timeoutMs)
+ throws InterruptedException {
+ if (timeoutMs == 0) {
+ return waitForCountedSignals(targetCount);
+ }
+ long deadline = System.currentTimeMillis() + timeoutMs;
+ while (mNumSignal < targetCount) {
+ long delay = deadline - System.currentTimeMillis();
+ if (delay <= 0) {
+ break;
+ }
+ wait(delay);
+ }
+ return mNumSignal;
+ }
+
+ public synchronized boolean isSignalled() {
+ return mNumSignal >= 1;
+ }
+
+ public synchronized int getNumSignal() {
+ return mNumSignal;
+ }
+ }
+
+ @Before
+ @CallSuper
+ public void setUp() throws Exception {
+ mActivity = mActivityRule.getActivity();
+ try {
+ mActivityRule.runOnUiThread(new Runnable() {
+ public void run() {
+ mPlayer = MediaPlayer2.create();
+ mPlayer2 = MediaPlayer2.create();
+ }
+ });
+ } catch (Throwable e) {
+ e.printStackTrace();
+ fail();
+ }
+ mContext = mActivityRule.getActivity();
+ mResources = mContext.getResources();
+ mExecutor = Executors.newFixedThreadPool(1);
+
+ setUpMP2ECb(mPlayer, mEventCbLock, mEventCallbacks);
+ setUpMP2ECb(mPlayer2, mEventCbLock2, mEventCallbacks2);
+ }
+
+ @After
+ @CallSuper
+ public void tearDown() throws Exception {
+ if (mPlayer != null) {
+ mPlayer.close();
+ mPlayer = null;
+ }
+ if (mPlayer2 != null) {
+ mPlayer2.close();
+ mPlayer2 = null;
+ }
+ mExecutor.shutdown();
+ mActivity = null;
+ }
+
+ protected void setUpMP2ECb(MediaPlayer2 mp, final Object cbLock,
+ final List<MediaPlayer2.MediaPlayer2EventCallback> ecbs) {
+ mp.setMediaPlayer2EventCallback(mExecutor, new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd, int w, int h) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onVideoSizeChanged(mp, dsd, w, h);
+ }
+ }
+ }
+
+ @Override
+ public void onTimedMetaDataAvailable(MediaPlayer2 mp, DataSourceDesc dsd,
+ TimedMetaData data) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onTimedMetaDataAvailable(mp, dsd, data);
+ }
+ }
+ }
+
+ @Override
+ public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onError(mp, dsd, what, extra);
+ }
+ }
+ }
+
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onInfo(mp, dsd, what, extra);
+ }
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onCallCompleted(mp, dsd, what, status);
+ }
+ }
+ }
+
+ @Override
+ public void onMediaTimeChanged(MediaPlayer2 mp, DataSourceDesc dsd,
+ MediaTimestamp timestamp) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onMediaTimeChanged(mp, dsd, timestamp);
+ }
+ }
+ }
+
+ @Override
+ public void onCommandLabelReached(MediaPlayer2 mp, Object label) {
+ synchronized (cbLock) {
+ for (MediaPlayer2.MediaPlayer2EventCallback ecb : ecbs) {
+ ecb.onCommandLabelReached(mp, label);
+ }
+ }
+ }
+ });
+ }
+
+ // returns true on success
+ protected boolean loadResource(int resid) throws Exception {
+ /* FIXME: ensure device has capability.
+ if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+ return false;
+ }
+ */
+
+ AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+ try {
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+ .build());
+ } finally {
+ // TODO: close afd only after setDataSource is confirmed.
+ // afd.close();
+ }
+ return true;
+ }
+
+ protected DataSourceDesc createDataSourceDesc(int resid) throws Exception {
+ /* FIXME: ensure device has capability.
+ if (!MediaUtils.hasCodecsForResource(mContext, resid)) {
+ return null;
+ }
+ */
+
+ AssetFileDescriptor afd = mResources.openRawResourceFd(resid);
+ return new DataSourceDesc.Builder()
+ .setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength())
+ .build();
+ }
+
+ protected boolean checkLoadResource(int resid) throws Exception {
+ return loadResource(resid);
+
+ /* FIXME: ensure device has capability.
+ return MediaUtils.check(loadResource(resid), "no decoder found");
+ */
+ }
+
+ protected void playLiveVideoTest(String path, int playTime) throws Exception {
+ playVideoWithRetries(path, null, null, playTime);
+ }
+
+ protected void playLiveAudioOnlyTest(String path, int playTime) throws Exception {
+ playVideoWithRetries(path, -1, -1, playTime);
+ }
+
+ protected void playVideoTest(String path, int width, int height) throws Exception {
+ playVideoWithRetries(path, width, height, 0);
+ }
+
+ protected void playVideoWithRetries(String path, Integer width, Integer height, int playTime)
+ throws Exception {
+ boolean playedSuccessfully = false;
+ final Uri uri = Uri.parse(path);
+ for (int i = 0; i < STREAM_RETRIES; i++) {
+ try {
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(mContext, uri)
+ .build());
+ playLoadedVideo(width, height, playTime);
+ playedSuccessfully = true;
+ break;
+ } catch (PrepareFailedException e) {
+ // prepare() can fail because of network issues, so try again
+ LOG.warning("prepare() failed on try " + i + ", trying playback again");
+ }
+ }
+ assertTrue("Stream did not play successfully after all attempts", playedSuccessfully);
+ }
+
+ protected void playVideoTest(int resid, int width, int height) throws Exception {
+ if (!checkLoadResource(resid)) {
+ return; // skip
+ }
+
+ playLoadedVideo(width, height, 0);
+ }
+
+ protected void playLiveVideoTest(
+ Uri uri, Map<String, String> headers, List<HttpCookie> cookies,
+ int playTime) throws Exception {
+ playVideoWithRetries(uri, headers, cookies, null /* width */, null /* height */, playTime);
+ }
+
+ protected void playVideoWithRetries(
+ Uri uri, Map<String, String> headers, List<HttpCookie> cookies,
+ Integer width, Integer height, int playTime) throws Exception {
+ boolean playedSuccessfully = false;
+ for (int i = 0; i < STREAM_RETRIES; i++) {
+ try {
+ mPlayer.setDataSource(new DataSourceDesc.Builder()
+ .setDataSource(mContext,
+ uri, headers, cookies)
+ .build());
+ playLoadedVideo(width, height, playTime);
+ playedSuccessfully = true;
+ break;
+ } catch (PrepareFailedException e) {
+ // prepare() can fail because of network issues, so try again
+ // playLoadedVideo already has reset the player so we can try again safely.
+ LOG.warning("prepare() failed on try " + i + ", trying playback again");
+ }
+ }
+ assertTrue("Stream did not play successfully after all attempts", playedSuccessfully);
+ }
+
+ /**
+ * Play a video which has already been loaded with setDataSource().
+ *
+ * @param width width of the video to verify, or null to skip verification
+ * @param height height of the video to verify, or null to skip verification
+ * @param playTime length of time to play video, or 0 to play entire video.
+ * with a non-negative value, this method stops the playback after the length of
+ * time or the duration the video is elapsed. With a value of -1,
+ * this method simply starts the video and returns immediately without
+ * stoping the video playback.
+ */
+ protected void playLoadedVideo(final Integer width, final Integer height, int playTime)
+ throws Exception {
+ final float volume = 0.5f;
+
+ boolean audioOnly = (width != null && width.intValue() == -1)
+ || (height != null && height.intValue() == -1);
+
+ mPlayer.setSurface(mActivity.getSurfaceHolder().getSurface());
+ /* FIXME: ensure that screen is on in activity level.
+ mPlayer.setScreenOnWhilePlaying(true);
+ */
+
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd, int w, int h) {
+ if (w == 0 && h == 0) {
+ // A size of 0x0 can be sent initially one time when using NuPlayer.
+ assertFalse(mOnVideoSizeChangedCalled.isSignalled());
+ return;
+ }
+ mOnVideoSizeChangedCalled.signal();
+ if (width != null) {
+ assertEquals(width.intValue(), w);
+ }
+ if (height != null) {
+ assertEquals(height.intValue(), h);
+ }
+ }
+
+ @Override
+ public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ fail("Media player had error " + what + " playing video");
+ }
+
+ @Override
+ public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ if (what == MediaPlayer2.MEDIA_INFO_VIDEO_RENDERING_START) {
+ mOnVideoRenderingStartCalled.signal();
+ } else if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
+ mOnPrepareCalled.signal();
+ }
+ }
+
+ @Override
+ public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd,
+ int what, int status) {
+ if (what == MediaPlayer2.CALL_COMPLETED_PLAY) {
+ mOnPlayCalled.signal();
+ }
+ }
+ });
+ }
+ try {
+ mOnPrepareCalled.reset();
+ mPlayer.prepare();
+ mOnPrepareCalled.waitForSignal();
+ } catch (Exception e) {
+ mPlayer.reset();
+ throw new PrepareFailedException();
+ }
+
+ mOnPlayCalled.reset();
+ mPlayer.play();
+ mOnPlayCalled.waitForSignal();
+ if (!audioOnly) {
+ mOnVideoSizeChangedCalled.waitForSignal();
+ mOnVideoRenderingStartCalled.waitForSignal();
+ }
+ mPlayer.setPlayerVolume(volume);
+
+ // waiting to complete
+ if (playTime == -1) {
+ return;
+ } else if (playTime == 0) {
+ while (mPlayer.getPlayerState() == MediaPlayerBase.PLAYER_STATE_PLAYING) {
+ Thread.sleep(SLEEP_TIME);
+ }
+ } else {
+ Thread.sleep(playTime);
+ }
+
+ mPlayer.reset();
+ }
+
+ private static class PrepareFailedException extends Exception {}
+
+ protected void setOnErrorListener() {
+ synchronized (mEventCbLock) {
+ mEventCallbacks.add(new MediaPlayer2.MediaPlayer2EventCallback() {
+ @Override
+ public void onError(MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) {
+ mOnErrorCalled.signal();
+ }
+ });
+ }
+ }
+}
diff --git a/androidx/media/MediaPlayerBase.java b/androidx/media/MediaPlayerBase.java
new file mode 100644
index 00000000..de0e1283
--- /dev/null
+++ b/androidx/media/MediaPlayerBase.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class for all media players that want media session.
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public abstract class MediaPlayerBase implements AutoCloseable {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({
+ PLAYER_STATE_IDLE,
+ PLAYER_STATE_PAUSED,
+ PLAYER_STATE_PLAYING,
+ PLAYER_STATE_ERROR })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlayerState {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({
+ BUFFERING_STATE_UNKNOWN,
+ BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+ BUFFERING_STATE_BUFFERING_AND_STARVED,
+ BUFFERING_STATE_BUFFERING_COMPLETE })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BuffState {}
+
+ /**
+ * State when the player is idle, and needs configuration to start playback.
+ */
+ public static final int PLAYER_STATE_IDLE = 0;
+
+ /**
+ * State when the player's playback is paused
+ */
+ public static final int PLAYER_STATE_PAUSED = 1;
+
+ /**
+ * State when the player's playback is ongoing
+ */
+ public static final int PLAYER_STATE_PLAYING = 2;
+
+ /**
+ * State when the player is in error state and cannot be recovered self.
+ */
+ public static final int PLAYER_STATE_ERROR = 3;
+
+ /**
+ * Buffering state is unknown.
+ */
+ public static final int BUFFERING_STATE_UNKNOWN = 0;
+
+ /**
+ * Buffering state indicating the player is buffering but enough has been buffered
+ * for this player to be able to play the content.
+ * See {@link #getBufferedPosition()} for how far is buffered already.
+ */
+ public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1;
+
+ /**
+ * Buffering state indicating the player is buffering, but the player is currently starved
+ * for data, and cannot play.
+ */
+ public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2;
+
+ /**
+ * Buffering state indicating the player is done buffering, and the remainder of the content is
+ * available for playback.
+ */
+ public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3;
+
+ /**
+ * Starts or resumes playback.
+ */
+ public abstract void play();
+
+ /**
+ * Prepares the player for playback.
+ * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
+ * notified when the preparation phase completed. During this time, the player may allocate
+ * resources required to play, such as audio and video decoders.
+ */
+ public abstract void prepare();
+
+ /**
+ * Pauses playback.
+ */
+ public abstract void pause();
+
+ /**
+ * Resets the MediaPlayerBase to its uninitialized state.
+ */
+ public abstract void reset();
+
+ /**
+ *
+ */
+ public abstract void skipToNext();
+
+ /**
+ * Moves the playback head to the specified position
+ * @param pos the new playback position expressed in ms.
+ */
+ public abstract void seekTo(long pos);
+
+ public static final long UNKNOWN_TIME = -1;
+
+ /**
+ * Gets the current playback head position.
+ * @return the current playback position in ms, or {@link #UNKNOWN_TIME} if unknown.
+ */
+ public long getCurrentPosition() {
+ return UNKNOWN_TIME;
+ }
+
+ /**
+ * Returns the duration of the current data source, or {@link #UNKNOWN_TIME} if unknown.
+ * @return the duration in ms, or {@link #UNKNOWN_TIME}.
+ */
+ public long getDuration() {
+ return UNKNOWN_TIME;
+ }
+
+ /**
+ * Gets the buffered position of current playback, or {@link #UNKNOWN_TIME} if unknown.
+ * @return the buffered position in ms, or {@link #UNKNOWN_TIME}.
+ */
+ public long getBufferedPosition() {
+ return UNKNOWN_TIME;
+ }
+
+ /**
+ * Returns the current player state.
+ * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
+ * notification of changes.
+ * @return the current player state
+ */
+ public abstract @PlayerState int getPlayerState();
+
+ /**
+ * Returns the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ * @return the buffering state.
+ */
+ public abstract @BuffState int getBufferingState();
+
+ /**
+ * Sets the {@link AudioAttributesCompat} to be used during the playback of the media.
+ *
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public abstract void setAudioAttributes(@NonNull AudioAttributesCompat attributes);
+
+ /**
+ * Returns AudioAttributes that media player has.
+ */
+ public abstract @Nullable AudioAttributesCompat getAudioAttributes();
+
+ /**
+ * Sets the data source to be played.
+ * @param dsd
+ */
+ public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets the data source that will be played immediately after the current one is done playing.
+ * @param dsd
+ */
+ public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+ /**
+ * Sets the list of data sources that will be sequentially played after the current one. Each
+ * data source is played immediately after the previous one is done playing.
+ * @param dsds
+ */
+ public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+ /**
+ * Returns the current data source.
+ * @return the current data source, or null if none is set, or none available to play.
+ */
+ public abstract @Nullable DataSourceDesc getCurrentDataSource();
+
+ /**
+ * Configures the player to loop on the current data source.
+ * @param loop true if the current data source is meant to loop.
+ */
+ public abstract void loopCurrent(boolean loop);
+
+ /**
+ * Sets the playback speed.
+ * A value of 1.0f is the default playback value.
+ * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+ * before using negative values.<br>
+ * After changing the playback speed, it is recommended to query the actual speed supported
+ * by the player, see {@link #getPlaybackSpeed()}.
+ * @param speed
+ */
+ public abstract void setPlaybackSpeed(float speed);
+
+ /**
+ * Returns the actual playback speed to be used by the player when playing.
+ * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+ * @return the actual playback speed
+ */
+ public float getPlaybackSpeed() {
+ return 1.0f;
+ }
+
+ /**
+ * Indicates whether reverse playback is supported.
+ * Reverse playback is indicated by negative playback speeds, see
+ * {@link #setPlaybackSpeed(float)}.
+ * @return true if reverse playback is supported.
+ */
+ public boolean isReversePlaybackSupported() {
+ return false;
+ }
+
+ /**
+ * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+ * on the audio samples.
+ * Note that this volume is specific to the player, and is separate from stream volume
+ * used across the platform.<br>
+ * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+ * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+ * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+ */
+ public abstract void setPlayerVolume(float volume);
+
+ /**
+ * Returns the current volume of this player to this player.
+ * Note that it does not take into account the associated stream volume.
+ * @return the player volume.
+ */
+ public abstract float getPlayerVolume();
+
+ /**
+ * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+ */
+ public float getMaxPlayerVolume() {
+ return 1.0f;
+ }
+
+ /**
+ * Adds a callback to be notified of events for this player.
+ * @param e the {@link Executor} to be used for the events.
+ * @param cb the callback to receive the events.
+ */
+ public abstract void registerPlayerEventCallback(@NonNull Executor e,
+ @NonNull PlayerEventCallback cb);
+
+ /**
+ * Removes a previously registered callback for player events
+ * @param cb the callback to remove
+ */
+ public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+ /**
+ * A callback class to receive notifications for events on the media player.
+ * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
+ * register this callback.
+ */
+ public abstract static class PlayerEventCallback {
+ /**
+ * Called when the player's current data source has changed.
+ *
+ * @param mpb the player whose data source changed.
+ * @param dsd the new current data source. null, if no more data sources available.
+ */
+ public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+ @Nullable DataSourceDesc dsd) { }
+
+ /**
+ * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+ * referenced by the given data source.
+ * @param mpb the player that is prepared.
+ * @param dsd the data source that the player is prepared to play.
+ */
+ public void onMediaPrepared(@NonNull MediaPlayerBase mpb,
+ @NonNull DataSourceDesc dsd) { }
+
+ /**
+ * Called to indicate that the state of the player has changed.
+ * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+ * @param mpb the player whose state has changed.
+ * @param state the new state of the player.
+ */
+ public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
+
+ /**
+ * Called to report buffering events for a data source.
+ * @param mpb the player that is buffering
+ * @param dsd the data source for which buffering is happening.
+ * @param state the new buffering state.
+ */
+ public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
+ @NonNull DataSourceDesc dsd, @BuffState int state) { }
+
+ /**
+ * Called to indicate that the playback speed has changed.
+ * @param mpb the player that has changed the playback speed.
+ * @param speed the new playback speed.
+ */
+ public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
+
+ /**
+ * Called to indicate that {@link #seekTo(long)} is completed.
+ *
+ * @param mpb the player that has completed seeking.
+ * @param position the previous seeking request.
+ * @see #seekTo(long)
+ */
+ public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
+ }
+}
diff --git a/androidx/media/MediaPlaylistAgent.java b/androidx/media/MediaPlaylistAgent.java
new file mode 100644
index 00000000..07838e80
--- /dev/null
+++ b/androidx/media/MediaPlaylistAgent.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.collection.SimpleArrayMap;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
+ * to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
+ * notify methods to signal MediaSession2 that playlist-related state has changed.
+ * <p>
+ * Playlists are composed of one or multiple {@link MediaItem2} instances, which combine metadata
+ * and data sources (as {@link DataSourceDesc})
+ * Used by {@link MediaSession2} and {@link MediaController2}.
+ */
+// This class only includes methods that contain {@link MediaItem2}.
+public abstract class MediaPlaylistAgent {
+ private static final String TAG = "MediaPlaylistAgent";
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
+ REPEAT_MODE_GROUP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RepeatMode {}
+
+ /**
+ * Playback will be stopped at the end of the playing media list.
+ */
+ public static final int REPEAT_MODE_NONE = 0;
+
+ /**
+ * Playback of the current playing media item will be repeated.
+ */
+ public static final int REPEAT_MODE_ONE = 1;
+
+ /**
+ * Playing media list will be repeated.
+ */
+ public static final int REPEAT_MODE_ALL = 2;
+
+ /**
+ * Playback of the playing media group will be repeated.
+ * A group is a logical block of media items which is specified in the section 5.7 of the
+ * Bluetooth AVRCP 1.6. An example of a group is the playlist.
+ */
+ public static final int REPEAT_MODE_GROUP = 3;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShuffleMode {}
+
+ /**
+ * Media list will be played in order.
+ */
+ public static final int SHUFFLE_MODE_NONE = 0;
+
+ /**
+ * Media list will be played in shuffled order.
+ */
+ public static final int SHUFFLE_MODE_ALL = 1;
+
+ /**
+ * Media group will be played in shuffled order.
+ * A group is a logical block of media items which is specified in the section 5.7 of the
+ * Bluetooth AVRCP 1.6. An example of a group is the playlist.
+ */
+ public static final int SHUFFLE_MODE_GROUP = 2;
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final SimpleArrayMap<PlaylistEventCallback, Executor> mCallbacks =
+ new SimpleArrayMap<>();
+
+ /**
+ * Register {@link PlaylistEventCallback} to listen changes in the underlying
+ * {@link MediaPlaylistAgent}.
+ *
+ * @param executor a callback Executor
+ * @param callback a PlaylistEventCallback
+ * @throws IllegalArgumentException if executor or callback is {@code null}.
+ */
+ public final void registerPlaylistEventCallback(
+ @NonNull /*@CallbackExecutor*/ Executor executor,
+ @NonNull PlaylistEventCallback callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+
+ synchronized (mLock) {
+ if (mCallbacks.get(callback) != null) {
+ Log.w(TAG, "callback is already added. Ignoring.");
+ return;
+ }
+ mCallbacks.put(callback, executor);
+ }
+ }
+
+ /**
+ * Unregister the previously registered {@link PlaylistEventCallback}.
+ *
+ * @param callback the callback to be removed
+ * @throws IllegalArgumentException if the callback is {@code null}.
+ */
+ public final void unregisterPlaylistEventCallback(@NonNull PlaylistEventCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ synchronized (mLock) {
+ mCallbacks.remove(callback);
+ }
+ }
+
+ /**
+ * Notifies the current playlist and playlist metadata. Call this API when the playlist is
+ * changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent, List, MediaMetadata2)}.
+ */
+ public final void notifyPlaylistChanged() {
+ SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+ final List<MediaItem2> playlist = getPlaylist();
+ final MediaMetadata2 metadata = getPlaylistMetadata();
+ for (int i = 0; i < callbacks.size(); i++) {
+ final PlaylistEventCallback callback = callbacks.keyAt(i);
+ final Executor executor = callbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlaylistChanged(
+ MediaPlaylistAgent.this, playlist, metadata);
+ }
+ });
+ }
+ }
+
+ /**
+ * Notifies the current playlist metadata. Call this API when the playlist metadata is changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onPlaylistMetadataChanged(MediaPlaylistAgent, MediaMetadata2)}.
+ */
+ public final void notifyPlaylistMetadataChanged() {
+ SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+ for (int i = 0; i < callbacks.size(); i++) {
+ final PlaylistEventCallback callback = callbacks.keyAt(i);
+ final Executor executor = callbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlaylistMetadataChanged(
+ MediaPlaylistAgent.this, MediaPlaylistAgent.this.getPlaylistMetadata());
+ }
+ });
+ }
+ }
+
+ /**
+ * Notifies the current shuffle mode. Call this API when the shuffle mode is changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onShuffleModeChanged(MediaPlaylistAgent, int)}.
+ */
+ public final void notifyShuffleModeChanged() {
+ SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+ for (int i = 0; i < callbacks.size(); i++) {
+ final PlaylistEventCallback callback = callbacks.keyAt(i);
+ final Executor executor = callbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onShuffleModeChanged(
+ MediaPlaylistAgent.this, MediaPlaylistAgent.this.getShuffleMode());
+ }
+ });
+ }
+ }
+
+ /**
+ * Notifies the current repeat mode. Call this API when the repeat mode is changed.
+ * <p>
+ * Registered {@link PlaylistEventCallback} would receive this event through the
+ * {@link PlaylistEventCallback#onRepeatModeChanged(MediaPlaylistAgent, int)}.
+ */
+ public final void notifyRepeatModeChanged() {
+ SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
+ for (int i = 0; i < callbacks.size(); i++) {
+ final PlaylistEventCallback callback = callbacks.keyAt(i);
+ final Executor executor = callbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onRepeatModeChanged(
+ MediaPlaylistAgent.this, MediaPlaylistAgent.this.getRepeatMode());
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the playlist
+ *
+ * @return playlist, or null if none is set.
+ */
+ public abstract @Nullable List<MediaItem2> getPlaylist();
+
+ /**
+ * Sets the playlist with the metadata.
+ * <p>
+ * When the playlist is changed, call {@link #notifyPlaylistChanged()} to notify changes to the
+ * registered callbacks.
+ *
+ * @param list playlist
+ * @param metadata metadata of the playlist
+ * @see #notifyPlaylistChanged()
+ */
+ public abstract void setPlaylist(@NonNull List<MediaItem2> list,
+ @Nullable MediaMetadata2 metadata);
+
+ /**
+ * Returns the playlist metadata
+ *
+ * @return metadata metadata of the playlist, or null if none is set
+ */
+ public abstract @Nullable MediaMetadata2 getPlaylistMetadata();
+
+ /**
+ * Updates the playlist metadata.
+ * <p>
+ * When the playlist metadata is changed, call {@link #notifyPlaylistMetadataChanged()} to
+ * notify changes to the registered callbacks.
+ *
+ * @param metadata metadata of the playlist
+ * @see #notifyPlaylistMetadataChanged()
+ */
+ public abstract void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
+
+ /**
+ * Returns currently playing media item.
+ */
+ public abstract MediaItem2 getCurrentMediaItem();
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the playlist,
+ * the current index of the playlist will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ public abstract void addPlaylistItem(int index, @NonNull MediaItem2 item);
+
+ /**
+ * Removes the media item from the playlist
+ *
+ * @param item media item to remove
+ */
+ public abstract void removePlaylistItem(@NonNull MediaItem2 item);
+
+ /**
+ * Replace the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ public abstract void replacePlaylistItem(int index, @NonNull MediaItem2 item);
+
+ /**
+ * Skips to the the media item, and plays from it.
+ *
+ * @param item media item to start playing from
+ */
+ public abstract void skipToPlaylistItem(@NonNull MediaItem2 item);
+
+ /**
+ * Skips to the previous item in the playlist.
+ */
+ public abstract void skipToPreviousItem();
+
+ /**
+ * Skips to the next item in the playlist.
+ */
+ public abstract void skipToNextItem();
+
+ /**
+ * Gets the repeat mode
+ *
+ * @return repeat mode
+ * @see #REPEAT_MODE_NONE
+ * @see #REPEAT_MODE_ONE
+ * @see #REPEAT_MODE_ALL
+ * @see #REPEAT_MODE_GROUP
+ */
+ public abstract @RepeatMode int getRepeatMode();
+
+ /**
+ * Sets the repeat mode.
+ * <p>
+ * When the repeat mode is changed, call {@link #notifyRepeatModeChanged()} to notify changes
+ * to the registered callbacks.
+ *
+ * @param repeatMode repeat mode
+ * @see #REPEAT_MODE_NONE
+ * @see #REPEAT_MODE_ONE
+ * @see #REPEAT_MODE_ALL
+ * @see #REPEAT_MODE_GROUP
+ * @see #notifyRepeatModeChanged()
+ */
+ public abstract void setRepeatMode(@RepeatMode int repeatMode);
+
+ /**
+ * Gets the shuffle mode
+ *
+ * @return The shuffle mode
+ * @see #SHUFFLE_MODE_NONE
+ * @see #SHUFFLE_MODE_ALL
+ * @see #SHUFFLE_MODE_GROUP
+ */
+ public abstract @ShuffleMode int getShuffleMode();
+
+ /**
+ * Sets the shuffle mode.
+ * <p>
+ * When the shuffle mode is changed, call {@link #notifyShuffleModeChanged()} to notify changes
+ * to the registered callbacks.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see #SHUFFLE_MODE_NONE
+ * @see #SHUFFLE_MODE_ALL
+ * @see #SHUFFLE_MODE_GROUP
+ * @see #notifyShuffleModeChanged()
+ */
+ public abstract void setShuffleMode(@ShuffleMode int shuffleMode);
+
+ /**
+ * Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
+ * {@link MediaPlayerBase.PlayerEventCallback} to the {@link MediaItem2}. Override this method
+ * if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them with
+ * {@link #setPlaylist(List, MediaMetadata2)}.
+ * <p>
+ * Session would throw an exception if this returns {@code null} for the dsd from the
+ * {@link MediaPlayerBase.PlayerEventCallback}.
+ * <p>
+ * Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
+ * with the {@param dsd}.
+ *
+ * @param dsd The dsd to query
+ * @return A {@link MediaItem2} object in the playlist that matches given {@code dsd}.
+ * @throws IllegalArgumentException if {@code dsd} is null
+ */
+ public @Nullable MediaItem2 getMediaItem(@NonNull DataSourceDesc dsd) {
+ if (dsd == null) {
+ throw new IllegalArgumentException("dsd shouldn't be null");
+ }
+ List<MediaItem2> itemList = getPlaylist();
+ if (itemList == null) {
+ return null;
+ }
+ for (int i = 0; i < itemList.size(); i++) {
+ MediaItem2 item = itemList.get(i);
+ if (item != null && item.getDataSourceDesc().equals(dsd)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private SimpleArrayMap<PlaylistEventCallback, Executor> getCallbacks() {
+ SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = new SimpleArrayMap<>();
+ synchronized (mLock) {
+ callbacks.putAll(mCallbacks);
+ }
+ return callbacks;
+ }
+
+ /**
+ * A callback class to receive notifications for events on the media player. See
+ * {@link MediaPlaylistAgent#registerPlaylistEventCallback(Executor, PlaylistEventCallback)}
+ * to register this callback.
+ */
+ public abstract static class PlaylistEventCallback {
+ /**
+ * Called when a playlist is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param list new playlist
+ * @param metadata new metadata
+ */
+ public void onPlaylistChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when a playlist metadata is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param metadata new metadata
+ */
+ public void onPlaylistMetadataChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when the shuffle mode is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param shuffleMode repeat mode
+ * @see #SHUFFLE_MODE_NONE
+ * @see #SHUFFLE_MODE_ALL
+ * @see #SHUFFLE_MODE_GROUP
+ */
+ public void onShuffleModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @ShuffleMode int shuffleMode) { }
+
+ /**
+ * Called when the repeat mode is changed.
+ *
+ * @param playlistAgent playlist agent for this event
+ * @param repeatMode repeat mode
+ * @see #REPEAT_MODE_NONE
+ * @see #REPEAT_MODE_ONE
+ * @see #REPEAT_MODE_ALL
+ * @see #REPEAT_MODE_GROUP
+ */
+ public void onRepeatModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+ @RepeatMode int repeatMode) { }
+ }
+}
diff --git a/androidx/media/MediaSession2.java b/androidx/media/MediaSession2.java
new file mode 100644
index 00000000..909e979b
--- /dev/null
+++ b/androidx/media/MediaSession2.java
@@ -0,0 +1,1670 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioFocusRequest;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaPlayerBase.BuffState;
+import androidx.media.MediaPlayerBase.PlayerState;
+import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
+import androidx.media.MediaPlaylistAgent.RepeatMode;
+import androidx.media.MediaPlaylistAgent.ShuffleMode;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps. Common use cases are as follows.
+ * <ul>
+ * <li>Bluetooth/wired headset key events support</li>
+ * <li>Android Auto/Wearable support</li>
+ * <li>Separating UI process and playback process</li>
+ * </ul>
+ * <p>
+ * A MediaSession2 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.
+ * <p>
+ * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
+ * to other processes to allow them to create a {@link MediaController2} to interact with the
+ * session.
+ * <p>
+ * When a session receive transport control commands, the session sends the commands directly to
+ * the the underlying media player set by {@link Builder} or
+ * {@link #updatePlayer}.
+ * <p>
+ * When an app is finished performing playback it must call {@link #close()} to clean up the session
+ * and notify any controllers.
+ * <p>
+ * {@link MediaSession2} objects should be used on the thread on the looper.
+ */
+@TargetApi(Build.VERSION_CODES.KITKAT)
+public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED,
+ ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
+ ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
+ ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING,
+ ERROR_CODE_SKIP_LIMIT_REACHED, ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE,
+ ERROR_CODE_SETUP_REQUIRED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ErrorCode {}
+
+ /**
+ * This is the default error code and indicates that none of the other error codes applies.
+ */
+ public static final int ERROR_CODE_UNKNOWN_ERROR = 0;
+
+ /**
+ * Error code when the application state is invalid to fulfill the request.
+ */
+ public static final int ERROR_CODE_APP_ERROR = 1;
+
+ /**
+ * Error code when the request is not supported by the application.
+ */
+ public static final int ERROR_CODE_NOT_SUPPORTED = 2;
+
+ /**
+ * Error code when the request cannot be performed because authentication has expired.
+ */
+ public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3;
+
+ /**
+ * Error code when a premium account is required for the request to succeed.
+ */
+ public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4;
+
+ /**
+ * Error code when too many concurrent streams are detected.
+ */
+ public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5;
+
+ /**
+ * Error code when the content is blocked due to parental controls.
+ */
+ public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6;
+
+ /**
+ * Error code when the content is blocked due to being regionally unavailable.
+ */
+ public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7;
+
+ /**
+ * Error code when the requested content is already playing.
+ */
+ public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8;
+
+ /**
+ * Error code when the application cannot skip any more songs because skip limit is reached.
+ */
+ public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9;
+
+ /**
+ * Error code when the action is interrupted due to some external event.
+ */
+ public static final int ERROR_CODE_ACTION_ABORTED = 10;
+
+ /**
+ * Error code when the playback navigation (previous, next) is not possible because the queue
+ * was exhausted.
+ */
+ public static final int ERROR_CODE_END_OF_QUEUE = 11;
+
+ /**
+ * Error code when the session needs user's manual intervention.
+ */
+ public static final int ERROR_CODE_SETUP_REQUIRED = 12;
+
+ /**
+ * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
+ * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
+ *
+ * #see #setOnDataSourceMissingHelper
+ */
+ public interface OnDataSourceMissingHelper {
+ /**
+ * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
+ * but it's needed now for preparing or playing it. Returned data source descriptor will be
+ * sent to the player directly to prepare or play the contents.
+ * <p>
+ * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
+ * playlist, so items cannot be differentiated.
+ *
+ * @param session the session for this event
+ * @param item media item from the controller
+ * @return a data source descriptor if the media item. Can be {@code null} if the content
+ * isn't available.
+ */
+ @Nullable DataSourceDesc onDataSourceMissing(@NonNull MediaSession2 session,
+ @NonNull MediaItem2 item);
+ }
+
+ /**
+ * Callback to be called for all incoming commands from {@link MediaController2}s.
+ * <p>
+ * If it's not set, the session will accept all controllers and all incoming commands by
+ * default.
+ */
+ public abstract static class SessionCallback {
+ /**
+ * Called when a controller is created for this session. Return allowed commands for
+ * controller. By default it allows all connection requests and commands.
+ * <p>
+ * You can reject the connection by return {@code null}. In that case, controller receives
+ * {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} and cannot
+ * be usable.
+ *
+ * @param session the session for this event
+ * @param controller controller information.
+ * @return allowed commands. Can be {@code null} to reject connection.
+ */
+ public @Nullable SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addAllPredefinedCommands();
+ return commands;
+ }
+
+ /**
+ * Called when a controller is disconnected
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ */
+ public void onDisconnected(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) { }
+
+ /**
+ * Called when a controller sent a command which will be sent directly to one of the
+ * following:
+ * <ul>
+ * <li> {@link MediaPlayerBase} </li>
+ * <li> {@link MediaPlaylistAgent} </li>
+ * <li> {@link android.media.AudioManager} or {@link VolumeProviderCompat} </li>
+ * </ul>
+ * Return {@code false} here to reject the request and stop sending command.
+ *
+ * @param session the session for this event
+ * @param controller controller information.
+ * @param command a command. This method will be called for every single command.
+ * @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PLAY
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PAUSE
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_RESET
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PREPARE
+ * @see SessionCommand2#COMMAND_CODE_PLAYBACK_SEEK_TO
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REMOVE_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_LIST
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_LIST_METADATA
+ * @see SessionCommand2#COMMAND_CODE_VOLUME_SET_VOLUME
+ * @see SessionCommand2#COMMAND_CODE_VOLUME_ADJUST_VOLUME
+ */
+ public boolean onCommandRequest(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull SessionCommand2 command) {
+ return true;
+ }
+
+ /**
+ * Called when a controller set rating of a media item through
+ * {@link MediaController2#setRating(String, Rating2)}.
+ * <p>
+ * To allow setting user rating for a {@link MediaItem2}, the media item's metadata
+ * should have {@link Rating2} with the key {@link MediaMetadata2#METADATA_KEY_USER_RATING},
+ * in order to provide possible rating style for controller. Controller will follow the
+ * rating style.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param mediaId media id from the controller
+ * @param rating new rating from the controller
+ * @see SessionCommand2#COMMAND_CODE_SESSION_SET_RATING
+ */
+ public void onSetRating(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
+ @NonNull String mediaId, @NonNull Rating2 rating) { }
+
+ /**
+ * Called when a controller sent a custom command through
+ * {@link MediaController2#sendCustomCommand(SessionCommand2, Bundle, ResultReceiver)}.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param customCommand custom command.
+ * @param args optional arguments
+ * @param cb optional result receiver
+ * @see SessionCommand2#COMMAND_CODE_CUSTOM
+ */
+ public void onCustomCommand(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull SessionCommand2 customCommand,
+ @Nullable Bundle args, @Nullable ResultReceiver cb) { }
+
+ /**
+ * Called when a controller requested to play a specific mediaId through
+ * {@link MediaController2#playFromMediaId(String, Bundle)}.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param mediaId media id
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
+ */
+ public void onPlayFromMediaId(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String mediaId,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to begin playback from a search query through
+ * {@link MediaController2#playFromSearch(String, Bundle)}
+ * <p>
+ * An empty query indicates that the app may play any music. The implementation should
+ * attempt to make a smart choice about what to play.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param query query string. Can be empty to indicate any suggested media
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
+ */
+ public void onPlayFromSearch(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String query,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to play a specific media item represented by a URI
+ * through {@link MediaController2#playFromUri(Uri, Bundle)}
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param uri uri
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_URI
+ */
+ public void onPlayFromUri(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Uri uri,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to prepare for playing a specific mediaId through
+ * {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+ * <p>
+ * During the preparation, a session should not hold audio focus in order to allow other
+ * sessions play seamlessly. The state of playback should be updated to
+ * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromMediaId} to handle requests for starting
+ * playback without preparation.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param mediaId media id to prepare
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
+ */
+ public void onPrepareFromMediaId(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String mediaId,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to prepare playback from a search query through
+ * {@link MediaController2#prepareFromSearch(String, Bundle)}.
+ * <p>
+ * An empty query indicates that the app may prepare any music. The implementation should
+ * attempt to make a smart choice about what to play.
+ * <p>
+ * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}
+ * after the preparation is done. The playback of the prepared content should start in the
+ * later calls of {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromSearch} to handle requests for starting playback without
+ * preparation.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param query query string. Can be empty to indicate any suggested media
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
+ */
+ public void onPrepareFromSearch(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull String query,
+ @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller requested to prepare a specific media item represented by a URI
+ * through {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+ * <p>
+ * During the preparation, a session should not hold audio focus in order to allow
+ * other sessions play seamlessly. The state of playback should be updated to
+ * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromUri} to handle requests for starting playback without
+ * preparation.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @param uri uri
+ * @param extras optional extra bundle
+ * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI
+ */
+ public void onPrepareFromUri(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller called {@link MediaController2#fastForward()}
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @see SessionCommand2#COMMAND_CODE_SESSION_FAST_FORWARD
+ */
+ public void onFastForward(@NonNull MediaSession2 session, ControllerInfo controller) { }
+
+ /**
+ * Called when a controller called {@link MediaController2#rewind()}
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @see SessionCommand2#COMMAND_CODE_SESSION_REWIND
+ */
+ public void onRewind(@NonNull MediaSession2 session, ControllerInfo controller) { }
+
+ /**
+ * Called when a controller called {@link MediaController2#subscribeRoutesInfo()}
+ * Session app should notify the routes information by calling
+ * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List)}.
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @see SessionCommand2#COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO
+ */
+ public void onSubscribeRoutesInfo(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) { }
+
+ /**
+ * Called when a controller called {@link MediaController2#unsubscribeRoutesInfo()}
+ *
+ * @param session the session for this event
+ * @param controller controller information
+ * @see SessionCommand2#COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO
+ */
+ public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller) { }
+
+ /**
+ * Called when a controller called {@link MediaController2#selectRoute(Bundle)}.
+ * @param session the session for this event
+ * @param controller controller information
+ * @param route The route bundle which may be from MediaRouteDescritor.asBundle().
+ * @see SessionCommand2#COMMAND_CODE_SESSION_SELECT_ROUTE
+ */
+ public void onSelectRoute(@NonNull MediaSession2 session,
+ @NonNull ControllerInfo controller, @NonNull Bundle route) { }
+ /**
+ * Called when the player's current playing item is changed
+ * <p>
+ * When it's called, you should invalidate previous playback information and wait for later
+ * callbacks.
+ *
+ * @param session the controller for this event
+ * @param player the player for this event
+ * @param item new item
+ */
+ public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
+
+ /**
+ * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+ * referenced by the given data source.
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param item the media item for which buffering is happening
+ */
+ public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
+ @NonNull MediaItem2 item) { }
+
+ /**
+ * Called to indicate that the state of the player has changed.
+ * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param state the new state of the player.
+ */
+ public void onPlayerStateChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, @PlayerState int state) { }
+
+ /**
+ * Called to report buffering events for a data source.
+ *
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param item the media item for which buffering is happening.
+ * @param state the new buffering state.
+ */
+ public void onBufferingStateChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { }
+
+ /**
+ * Called to indicate that the playback speed has changed.
+ * @param session the session for this event
+ * @param player the player for this event
+ * @param speed the new playback speed.
+ */
+ public void onPlaybackSpeedChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlayerBase player, float speed) { }
+
+ /**
+ * Called to indicate that {@link #seekTo(long)} is completed.
+ *
+ * @param session the session for this event.
+ * @param mpb the player that has completed seeking.
+ * @param position the previous seeking request.
+ * @see #seekTo(long)
+ */
+ public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb,
+ long position) { }
+
+ /**
+ * Called when a playlist is changed from the {@link MediaPlaylistAgent}.
+ * <p>
+ * This is called when the underlying agent has called
+ * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent,
+ * List, MediaMetadata2)}.
+ *
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param list new playlist
+ * @param metadata new metadata
+ */
+ public void onPlaylistChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent, @NonNull List<MediaItem2> list,
+ @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when a playlist metadata is changed.
+ *
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param metadata new metadata
+ */
+ public void onPlaylistMetadataChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent, @Nullable MediaMetadata2 metadata) { }
+
+ /**
+ * Called when the shuffle mode is changed.
+ *
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param shuffleMode repeat mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ public void onShuffleModeChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent,
+ @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+ /**
+ * Called when the repeat mode is changed.
+ *
+ * @param session the session for this event
+ * @param playlistAgent playlist agent for this event
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ public void onRepeatModeChanged(@NonNull MediaSession2 session,
+ @NonNull MediaPlaylistAgent playlistAgent,
+ @MediaPlaylistAgent.RepeatMode int repeatMode) { }
+ }
+
+ /**
+ * Base builder class for MediaSession2 and its subclass. Any change in this class should be
+ * also applied to the subclasses {@link MediaSession2.Builder} and
+ * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
+ * <p>
+ * APIs here should be package private, but should have documentations for developers.
+ * Otherwise, javadoc will generate documentation with the generic types such as follows.
+ * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
+ * <p>
+ * This class is hidden to prevent from generating test stub, which fails with
+ * 'unexpected bound' because it tries to auto generate stub class as follows.
+ * <pre>abstract static class BuilderBase<
+ * T extends android.media.MediaSession2,
+ * U extends android.media.MediaSession2.BuilderBase<
+ * T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ abstract static class BuilderBase
+ <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
+ final Context mContext;
+ MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
+ MediaPlayerBase mPlayer;
+ String mId;
+ Executor mCallbackExecutor;
+ C mCallback;
+ MediaPlaylistAgent mPlaylistAgent;
+ VolumeProviderCompat mVolumeProvider;
+ PendingIntent mSessionActivity;
+
+ BuilderBase(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ mContext = context;
+ // Ensure non-null
+ mId = "";
+ }
+
+ /**
+ * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
+ * to.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+ */
+ @NonNull U setPlayer(@NonNull MediaPlayerBase player) {
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mBaseImpl.setPlayer(player);
+ return (U) this;
+ }
+
+ /**
+ * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
+ * underlying {@link MediaPlayerBase}. The playlist agent should manage
+ * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
+ * <p>
+ * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
+ * agent.
+ *
+ * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
+ * {@code player}
+ */
+ U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ if (playlistAgent == null) {
+ throw new IllegalArgumentException("playlistAgent shouldn't be null");
+ }
+ mBaseImpl.setPlaylistAgent(playlistAgent);
+ return (U) this;
+ }
+
+ /**
+ * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
+ * set, system will adjust the appropriate stream volume for this session's player.
+ *
+ * @param volumeProvider The provider that will receive volume button events.
+ */
+ @NonNull U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+ mBaseImpl.setVolumeProvider(volumeProvider);
+ return (U) this;
+ }
+
+ /**
+ * 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 Context#startActivity(Intent)}.
+ *
+ * @param pi The intent to launch to show UI for this session.
+ */
+ @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
+ mBaseImpl.setSessionActivity(pi);
+ return (U) this;
+ }
+
+ /**
+ * Set ID of the session. If it's not set, an empty string with used to create a session.
+ * <p>
+ * Use this if and only if your app supports multiple playback at the same time and also
+ * wants to provide external apps to have finer controls of them.
+ *
+ * @param id id of the session. Must be unique per package.
+ * @throws IllegalArgumentException if id is {@code null}
+ * @return
+ */
+ @NonNull U setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mBaseImpl.setId(id);
+ return (U) this;
+ }
+
+ /**
+ * Set callback for the session.
+ *
+ * @param executor callback executor
+ * @param callback session callback.
+ * @return
+ */
+ @NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ mBaseImpl.setSessionCallback(executor, callback);
+ return (U) this;
+ }
+
+ /**
+ * Build {@link MediaSession2}.
+ *
+ * @return a new session
+ * @throws IllegalStateException if the session with the same id is already exists for the
+ * package.
+ */
+ @NonNull T build() {
+ return mBaseImpl.build();
+ }
+
+ void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
+ mBaseImpl = impl;
+ }
+ }
+
+ /**
+ * Builder for {@link MediaSession2}.
+ * <p>
+ * Any incoming event from the {@link MediaController2} will be handled on the thread
+ * that created session with the {@link Builder#build()}.
+ */
+ public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> {
+ private MediaSession2ImplBase.Builder mImpl;
+
+ public Builder(Context context) {
+ super(context);
+ mImpl = new MediaSession2ImplBase.Builder(context);
+ setImpl(mImpl);
+ }
+
+ @Override
+ public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+ return super.setPlayer(player);
+ }
+
+ @Override
+ public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ return super.setPlaylistAgent(playlistAgent);
+ }
+
+ @Override
+ public @NonNull Builder setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+ return super.setVolumeProvider(volumeProvider);
+ }
+
+ @Override
+ public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) {
+ return super.setSessionActivity(pi);
+ }
+
+ @Override
+ public @NonNull Builder setId(@NonNull String id) {
+ return super.setId(id);
+ }
+
+ @Override
+ public @NonNull Builder setSessionCallback(@NonNull Executor executor,
+ @NonNull SessionCallback callback) {
+ return super.setSessionCallback(executor, callback);
+ }
+
+ @Override
+ public @NonNull MediaSession2 build() {
+ return super.build();
+ }
+ }
+
+ /**
+ * Information of a controller.
+ */
+ public static final class ControllerInfo {
+ private final int mUid;
+ private final String mPackageName;
+ // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
+ private final IMediaControllerCallback mIControllerCallback;
+ private final boolean mIsTrusted;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public ControllerInfo(@NonNull Context context, int uid, int pid,
+ @NonNull String packageName, @NonNull IMediaControllerCallback callback) {
+ mUid = uid;
+ mPackageName = packageName;
+ mIControllerCallback = callback;
+ mIsTrusted = false;
+ }
+
+ /**
+ * @return package name of the controller
+ */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return uid of the controller
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+ * has a enabled notification listener so can be trusted to accept connection and incoming
+ * command request.
+ *
+ * @return {@code true} if the controller is trusted.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public boolean isTrusted() {
+ return mIsTrusted;
+ }
+
+ IBinder getId() {
+ return mIControllerCallback.asBinder();
+ }
+
+ @Override
+ public int hashCode() {
+ return mIControllerCallback.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerInfo)) {
+ return false;
+ }
+ ControllerInfo other = (ControllerInfo) obj;
+ return mIControllerCallback.asBinder().equals(other.mIControllerCallback.asBinder());
+ }
+
+ @Override
+ public String toString() {
+ return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + "})";
+ }
+
+ /**
+ * @hide
+ * @return Bundle
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public @NonNull Bundle toBundle() {
+ return new Bundle();
+ }
+
+ /**
+ * @hide
+ * @return Bundle
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
+ return new ControllerInfo(context, -1, -1, "TODO", null);
+ }
+
+ IMediaControllerCallback getControllerBinder() {
+ return mIControllerCallback;
+ }
+ }
+
+ /**
+ * Button for a {@link SessionCommand2} that will be shown by the controller.
+ * <p>
+ * It's up to the controller's decision to respect or ignore this customization request.
+ */
+ public static final class CommandButton {
+ private static final String KEY_COMMAND =
+ "android.media.media_session2.command_button.command";
+ private static final String KEY_ICON_RES_ID =
+ "android.media.media_session2.command_button.icon_res_id";
+ private static final String KEY_DISPLAY_NAME =
+ "android.media.media_session2.command_button.display_name";
+ private static final String KEY_EXTRAS =
+ "android.media.media_session2.command_button.extras";
+ private static final String KEY_ENABLED =
+ "android.media.media_session2.command_button.enabled";
+
+ private SessionCommand2 mCommand;
+ private int mIconResId;
+ private String mDisplayName;
+ private Bundle mExtras;
+ private boolean mEnabled;
+
+ private CommandButton(@Nullable SessionCommand2 command, int iconResId,
+ @Nullable String displayName, Bundle extras, boolean enabled) {
+ mCommand = command;
+ mIconResId = iconResId;
+ mDisplayName = displayName;
+ mExtras = extras;
+ mEnabled = enabled;
+ }
+
+ /**
+ * Get command associated with this button. Can be {@code null} if the button isn't enabled
+ * and only providing placeholder.
+ *
+ * @return command or {@code null}
+ */
+ public @Nullable SessionCommand2 getCommand() {
+ return mCommand;
+ }
+
+ /**
+ * Resource id of the button in this package. Can be {@code 0} if the command is predefined
+ * and custom icon isn't needed.
+ *
+ * @return resource id of the icon. Can be {@code 0}.
+ */
+ public int getIconResId() {
+ return mIconResId;
+ }
+
+ /**
+ * Display name of the button. Can be {@code null} or empty if the command is predefined
+ * and custom name isn't needed.
+ *
+ * @return custom display name. Can be {@code null} or empty.
+ */
+ public @Nullable String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Extra information of the button. It's private information between session and controller.
+ *
+ * @return
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Return whether it's enabled.
+ *
+ * @return {@code true} if enabled. {@code false} otherwise.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * @hide
+ * @return Bundle
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
+ bundle.putInt(KEY_ICON_RES_ID, mIconResId);
+ bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
+ bundle.putBundle(KEY_EXTRAS, mExtras);
+ bundle.putBoolean(KEY_ENABLED, mEnabled);
+ return bundle;
+ }
+
+ /**
+ * @hide
+ * @return CommandButton
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static @Nullable CommandButton fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ CommandButton.Builder builder = new CommandButton.Builder();
+ builder.setCommand(SessionCommand2.fromBundle(bundle.getBundle(KEY_COMMAND)));
+ builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
+ builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
+ builder.setExtras(bundle.getBundle(KEY_EXTRAS));
+ builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
+ try {
+ return builder.build();
+ } catch (IllegalStateException e) {
+ // Malformed or version mismatch. Return null for now.
+ return null;
+ }
+ }
+
+ /**
+ * Builder for {@link CommandButton}.
+ */
+ public static final class Builder {
+ private SessionCommand2 mCommand;
+ private int mIconResId;
+ private String mDisplayName;
+ private Bundle mExtras;
+ private boolean mEnabled;
+
+ /**
+ * Sets the {@link SessionCommand2} that would be sent to the session when the button
+ * is clicked.
+ *
+ * @param command session command
+ */
+ public @NonNull Builder setCommand(@Nullable SessionCommand2 command) {
+ mCommand = command;
+ return this;
+ }
+
+ /**
+ * Sets the bitmap-type (e.g. PNG) icon resource id of the button.
+ * <p>
+ * None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
+ * to {@link MediaController2} app, so please avoid using it especially for the older
+ * platform (API < 21).
+ *
+ * @param resId resource id of the button
+ */
+ public @NonNull Builder setIconResId(int resId) {
+ mIconResId = resId;
+ return this;
+ }
+
+ /**
+ * Sets the display name of the button.
+ *
+ * @param displayName display name of the button
+ */
+ public @NonNull Builder setDisplayName(@Nullable String displayName) {
+ mDisplayName = displayName;
+ return this;
+ }
+
+ /**
+ * Sets whether the button is enabled. Can be {@code false} to indicate that the button
+ * should be shown but isn't clickable.
+ *
+ * @param enabled {@code true} if the button is enabled and ready.
+ * {@code false} otherwise.
+ */
+ public @NonNull Builder setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets the extras of the button.
+ *
+ * @param extras extras information of the button
+ */
+ public @NonNull Builder setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /**
+ * Builds the {@link CommandButton}.
+ *
+ * @return a new {@link CommandButton}
+ */
+ public @NonNull CommandButton build() {
+ return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
+ }
+ }
+ }
+
+ abstract static class SupportLibraryImpl extends MediaInterface2.SessionPlayer
+ implements AutoCloseable {
+ abstract void updatePlayer(@NonNull MediaPlayerBase player,
+ @Nullable MediaPlaylistAgent playlistAgent,
+ @Nullable VolumeProviderCompat volumeProvider);
+ abstract @NonNull MediaPlayerBase getPlayer();
+ abstract @NonNull MediaPlaylistAgent getPlaylistAgent();
+ abstract @Nullable VolumeProviderCompat getVolumeProvider();
+ abstract @NonNull SessionToken2 getToken();
+ abstract @NonNull List<ControllerInfo> getConnectedControllers();
+
+ abstract void setAudioFocusRequest(@Nullable AudioFocusRequest afr);
+ abstract void setCustomLayout(@NonNull ControllerInfo controller,
+ @NonNull List<CommandButton> layout);
+ abstract void setAllowedCommands(@NonNull ControllerInfo controller,
+ @NonNull SessionCommandGroup2 commands);
+ abstract void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args);
+ abstract void sendCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver);
+ abstract void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+ @Nullable List<Bundle> routes);
+
+ // Internally used methods
+ abstract void setInstance(MediaSession2 session);
+ abstract MediaSession2 getInstance();
+ abstract Context getContext();
+ abstract Executor getCallbackExecutor();
+ abstract SessionCallback getCallback();
+ abstract boolean isClosed();
+ abstract PlaybackStateCompat getPlaybackStateCompat();
+ abstract PlaybackInfo getPlaybackInfo();
+ }
+
+ static final String TAG = "MediaSession2";
+
+ private final SupportLibraryImpl mImpl;
+
+ MediaSession2(SupportLibraryImpl impl) {
+ mImpl = impl;
+ mImpl.setInstance(this);
+ }
+
+ /**
+ * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
+ * to dispatch incoming event to.
+ * <p>
+ * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
+ * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
+ * <p>
+ * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
+ * agent.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
+ * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
+ * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
+ * appropriate stream volume for this session's player.
+ */
+ public void updatePlayer(@NonNull MediaPlayerBase player,
+ @Nullable MediaPlaylistAgent playlistAgent,
+ @Nullable VolumeProviderCompat volumeProvider) {
+ mImpl.updatePlayer(player, playlistAgent, volumeProvider);
+ }
+
+ @Override
+ public void close() {
+ try {
+ mImpl.close();
+ } catch (Exception e) {
+ // Should not be here.
+ }
+ }
+
+ /**
+ * @return player
+ */
+ public @NonNull MediaPlayerBase getPlayer() {
+ return mImpl.getPlayer();
+ }
+
+ /**
+ * @return playlist agent
+ */
+ public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+ return mImpl.getPlaylistAgent();
+ }
+
+ /**
+ * @return volume provider
+ */
+ public @Nullable VolumeProviderCompat getVolumeProvider() {
+ return mImpl.getVolumeProvider();
+ }
+
+ /**
+ * Returns the {@link SessionToken2} for creating {@link MediaController2}.
+ */
+ public @NonNull SessionToken2 getToken() {
+ return mImpl.getToken();
+ }
+
+ @NonNull Context getContext() {
+ return mImpl.getContext();
+ }
+
+ @NonNull Executor getCallbackExecutor() {
+ return mImpl.getCallbackExecutor();
+ }
+
+ @NonNull SessionCallback getCallback() {
+ return mImpl.getCallback();
+ }
+
+ /**
+ * Returns the list of connected controller.
+ *
+ * @return list of {@link ControllerInfo}
+ */
+ public @NonNull List<ControllerInfo> getConnectedControllers() {
+ return mImpl.getConnectedControllers();
+ }
+
+ /**
+ * Set the {@link AudioFocusRequest} to obtain the audio focus
+ *
+ * @param afr the full request parameters
+ */
+ public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+ mImpl.setAudioFocusRequest(afr);
+ }
+
+ /**
+ * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
+ * <p>
+ * It's up to controller's decision how to represent the layout in its own UI.
+ * Here's the same way
+ * (layout[i] means a CommandButton at index i in the given list)
+ * For 5 icons row
+ * layout[3] layout[1] layout[0] layout[2] layout[4]
+ * For 3 icons row
+ * layout[1] layout[0] layout[2]
+ * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
+ * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9]
+ * main row: layout[3] layout[1] layout[0] layout[2] layout[4]
+ * <p>
+ * This API can be called in the
+ * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
+ *
+ * @param controller controller to specify layout.
+ * @param layout ordered list of layout.
+ */
+ public void setCustomLayout(@NonNull ControllerInfo controller,
+ @NonNull List<CommandButton> layout) {
+ mImpl.setCustomLayout(controller, layout);
+ }
+
+ /**
+ * Set the new allowed command group for the controller
+ *
+ * @param controller controller to change allowed commands
+ * @param commands new allowed commands
+ */
+ public void setAllowedCommands(@NonNull ControllerInfo controller,
+ @NonNull SessionCommandGroup2 commands) {
+ mImpl.setAllowedCommands(controller, commands);
+ }
+
+ /**
+ * Send custom command to all connected controllers.
+ *
+ * @param command a command
+ * @param args optional argument
+ */
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+ mImpl.sendCustomCommand(command, args);
+ }
+
+ /**
+ * Send custom command to a specific controller.
+ *
+ * @param command a command
+ * @param args optional argument
+ * @param receiver result receiver for the session
+ */
+ public void sendCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) {
+ mImpl.sendCustomCommand(controller, command, args, receiver);
+ }
+
+ /**
+ * Play playback.
+ * <p>
+ * This calls {@link MediaPlayerBase#play()}.
+ */
+ @Override
+ public void play() {
+ mImpl.play();
+ }
+
+ /**
+ * Pause playback.
+ * <p>
+ * This calls {@link MediaPlayerBase#pause()}.
+ */
+ @Override
+ public void pause() {
+ mImpl.pause();
+ }
+
+ /**
+ * Stop playback, and reset the player to the initial state.
+ * <p>
+ * This calls {@link MediaPlayerBase#reset()}.
+ */
+ @Override
+ public void reset() {
+ mImpl.reset();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+ * to start playback.
+ * <p>
+ * This calls {@link MediaPlayerBase#reset()}.
+ */
+ @Override
+ public void prepare() {
+ mImpl.prepare();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ @Override
+ public void seekTo(long pos) {
+ mImpl.seekTo(pos);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void skipForward() {
+ mImpl.skipForward();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void skipBackward() {
+ mImpl.skipBackward();
+ }
+
+ /**
+ * Notify errors to the connected controllers
+ *
+ * @param errorCode error code
+ * @param extras extras
+ */
+ @Override
+ public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+ mImpl.notifyError(errorCode, extras);
+ }
+
+ /**
+ * Notify routes information to a connected controller
+ *
+ * @param controller controller information
+ * @param routes The routes information. Each bundle should be from
+ * MediaRouteDescritor.asBundle().
+ */
+ public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+ @Nullable List<Bundle> routes) {
+ mImpl.notifyRoutesInfoChanged(controller, routes);
+ }
+
+ /**
+ * Gets the current player state.
+ *
+ * @return the current player state
+ */
+ @Override
+ public @PlayerState int getPlayerState() {
+ return mImpl.getPlayerState();
+ }
+
+ /**
+ * Gets the current position.
+ *
+ * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
+ * unknown.
+ */
+ @Override
+ public long getCurrentPosition() {
+ return mImpl.getCurrentPosition();
+ }
+
+ @Override
+ public long getDuration() {
+ return mImpl.getDuration();
+ }
+
+ /**
+ * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
+ *
+ * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
+ */
+ @Override
+ public long getBufferedPosition() {
+ return mImpl.getBufferedPosition();
+ }
+
+ /**
+ * Gets the current buffering state of the player.
+ * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+ * buffered.
+ *
+ * @return the buffering state.
+ */
+ @Override
+ public @BuffState int getBufferingState() {
+ return mImpl.getBufferingState();
+ }
+
+ /**
+ * Get the playback speed.
+ *
+ * @return speed
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ return mImpl.getPlaybackSpeed();
+ }
+
+ /**
+ * Set the playback speed.
+ */
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ mImpl.setPlaybackSpeed(speed);
+ }
+
+ /**
+ * Sets the data source missing helper. Helper will be used to provide default implementation of
+ * {@link MediaPlaylistAgent} when it isn't set by developer.
+ * <p>
+ * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
+ * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
+ * when
+ * <ul>
+ * <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
+ * have {@link DataSourceDesc}</li>
+ * <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
+ * by {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ * In that case, an item would be added automatically without the data source.</li>
+ * </ul>
+ * <p>
+ * If it's not set, playback wouldn't happen for the item without data source descriptor.
+ * <p>
+ * The helper will be run on the executor that was specified by
+ * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
+ *
+ * @param helper a data source missing helper.
+ * @throws IllegalStateException when the helper is set when the playlist agent is set
+ * @see #setPlaylist(List, MediaMetadata2)
+ * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+ * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+ */
+ @Override
+ public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
+ mImpl.setOnDataSourceMissingHelper(helper);
+ }
+
+ /**
+ * Clears the data source missing helper.
+ *
+ * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
+ */
+ @Override
+ public void clearOnDataSourceMissingHelper() {
+ mImpl.clearOnDataSourceMissingHelper();
+ }
+
+ /**
+ * Returns the playlist from the {@link MediaPlaylistAgent}.
+ * <p>
+ * This list may differ with the list that was specified with
+ * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+ * implementation. Use media items returned here for other playlist agent APIs such as
+ * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+ *
+ * @return playlist
+ * @see MediaPlaylistAgent#getPlaylist()
+ * @see SessionCallback#onPlaylistChanged(
+ * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+ */
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return mImpl.getPlaylist();
+ }
+
+ /**
+ * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
+ * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
+ * items.
+ * <p>
+ * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
+ * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
+ * List, MediaMetadata2)} to know the operation finishes.
+ * <p>
+ * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
+ * {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
+ * when such media item is ready for preparation or play. Default implementation needs
+ * {@link OnDataSourceMissingHelper} for such case.
+ *
+ * @param list A list of {@link MediaItem2} objects to set as a play list.
+ * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
+ * items.
+ * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
+ * @see SessionCallback#onPlaylistChanged(
+ * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+ * @see #setOnDataSourceMissingHelper
+ */
+ @Override
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ mImpl.setPlaylist(list, metadata);
+ }
+
+ /**
+ * Skips to the item in the playlist.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
+ * on the playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @param item The item in the playlist you want to play
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ */
+ @Override
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ mImpl.skipToPlaylistItem(item);
+ }
+
+ /**
+ * Skips to the previous item.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
+ * playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ **/
+ @Override
+ public void skipToPreviousItem() {
+ mImpl.skipToPreviousItem();
+ }
+
+ /**
+ * Skips to the next item.
+ * <p>
+ * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
+ * playlist agent implementation, especially with the shuffle/repeat mode.
+ *
+ * @see #getShuffleMode()
+ * @see #getRepeatMode()
+ */
+ @Override
+ public void skipToNextItem() {
+ mImpl.skipToNextItem();
+ }
+
+ /**
+ * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
+ *
+ * @return the playlist metadata
+ */
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mImpl.getPlaylistMetadata();
+ }
+
+ /**
+ * Adds the media item to the playlist at position index. Index equals or greater than
+ * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
+ * the playlist.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add
+ * @param item the media item you want to add
+ */
+ @Override
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ mImpl.addPlaylistItem(index, item);
+ }
+
+ /**
+ * Removes the media item in the playlist.
+ * <p>
+ * If the item is the currently playing item of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @param item the media item you want to add
+ */
+ @Override
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ mImpl.removePlaylistItem(item);
+ }
+
+ /**
+ * Replaces the media item at index in the playlist. This can be also used to update metadata of
+ * an item.
+ *
+ * @param index the index of the item to replace
+ * @param item the new item
+ */
+ @Override
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ mImpl.replacePlaylistItem(index, item);
+ }
+
+ /**
+ * Return currently playing media item.
+ *
+ * @return currently playing media item
+ */
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ return mImpl.getCurrentMediaItem();
+ }
+
+ /**
+ * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
+ *
+ * @param metadata metadata of the playlist
+ */
+ @Override
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ mImpl.updatePlaylistMetadata(metadata);
+ }
+
+ /**
+ * Gets the repeat mode from the {@link MediaPlaylistAgent}.
+ *
+ * @return repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return mImpl.getRepeatMode();
+ }
+
+ /**
+ * Sets the repeat mode to the {@link MediaPlaylistAgent}.
+ *
+ * @param repeatMode repeat mode
+ * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+ * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+ * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+ */
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ mImpl.setRepeatMode(repeatMode);
+ }
+
+ /**
+ * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
+ *
+ * @return The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ @Override
+ public @ShuffleMode int getShuffleMode() {
+ return mImpl.getShuffleMode();
+ }
+
+ /**
+ * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
+ *
+ * @param shuffleMode The shuffle mode
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+ * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+ */
+ @Override
+ public void setShuffleMode(@ShuffleMode int shuffleMode) {
+ mImpl.setShuffleMode(shuffleMode);
+ }
+}
diff --git a/androidx/media/MediaSession2ImplBase.java b/androidx/media/MediaSession2ImplBase.java
new file mode 100644
index 00000000..e474b456
--- /dev/null
+++ b/androidx/media/MediaSession2ImplBase.java
@@ -0,0 +1,1220 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+import static androidx.media.MediaSession2.ControllerInfo;
+import static androidx.media.MediaSession2.ErrorCode;
+import static androidx.media.MediaSession2.OnDataSourceMissingHelper;
+import static androidx.media.MediaSession2.SessionCallback;
+import static androidx.media.SessionToken2.TYPE_LIBRARY_SERVICE;
+import static androidx.media.SessionToken2.TYPE_SESSION;
+import static androidx.media.SessionToken2.TYPE_SESSION_SERVICE;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaSession2ImplBase extends MediaSession2.SupportLibraryImpl {
+ static final String TAG = "MS2ImplBase";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final Object mLock = new Object();
+
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
+ private final MediaSessionCompat mSessionCompat;
+ private final MediaSession2StubImplBase mSession2Stub;
+ private final String mId;
+ private final Executor mCallbackExecutor;
+ private final SessionCallback mCallback;
+ private final SessionToken2 mSessionToken;
+ private final AudioManager mAudioManager;
+ private final MediaPlayerBase.PlayerEventCallback mPlayerEventCallback;
+ private final MediaPlaylistAgent.PlaylistEventCallback mPlaylistEventCallback;
+
+ private WeakReference<MediaSession2> mInstance;
+
+ @GuardedBy("mLock")
+ private MediaPlayerBase mPlayer;
+ @GuardedBy("mLock")
+ private MediaPlaylistAgent mPlaylistAgent;
+ @GuardedBy("mLock")
+ private SessionPlaylistAgentImplBase mSessionPlaylistAgent;
+ @GuardedBy("mLock")
+ private VolumeProviderCompat mVolumeProvider;
+ @GuardedBy("mLock")
+ private OnDataSourceMissingHelper mDsmHelper;
+ @GuardedBy("mLock")
+ private PlaybackStateCompat mPlaybackStateCompat;
+ @GuardedBy("mLock")
+ private PlaybackInfo mPlaybackInfo;
+
+ MediaSession2ImplBase(Context context, MediaSessionCompat sessionCompat, String id,
+ MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+ VolumeProviderCompat volumeProvider, PendingIntent sessionActivity,
+ Executor callbackExecutor, SessionCallback callback) {
+ mContext = context;
+ mHandlerThread = new HandlerThread("MediaController2_Thread");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ mSessionCompat = sessionCompat;
+ mSession2Stub = new MediaSession2StubImplBase(this);
+ mSessionCompat.setCallback(mSession2Stub, mHandler);
+ mSessionCompat.setSessionActivity(sessionActivity);
+
+ mId = id;
+ mCallback = callback;
+ mCallbackExecutor = callbackExecutor;
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ // TODO: Set callback values properly
+ mPlayerEventCallback = new MyPlayerEventCallback(this);
+ mPlaylistEventCallback = new MyPlaylistEventCallback(this);
+
+ // Infer type from the id and package name.
+ String libraryService = getServiceName(context, MediaLibraryService2.SERVICE_INTERFACE, id);
+ String sessionService = getServiceName(context, MediaSessionService2.SERVICE_INTERFACE, id);
+ if (sessionService != null && libraryService != null) {
+ throw new IllegalArgumentException("Ambiguous session type. Multiple"
+ + " session services define the same id=" + id);
+ } else if (libraryService != null) {
+ mSessionToken = new SessionToken2(Process.myUid(), TYPE_LIBRARY_SERVICE,
+ context.getPackageName(), libraryService, id, mSessionCompat.getSessionToken());
+ } else if (sessionService != null) {
+ mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION_SERVICE,
+ context.getPackageName(), sessionService, id, mSessionCompat.getSessionToken());
+ } else {
+ mSessionToken = new SessionToken2(Process.myUid(), TYPE_SESSION,
+ context.getPackageName(), null, id, mSessionCompat.getSessionToken());
+ }
+ updatePlayer(player, playlistAgent, volumeProvider);
+ }
+
+ @Override
+ public void updatePlayer(@NonNull MediaPlayerBase player,
+ @Nullable MediaPlaylistAgent playlistAgent,
+ @Nullable VolumeProviderCompat volumeProvider) {
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ final MediaPlayerBase oldPlayer;
+ final MediaPlaylistAgent oldAgent;
+ final PlaybackInfo info = createPlaybackInfo(volumeProvider, player.getAudioAttributes());
+ synchronized (mLock) {
+ oldPlayer = mPlayer;
+ oldAgent = mPlaylistAgent;
+ mPlayer = player;
+ if (playlistAgent == null) {
+ mSessionPlaylistAgent = new SessionPlaylistAgentImplBase(this, mPlayer);
+ if (mDsmHelper != null) {
+ mSessionPlaylistAgent.setOnDataSourceMissingHelper(mDsmHelper);
+ }
+ playlistAgent = mSessionPlaylistAgent;
+ }
+ mPlaylistAgent = playlistAgent;
+ mVolumeProvider = volumeProvider;
+ mPlaybackInfo = info;
+ }
+ if (player != oldPlayer) {
+ player.registerPlayerEventCallback(mCallbackExecutor, mPlayerEventCallback);
+ if (oldPlayer != null) {
+ // Warning: Poorly implement player may ignore this
+ oldPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
+ }
+ }
+ if (playlistAgent != oldAgent) {
+ playlistAgent.registerPlaylistEventCallback(mCallbackExecutor, mPlaylistEventCallback);
+ if (oldAgent != null) {
+ // Warning: Poorly implement player may ignore this
+ oldAgent.unregisterPlaylistEventCallback(mPlaylistEventCallback);
+ }
+ }
+
+ if (oldPlayer != null) {
+ mSession2Stub.notifyPlaybackInfoChanged(info);
+ notifyPlayerUpdatedNotLocked(oldPlayer);
+ }
+ // TODO(jaewan): Repeat the same thing for the playlist agent.
+ }
+
+ private PlaybackInfo createPlaybackInfo(VolumeProviderCompat volumeProvider,
+ AudioAttributesCompat attrs) {
+ PlaybackInfo info;
+ if (volumeProvider == null) {
+ int stream;
+ if (attrs == null) {
+ stream = AudioManager.STREAM_MUSIC;
+ } else {
+ stream = attrs.getVolumeControlStream();
+ if (stream == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+ // It may happen if the AudioAttributes doesn't have usage.
+ // Change it to the STREAM_MUSIC because it's not supported by audio manager
+ // for querying volume level.
+ stream = AudioManager.STREAM_MUSIC;
+ }
+ }
+
+ int controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
+ controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+ }
+ info = PlaybackInfo.createPlaybackInfo(
+ PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ attrs,
+ controlType,
+ mAudioManager.getStreamMaxVolume(stream),
+ mAudioManager.getStreamVolume(stream));
+ } else {
+ info = PlaybackInfo.createPlaybackInfo(
+ PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ attrs,
+ volumeProvider.getVolumeControl(),
+ volumeProvider.getMaxVolume(),
+ volumeProvider.getCurrentVolume());
+ }
+ return info;
+ }
+
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ if (mPlayer == null) {
+ return;
+ }
+ mPlayer.unregisterPlayerEventCallback(mPlayerEventCallback);
+ mPlayer = null;
+ mSessionCompat.release();
+ mHandler.removeCallbacksAndMessages(null);
+ if (mHandlerThread.isAlive()) {
+ mHandlerThread.quitSafely();
+ }
+ }
+ }
+
+ @Override
+ public @NonNull MediaPlayerBase getPlayer() {
+ synchronized (mLock) {
+ return mPlayer;
+ }
+ }
+
+ @Override
+ public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+ synchronized (mLock) {
+ return mPlaylistAgent;
+ }
+ }
+
+ @Override
+ public @Nullable VolumeProviderCompat getVolumeProvider() {
+ synchronized (mLock) {
+ return mVolumeProvider;
+ }
+ }
+
+ @Override
+ public @NonNull SessionToken2 getToken() {
+ return mSessionToken;
+ }
+
+ @Override
+ public @NonNull List<MediaSession2.ControllerInfo> getConnectedControllers() {
+ return mSession2Stub.getConnectedControllers();
+ }
+
+ @Override
+ public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+ // TODO(jaewan): implement this (b/72529899)
+ // mProvider.setAudioFocusRequest_impl(focusGain);
+ }
+
+ @Override
+ public void setCustomLayout(@NonNull ControllerInfo controller,
+ @NonNull List<MediaSession2.CommandButton> layout) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (layout == null) {
+ throw new IllegalArgumentException("layout shouldn't be null");
+ }
+ mSession2Stub.notifyCustomLayout(controller, layout);
+ }
+
+ @Override
+ public void setAllowedCommands(@NonNull ControllerInfo controller,
+ @NonNull SessionCommandGroup2 commands) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (commands == null) {
+ throw new IllegalArgumentException("commands shouldn't be null");
+ }
+ mSession2Stub.setAllowedCommands(controller, commands);
+ }
+
+ @Override
+ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mSession2Stub.sendCustomCommand(command, args);
+ }
+
+ @Override
+ public void sendCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull SessionCommand2 command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) {
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mSession2Stub.sendCustomCommand(controller, command, args, receiver);
+ }
+
+ @Override
+ public void play() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ player.play();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void pause() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ player.pause();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void reset() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ player.reset();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void prepare() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ player.prepare();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ player.seekTo(pos);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ @Override
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ @Override
+ public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+ mSession2Stub.notifyError(errorCode, extras);
+ }
+
+ @Override
+ public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller,
+ @Nullable List<Bundle> routes) {
+ mSession2Stub.notifyRoutesInfoChanged(controller, routes);
+ }
+
+ @Override
+ public @MediaPlayerBase.PlayerState int getPlayerState() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ return player.getPlayerState();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return MediaPlayerBase.PLAYER_STATE_ERROR;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ return player.getCurrentPosition();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return MediaPlayerBase.UNKNOWN_TIME;
+ }
+
+ @Override
+ public long getDuration() {
+ // TODO: implement
+ return 0;
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ return player.getBufferedPosition();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return MediaPlayerBase.UNKNOWN_TIME;
+ }
+
+ @Override
+ public @MediaPlayerBase.BuffState int getBufferingState() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ return player.getBufferingState();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return BUFFERING_STATE_UNKNOWN;
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ return player.getPlaybackSpeed();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return 1.0f;
+ }
+
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ if (player != null) {
+ player.setPlaybackSpeed(speed);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void setOnDataSourceMissingHelper(
+ @NonNull OnDataSourceMissingHelper helper) {
+ if (helper == null) {
+ throw new IllegalArgumentException("helper shouldn't be null");
+ }
+ synchronized (mLock) {
+ mDsmHelper = helper;
+ if (mSessionPlaylistAgent != null) {
+ mSessionPlaylistAgent.setOnDataSourceMissingHelper(helper);
+ }
+ }
+ }
+
+ @Override
+ public void clearOnDataSourceMissingHelper() {
+ synchronized (mLock) {
+ mDsmHelper = null;
+ if (mSessionPlaylistAgent != null) {
+ mSessionPlaylistAgent.clearOnDataSourceMissingHelper();
+ }
+ }
+ }
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ return agent.getPlaylist();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return null;
+ }
+
+ @Override
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ if (list == null) {
+ throw new IllegalArgumentException("list shouldn't be null");
+ }
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.setPlaylist(list, metadata);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.skipToPlaylistItem(item);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.skipToPreviousItem();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void skipToNextItem() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.skipToNextItem();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ return agent.getPlaylistMetadata();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return null;
+ }
+
+ @Override
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ if (index < 0) {
+ throw new IllegalArgumentException("index shouldn't be negative");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.addPlaylistItem(index, item);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.removePlaylistItem(item);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ if (index < 0) {
+ throw new IllegalArgumentException("index shouldn't be negative");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.replacePlaylistItem(index, item);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ return agent.getCurrentMediaItem();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return null;
+ }
+
+ @Override
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.updatePlaylistMetadata(metadata);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public @MediaPlaylistAgent.RepeatMode int getRepeatMode() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ return agent.getRepeatMode();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return MediaPlaylistAgent.REPEAT_MODE_NONE;
+ }
+
+ @Override
+ public void setRepeatMode(@MediaPlaylistAgent.RepeatMode int repeatMode) {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.setRepeatMode(repeatMode);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public @MediaPlaylistAgent.ShuffleMode int getShuffleMode() {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ return agent.getShuffleMode();
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ return MediaPlaylistAgent.SHUFFLE_MODE_NONE;
+ }
+
+ @Override
+ public void setShuffleMode(int shuffleMode) {
+ MediaPlaylistAgent agent;
+ synchronized (mLock) {
+ agent = mPlaylistAgent;
+ }
+ if (agent != null) {
+ agent.setShuffleMode(shuffleMode);
+ } else if (DEBUG) {
+ Log.d(TAG, "API calls after the close()", new IllegalStateException());
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // package private and private methods
+ ///////////////////////////////////////////////////
+
+ @Override
+ void setInstance(MediaSession2 session) {
+ mInstance = new WeakReference<>(session);
+
+ }
+
+ @Override
+ MediaSession2 getInstance() {
+ return mInstance.get();
+ }
+
+ @Override
+ Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ Executor getCallbackExecutor() {
+ return mCallbackExecutor;
+ }
+
+ @Override
+ SessionCallback getCallback() {
+ return mCallback;
+ }
+
+ @Override
+ boolean isClosed() {
+ return !mHandlerThread.isAlive();
+ }
+
+ @Override
+ PlaybackStateCompat getPlaybackStateCompat() {
+ synchronized (mLock) {
+ int state = MediaUtils2.createPlaybackStateCompatState(getPlayerState(),
+ getBufferingState());
+ // TODO: Consider following missing stuff
+ // - setCustomAction(): Fill custom layout
+ // - setErrorMessage(): Fill error message when notifyError() is called.
+ // - setActiveQueueItemId(): Fill here with the current media item...
+ // - setExtra(): No idea at this moment.
+ // TODO: generate actions from the allowed commands.
+ long allActions = PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SET_RATING
+ | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_PLAY_PAUSE
+ | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
+ | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
+ | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
+ | PlaybackStateCompat.ACTION_PLAY_FROM_URI | PlaybackStateCompat.ACTION_PREPARE
+ | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID
+ | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH
+ | PlaybackStateCompat.ACTION_PREPARE_FROM_URI
+ | PlaybackStateCompat.ACTION_SET_REPEAT_MODE
+ | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
+ | PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED;
+ return new PlaybackStateCompat.Builder()
+ .setState(state, getCurrentPosition(), getPlaybackSpeed())
+ .setActions(allActions)
+ .setBufferedPosition(getBufferedPosition())
+ .build();
+ }
+ }
+
+ @Override
+ PlaybackInfo getPlaybackInfo() {
+ synchronized (mLock) {
+ return mPlaybackInfo;
+ }
+ }
+
+ MediaSession2StubImplBase getSession2Stub() {
+ return mSession2Stub;
+ }
+
+ private static String getServiceName(Context context, String serviceAction, String id) {
+ PackageManager manager = context.getPackageManager();
+ Intent serviceIntent = new Intent(serviceAction);
+ serviceIntent.setPackage(context.getPackageName());
+ List<ResolveInfo> services = manager.queryIntentServices(serviceIntent,
+ PackageManager.GET_META_DATA);
+ String serviceName = null;
+ if (services != null) {
+ for (int i = 0; i < services.size(); i++) {
+ String serviceId = SessionToken2.getSessionId(services.get(i));
+ if (serviceId != null && TextUtils.equals(id, serviceId)) {
+ if (services.get(i).serviceInfo == null) {
+ continue;
+ }
+ if (serviceName != null) {
+ throw new IllegalArgumentException("Ambiguous session type. Multiple"
+ + " session services define the same id=" + id);
+ }
+ serviceName = services.get(i).serviceInfo.name;
+ }
+ }
+ }
+ return serviceName;
+ }
+
+ private void notifyPlayerUpdatedNotLocked(MediaPlayerBase oldPlayer) {
+ MediaPlayerBase player;
+ synchronized (mLock) {
+ player = mPlayer;
+ }
+ // TODO(jaewan): (Can be post-P) Find better way for player.getPlayerState() //
+ // In theory, Session.getXXX() may not be the same as Player.getXXX()
+ // and we should notify information of the session.getXXX() instead of
+ // player.getXXX()
+ // Notify to controllers as well.
+ final int state = player.getPlayerState();
+ if (state != oldPlayer.getPlayerState()) {
+ // TODO: implement
+ mSession2Stub.notifyPlayerStateChanged(state);
+ }
+
+ final long currentTimeMs = System.currentTimeMillis();
+ final long position = player.getCurrentPosition();
+ if (position != oldPlayer.getCurrentPosition()) {
+ // TODO: implement
+ //mSession2Stub.notifyPositionChangedNotLocked(currentTimeMs, position);
+ }
+
+ final float speed = player.getPlaybackSpeed();
+ if (speed != oldPlayer.getPlaybackSpeed()) {
+ // TODO: implement
+ //mSession2Stub.notifyPlaybackSpeedChangedNotLocked(speed);
+ }
+
+ final long bufferedPosition = player.getBufferedPosition();
+ if (bufferedPosition != oldPlayer.getBufferedPosition()) {
+ // TODO: implement
+ //mSession2Stub.notifyBufferedPositionChangedNotLocked(bufferedPosition);
+ }
+ }
+
+ private void notifyPlaylistChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+ List<MediaItem2> list, MediaMetadata2 metadata) {
+ synchronized (mLock) {
+ if (playlistAgent != mPlaylistAgent) {
+ // Ignore calls from the old agent.
+ return;
+ }
+ }
+ MediaSession2 session2 = mInstance.get();
+ if (session2 != null) {
+ mCallback.onPlaylistChanged(session2, playlistAgent, list, metadata);
+ mSession2Stub.notifyPlaylistChanged(list, metadata);
+ }
+ }
+
+ private void notifyPlaylistMetadataChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+ MediaMetadata2 metadata) {
+ synchronized (mLock) {
+ if (playlistAgent != mPlaylistAgent) {
+ // Ignore calls from the old agent.
+ return;
+ }
+ }
+ MediaSession2 session2 = mInstance.get();
+ if (session2 != null) {
+ mCallback.onPlaylistMetadataChanged(session2, playlistAgent, metadata);
+ mSession2Stub.notifyPlaylistMetadataChanged(metadata);
+ }
+ }
+
+ private void notifyRepeatModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+ int repeatMode) {
+ synchronized (mLock) {
+ if (playlistAgent != mPlaylistAgent) {
+ // Ignore calls from the old agent.
+ return;
+ }
+ }
+ MediaSession2 session2 = mInstance.get();
+ if (session2 != null) {
+ mCallback.onRepeatModeChanged(session2, playlistAgent, repeatMode);
+ mSession2Stub.notifyRepeatModeChanged(repeatMode);
+ }
+ }
+
+ private void notifyShuffleModeChangedOnExecutor(MediaPlaylistAgent playlistAgent,
+ int shuffleMode) {
+ synchronized (mLock) {
+ if (playlistAgent != mPlaylistAgent) {
+ // Ignore calls from the old agent.
+ return;
+ }
+ }
+ MediaSession2 session2 = mInstance.get();
+ if (session2 != null) {
+ mCallback.onShuffleModeChanged(session2, playlistAgent, shuffleMode);
+ mSession2Stub.notifyShuffleModeChanged(shuffleMode);
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Inner classes
+ ///////////////////////////////////////////////////
+
+ private static class MyPlayerEventCallback extends PlayerEventCallback {
+ private final WeakReference<MediaSession2ImplBase> mSession;
+
+ private MyPlayerEventCallback(MediaSession2ImplBase session) {
+ mSession = new WeakReference<>(session);
+ }
+
+ @Override
+ public void onCurrentDataSourceChanged(final MediaPlayerBase mpb,
+ final DataSourceDesc dsd) {
+ final MediaSession2ImplBase session = getSession();
+ // TODO: handle properly when dsd == null
+ if (session == null || dsd == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+ if (item == null) {
+ return;
+ }
+ session.getCallback().onCurrentMediaItemChanged(session.getInstance(), mpb,
+ item);
+ if (item.equals(session.getCurrentMediaItem())) {
+ session.getSession2Stub().notifyCurrentMediaItemChanged(item);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onMediaPrepared(final MediaPlayerBase mpb, final DataSourceDesc dsd) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null || dsd == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+ if (item == null) {
+ return;
+ }
+ session.getCallback().onMediaPrepared(session.getInstance(), mpb, item);
+ // TODO (jaewan): Notify controllers through appropriate callback. (b/74505936)
+ }
+ });
+ }
+
+ @Override
+ public void onPlayerStateChanged(final MediaPlayerBase mpb, final int state) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ session.getCallback().onPlayerStateChanged(session.getInstance(), mpb, state);
+ session.getSession2Stub().notifyPlayerStateChanged(state);
+ }
+ });
+ }
+
+ @Override
+ public void onBufferingStateChanged(final MediaPlayerBase mpb, final DataSourceDesc dsd,
+ final int state) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null || dsd == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ MediaItem2 item = MyPlayerEventCallback.this.getMediaItem(session, dsd);
+ if (item == null) {
+ return;
+ }
+ session.getCallback().onBufferingStateChanged(
+ session.getInstance(), mpb, item, state);
+ session.getSession2Stub().notifyBufferingStateChanged(item, state);
+ }
+ });
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(final MediaPlayerBase mpb, final float speed) {
+ final MediaSession2ImplBase session = getSession();
+ if (session == null) {
+ return;
+ }
+ session.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ session.getCallback().onPlaybackSpeedChanged(session.getInstance(), mpb, speed);
+ session.getSession2Stub().notifyPlaybackSpeedChanged(speed);
+ }
+ });
+ }
+
+ private MediaSession2ImplBase getSession() {
+ final MediaSession2ImplBase session = mSession.get();
+ if (session == null && DEBUG) {
+ Log.d(TAG, "Session is closed", new IllegalStateException());
+ }
+ return session;
+ }
+
+ private MediaItem2 getMediaItem(MediaSession2ImplBase session, DataSourceDesc dsd) {
+ MediaPlaylistAgent agent = session.getPlaylistAgent();
+ if (agent == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Session is closed", new IllegalStateException());
+ }
+ return null;
+ }
+ MediaItem2 item = agent.getMediaItem(dsd);
+ if (item == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Could not find matching item for dsd=" + dsd,
+ new NoSuchElementException());
+ }
+ }
+ return item;
+ }
+ }
+
+ private static class MyPlaylistEventCallback extends PlaylistEventCallback {
+ private final WeakReference<MediaSession2ImplBase> mSession;
+
+ private MyPlaylistEventCallback(MediaSession2ImplBase session) {
+ mSession = new WeakReference<>(session);
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list,
+ MediaMetadata2 metadata) {
+ final MediaSession2ImplBase session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ session.notifyPlaylistChangedOnExecutor(playlistAgent, list, metadata);
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent,
+ MediaMetadata2 metadata) {
+ final MediaSession2ImplBase session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ session.notifyPlaylistMetadataChangedOnExecutor(playlistAgent, metadata);
+ }
+
+ @Override
+ public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) {
+ final MediaSession2ImplBase session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ session.notifyRepeatModeChangedOnExecutor(playlistAgent, repeatMode);
+ }
+
+ @Override
+ public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) {
+ final MediaSession2ImplBase session = mSession.get();
+ if (session == null) {
+ return;
+ }
+ session.notifyShuffleModeChangedOnExecutor(playlistAgent, shuffleMode);
+ }
+ }
+
+ abstract static class BuilderBase
+ <T extends MediaSession2, C extends SessionCallback> {
+ final Context mContext;
+ MediaPlayerBase mPlayer;
+ String mId;
+ Executor mCallbackExecutor;
+ C mCallback;
+ MediaPlaylistAgent mPlaylistAgent;
+ VolumeProviderCompat mVolumeProvider;
+ PendingIntent mSessionActivity;
+
+ BuilderBase(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ mContext = context;
+ // Ensure MediaSessionCompat non-null or empty
+ mId = TAG;
+ }
+
+ void setPlayer(@NonNull MediaPlayerBase player) {
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mPlayer = player;
+ }
+
+ void setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+ if (playlistAgent == null) {
+ throw new IllegalArgumentException("playlistAgent shouldn't be null");
+ }
+ mPlaylistAgent = playlistAgent;
+ }
+
+ void setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
+ mVolumeProvider = volumeProvider;
+ }
+
+ void setSessionActivity(@Nullable PendingIntent pi) {
+ mSessionActivity = pi;
+ }
+
+ void setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mId = id;
+ }
+
+ void setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ }
+
+ abstract @NonNull T build();
+ }
+
+ static final class Builder extends
+ BuilderBase<MediaSession2, MediaSession2.SessionCallback> {
+ Builder(Context context) {
+ super(context);
+ }
+
+ @Override
+ public @NonNull MediaSession2 build() {
+ if (mCallbackExecutor == null) {
+ mCallbackExecutor = new MainHandlerExecutor(mContext);
+ }
+ if (mCallback == null) {
+ mCallback = new SessionCallback() {};
+ }
+ return new MediaSession2(new MediaSession2ImplBase(mContext,
+ new MediaSessionCompat(mContext, mId), mId, mPlayer, mPlaylistAgent,
+ mVolumeProvider, mSessionActivity, mCallbackExecutor, mCallback));
+ }
+ }
+
+ static class MainHandlerExecutor implements Executor {
+ private final Handler mHandler;
+
+ MainHandlerExecutor(Context context) {
+ mHandler = new Handler(context.getMainLooper());
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (!mHandler.post(command)) {
+ throw new RejectedExecutionException(mHandler + " is shutting down");
+ }
+ }
+ }
+}
diff --git a/androidx/media/MediaSession2StubImplBase.java b/androidx/media/MediaSession2StubImplBase.java
new file mode 100644
index 00000000..48e641ea
--- /dev/null
+++ b/androidx/media/MediaSession2StubImplBase.java
@@ -0,0 +1,929 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS;
+import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS;
+import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS;
+import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE;
+import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS;
+import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID;
+import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM;
+import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME;
+import static androidx.media.MediaConstants2.ARGUMENT_PID;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX;
+import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA;
+import static androidx.media.MediaConstants2.ARGUMENT_QUERY;
+import static androidx.media.MediaConstants2.ARGUMENT_RATING;
+import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER;
+import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE;
+import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION;
+import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE;
+import static androidx.media.MediaConstants2.ARGUMENT_UID;
+import static androidx.media.MediaConstants2.ARGUMENT_URI;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION;
+import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED;
+import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT;
+import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND;
+import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.session.IMediaControllerCallback;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class MediaSession2StubImplBase extends MediaSessionCompat.Callback {
+
+ private static final String TAG = "MS2StubImplBase";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final SparseArray<SessionCommand2> sCommandsForOnCommandRequest =
+ new SparseArray<>();
+
+ static {
+ SessionCommandGroup2 group = new SessionCommandGroup2();
+ group.addAllPlaybackCommands();
+ group.addAllPlaylistCommands();
+ group.addAllVolumeCommands();
+ Set<SessionCommand2> commands = group.getCommands();
+ for (SessionCommand2 command : commands) {
+ sCommandsForOnCommandRequest.append(command.getCommandCode(), command);
+ }
+ }
+
+ private final Object mLock = new Object();
+
+ final MediaSession2.SupportLibraryImpl mSession;
+ final Context mContext;
+
+ @GuardedBy("mLock")
+ private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final Set<IBinder> mConnectingControllers = new HashSet<>();
+ @GuardedBy("mLock")
+ private final ArrayMap<ControllerInfo, SessionCommandGroup2> mAllowedCommandGroupMap =
+ new ArrayMap<>();
+
+ MediaSession2StubImplBase(MediaSession2.SupportLibraryImpl session) {
+ mSession = session;
+ mContext = mSession.getContext();
+ }
+
+ @Override
+ public void onPrepare() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.prepare();
+ }
+ });
+ }
+
+ @Override
+ public void onPlay() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.play();
+ }
+ });
+ }
+
+ @Override
+ public void onPause() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.pause();
+ }
+ });
+ }
+
+ @Override
+ public void onStop() {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.reset();
+ }
+ });
+ }
+
+ @Override
+ public void onSeekTo(final long pos) {
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.seekTo(pos);
+ }
+ });
+ }
+
+ @Override
+ public void onCommand(String command, final Bundle extras, final ResultReceiver cb) {
+ switch (command) {
+ case CONTROLLER_COMMAND_CONNECT:
+ connect(extras, cb);
+ break;
+ case CONTROLLER_COMMAND_DISCONNECT:
+ disconnect(extras);
+ break;
+ case CONTROLLER_COMMAND_BY_COMMAND_CODE: {
+ final int commandCode = extras.getInt(ARGUMENT_COMMAND_CODE);
+ IMediaControllerCallback caller =
+ (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+ if (caller == null) {
+ return;
+ }
+
+ onCommand2(caller.asBinder(), commandCode, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) {
+ switch (commandCode) {
+ case COMMAND_CODE_PLAYBACK_PLAY:
+ mSession.play();
+ break;
+ case COMMAND_CODE_PLAYBACK_PAUSE:
+ mSession.pause();
+ break;
+ case COMMAND_CODE_PLAYBACK_RESET:
+ mSession.reset();
+ break;
+ case COMMAND_CODE_PLAYBACK_PREPARE:
+ mSession.prepare();
+ break;
+ case COMMAND_CODE_PLAYBACK_SEEK_TO: {
+ long seekPos = extras.getLong(ARGUMENT_SEEK_POSITION);
+ mSession.seekTo(seekPos);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE: {
+ int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE);
+ mSession.setRepeatMode(repeatMode);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE: {
+ int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE);
+ mSession.setShuffleMode(shuffleMode);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SET_LIST: {
+ List<MediaItem2> list = MediaUtils2.fromMediaItem2ParcelableArray(
+ extras.getParcelableArray(ARGUMENT_PLAYLIST));
+ MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ mSession.setPlaylist(list, metadata);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SET_LIST_METADATA: {
+ MediaMetadata2 metadata = MediaMetadata2.fromBundle(
+ extras.getBundle(ARGUMENT_PLAYLIST_METADATA));
+ mSession.updatePlaylistMetadata(metadata);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_ADD_ITEM: {
+ int index = extras.getInt(ARGUMENT_PLAYLIST_INDEX);
+ MediaItem2 item = MediaItem2.fromBundle(
+ extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ mSession.addPlaylistItem(index, item);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_REMOVE_ITEM: {
+ MediaItem2 item = MediaItem2.fromBundle(
+ extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ mSession.removePlaylistItem(item);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_REPLACE_ITEM: {
+ int index = extras.getInt(ARGUMENT_PLAYLIST_INDEX);
+ MediaItem2 item = MediaItem2.fromBundle(
+ extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ mSession.replacePlaylistItem(index, item);
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM: {
+ mSession.skipToNextItem();
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM: {
+ mSession.skipToPreviousItem();
+ break;
+ }
+ case COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM: {
+ MediaItem2 item = MediaItem2.fromBundle(
+ extras.getBundle(ARGUMENT_MEDIA_ITEM));
+ mSession.skipToPlaylistItem(item);
+ break;
+ }
+ case COMMAND_CODE_VOLUME_SET_VOLUME: {
+ int value = extras.getInt(ARGUMENT_VOLUME);
+ int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
+ VolumeProviderCompat vp = mSession.getVolumeProvider();
+ if (vp == null) {
+ // TODO: Revisit
+ } else {
+ vp.onSetVolumeTo(value);
+ }
+ break;
+ }
+ case COMMAND_CODE_VOLUME_ADJUST_VOLUME: {
+ int direction = extras.getInt(ARGUMENT_VOLUME_DIRECTION);
+ int flags = extras.getInt(ARGUMENT_VOLUME_FLAGS);
+ VolumeProviderCompat vp = mSession.getVolumeProvider();
+ if (vp == null) {
+ // TODO: Revisit
+ } else {
+ vp.onAdjustVolume(direction);
+ }
+ break;
+ }
+ case COMMAND_CODE_SESSION_REWIND: {
+ mSession.getCallback().onRewind(
+ mSession.getInstance(), controller);
+ break;
+ }
+ case COMMAND_CODE_SESSION_FAST_FORWARD: {
+ mSession.getCallback().onFastForward(
+ mSession.getInstance(), controller);
+ break;
+ }
+ case COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID: {
+ String mediaId = extras.getString(ARGUMENT_MEDIA_ID);
+ Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+ mSession.getCallback().onPlayFromMediaId(
+ mSession.getInstance(), controller, mediaId, extra);
+ break;
+ }
+ case COMMAND_CODE_SESSION_PLAY_FROM_SEARCH: {
+ String query = extras.getString(ARGUMENT_QUERY);
+ Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+ mSession.getCallback().onPlayFromSearch(
+ mSession.getInstance(), controller, query, extra);
+ break;
+ }
+ case COMMAND_CODE_SESSION_PLAY_FROM_URI: {
+ Uri uri = extras.getParcelable(ARGUMENT_URI);
+ Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+ mSession.getCallback().onPlayFromUri(
+ mSession.getInstance(), controller, uri, extra);
+ break;
+ }
+ case COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID: {
+ String mediaId = extras.getString(ARGUMENT_MEDIA_ID);
+ Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+ mSession.getCallback().onPrepareFromMediaId(
+ mSession.getInstance(), controller, mediaId, extra);
+ break;
+ }
+ case COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH: {
+ String query = extras.getString(ARGUMENT_QUERY);
+ Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+ mSession.getCallback().onPrepareFromSearch(
+ mSession.getInstance(), controller, query, extra);
+ break;
+ }
+ case COMMAND_CODE_SESSION_PREPARE_FROM_URI: {
+ Uri uri = extras.getParcelable(ARGUMENT_URI);
+ Bundle extra = extras.getBundle(ARGUMENT_EXTRAS);
+ mSession.getCallback().onPrepareFromUri(
+ mSession.getInstance(), controller, uri, extra);
+ break;
+ }
+ case COMMAND_CODE_SESSION_SET_RATING: {
+ String mediaId = extras.getString(ARGUMENT_MEDIA_ID);
+ Rating2 rating = Rating2.fromBundle(
+ extras.getBundle(ARGUMENT_RATING));
+ mSession.getCallback().onSetRating(
+ mSession.getInstance(), controller, mediaId, rating);
+ break;
+ }
+ case COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO: {
+ mSession.getCallback().onSubscribeRoutesInfo(
+ mSession.getInstance(), controller);
+ break;
+ }
+ case COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO: {
+ mSession.getCallback().onUnsubscribeRoutesInfo(
+ mSession.getInstance(), controller);
+ break;
+ }
+ case COMMAND_CODE_SESSION_SELECT_ROUTE: {
+ Bundle route = extras.getBundle(ARGUMENT_ROUTE_BUNDLE);
+ mSession.getCallback().onSelectRoute(
+ mSession.getInstance(), controller, route);
+ break;
+ }
+ case COMMAND_CODE_PLAYBACK_SET_SPEED: {
+ float speed = extras.getFloat(ARGUMENT_PLAYBACK_SPEED);
+ mSession.setPlaybackSpeed(speed);
+ break;
+ }
+ }
+ }
+ });
+ break;
+ }
+ case CONTROLLER_COMMAND_BY_CUSTOM_COMMAND: {
+ final SessionCommand2 customCommand =
+ SessionCommand2.fromBundle(extras.getBundle(ARGUMENT_CUSTOM_COMMAND));
+ IMediaControllerCallback caller =
+ (IMediaControllerCallback) extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK);
+ if (caller == null || customCommand == null) {
+ return;
+ }
+
+ final Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS);
+ onCommand2(caller.asBinder(), customCommand, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ mSession.getCallback().onCustomCommand(
+ mSession.getInstance(), controller, customCommand, args, cb);
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ List<ControllerInfo> getConnectedControllers() {
+ ArrayList<ControllerInfo> controllers = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mControllers.size(); i++) {
+ controllers.add(mControllers.valueAt(i));
+ }
+ }
+ return controllers;
+ }
+
+ void notifyCustomLayout(ControllerInfo controller, final List<CommandButton> layout) {
+ notifyInternal(controller, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_COMMAND_BUTTONS,
+ MediaUtils2.toCommandButtonParcelableArray(layout));
+ controller.getControllerBinder().onEvent(SESSION_EVENT_SET_CUSTOM_LAYOUT, bundle);
+ }
+ });
+ }
+
+ void setAllowedCommands(ControllerInfo controller, final SessionCommandGroup2 commands) {
+ synchronized (mLock) {
+ mAllowedCommandGroupMap.put(controller, commands);
+ }
+ notifyInternal(controller, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_ALLOWED_COMMANDS, commands.toBundle());
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED, bundle);
+ }
+ });
+ }
+
+ public void sendCustomCommand(ControllerInfo controller, final SessionCommand2 command,
+ final Bundle args, final ResultReceiver receiver) {
+ if (receiver != null && controller == null) {
+ throw new IllegalArgumentException("Controller shouldn't be null if result receiver is"
+ + " specified");
+ }
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ notifyInternal(controller, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ // TODO: Send this event through MediaSessionCompat.XXX()
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+ bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+ bundle.putParcelable(ARGUMENT_RESULT_RECEIVER, receiver);
+ controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+ }
+ });
+ }
+
+ public void sendCustomCommand(final SessionCommand2 command, final Bundle args) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ final Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle());
+ bundle.putBundle(ARGUMENT_ARGUMENTS, args);
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ controller.getControllerBinder().onEvent(SESSION_EVENT_SEND_CUSTOM_COMMAND, bundle);
+ }
+ });
+ }
+
+ void notifyCurrentMediaItemChanged(final MediaItem2 item) {
+ notifyAll(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyPlaybackInfoChanged(final PlaybackInfo info) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_PLAYBACK_INFO, info.toBundle());
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyPlayerStateChanged(final int state) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_PLAYER_STATE, state);
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_PLAYER_STATE_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyPlaybackSpeedChanged(final float speed) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(
+ ARGUMENT_PLAYBACK_STATE_COMPAT, mSession.getPlaybackStateCompat());
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyBufferingStateChanged(final MediaItem2 item, final int bufferingState) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle());
+ bundle.putInt(ARGUMENT_BUFFERING_STATE, bufferingState);
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED, bundle);
+ }
+ });
+ }
+
+ void notifyError(final int errorCode, final Bundle extras) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_ERROR_CODE, errorCode);
+ bundle.putBundle(ARGUMENT_EXTRAS, extras);
+ controller.getControllerBinder().onEvent(SESSION_EVENT_ON_ERROR, bundle);
+ }
+ });
+ }
+
+ void notifyRoutesInfoChanged(@NonNull final ControllerInfo controller,
+ @Nullable final List<Bundle> routes) {
+ notifyInternal(controller, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = null;
+ if (routes != null) {
+ bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_ROUTE_BUNDLE, routes.toArray(new Bundle[0]));
+ }
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_ROUTES_INFO_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyPlaylistChanged(final List<MediaItem2> playlist,
+ final MediaMetadata2 metadata) {
+ notifyAll(COMMAND_CODE_PLAYLIST_GET_LIST, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArray(ARGUMENT_PLAYLIST,
+ MediaUtils2.toMediaItem2ParcelableArray(playlist));
+ bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ metadata == null ? null : metadata.toBundle());
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_PLAYLIST_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyPlaylistMetadataChanged(final MediaMetadata2 metadata) {
+ notifyAll(SessionCommand2.COMMAND_CODE_PLAYLIST_GET_LIST_METADATA, new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ metadata == null ? null : metadata.toBundle());
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyRepeatModeChanged(final int repeatMode) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_REPEAT_MODE, repeatMode);
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_REPEAT_MODE_CHANGED, bundle);
+ }
+ });
+ }
+
+ void notifyShuffleModeChanged(final int shuffleMode) {
+ notifyAll(new Session2Runnable() {
+ @Override
+ public void run(ControllerInfo controller) throws RemoteException {
+ Bundle bundle = new Bundle();
+ bundle.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode);
+ controller.getControllerBinder().onEvent(
+ SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED, bundle);
+ }
+ });
+ }
+
+ private List<ControllerInfo> getControllers() {
+ ArrayList<ControllerInfo> controllers = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mControllers.size(); i++) {
+ controllers.add(mControllers.valueAt(i));
+ }
+ }
+ return controllers;
+ }
+
+ private void notifyAll(@NonNull Session2Runnable runnable) {
+ List<ControllerInfo> controllers = getControllers();
+ for (int i = 0; i < controllers.size(); i++) {
+ notifyInternal(controllers.get(i), runnable);
+ }
+ }
+
+ private void notifyAll(int commandCode, @NonNull Session2Runnable runnable) {
+ List<ControllerInfo> controllers = getControllers();
+ for (int i = 0; i < controllers.size(); i++) {
+ ControllerInfo controller = controllers.get(i);
+ if (isAllowedCommand(controller, commandCode)) {
+ notifyInternal(controller, runnable);
+ }
+ }
+ }
+
+ // TODO: Add a way to check permission from here.
+ private void notifyInternal(@NonNull ControllerInfo controller,
+ @NonNull Session2Runnable runnable) {
+ if (controller == null || controller.getControllerBinder() == null) {
+ return;
+ }
+ try {
+ runnable.run(controller);
+ } catch (DeadObjectException e) {
+ if (DEBUG) {
+ Log.d(TAG, controller.toString() + " is gone", e);
+ }
+ onControllerClosed(controller.getControllerBinder());
+ } catch (RemoteException e) {
+ // Currently it's TransactionTooLargeException or DeadSystemException.
+ // We'd better to leave log for those cases because
+ // - TransactionTooLargeException means that we may need to fix our code.
+ // (e.g. add pagination or special way to deliver Bitmap)
+ // - DeadSystemException means that errors around it can be ignored.
+ Log.w(TAG, "Exception in " + controller.toString(), e);
+ }
+ }
+
+ private boolean isAllowedCommand(ControllerInfo controller, SessionCommand2 command) {
+ SessionCommandGroup2 allowedCommands;
+ synchronized (mLock) {
+ allowedCommands = mAllowedCommandGroupMap.get(controller);
+ }
+ return allowedCommands != null && allowedCommands.hasCommand(command);
+ }
+
+ private boolean isAllowedCommand(ControllerInfo controller, int commandCode) {
+ SessionCommandGroup2 allowedCommands;
+ synchronized (mLock) {
+ allowedCommands = mAllowedCommandGroupMap.get(controller);
+ }
+ return allowedCommands != null && allowedCommands.hasCommand(commandCode);
+ }
+
+ private void onCommand2(@NonNull IBinder caller, final int commandCode,
+ @NonNull final Session2Runnable runnable) {
+ // TODO: Prevent instantiation of SessionCommand2
+ onCommand2(caller, new SessionCommand2(commandCode), runnable);
+ }
+
+ private void onCommand2(@NonNull IBinder caller, @NonNull final SessionCommand2 sessionCommand,
+ @NonNull final Session2Runnable runnable) {
+ final ControllerInfo controller;
+ synchronized (mLock) {
+ controller = mControllers.get(caller);
+ }
+ if (mSession == null || controller == null) {
+ return;
+ }
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (!isAllowedCommand(controller, sessionCommand)) {
+ return;
+ }
+ int commandCode = sessionCommand.getCommandCode();
+ SessionCommand2 command = sCommandsForOnCommandRequest.get(commandCode);
+ if (command != null) {
+ boolean accepted = mSession.getCallback().onCommandRequest(
+ mSession.getInstance(), controller, command);
+ if (!accepted) {
+ // Don't run rejected command.
+ if (DEBUG) {
+ Log.d(TAG, "Command (code=" + commandCode + ") from "
+ + controller + " was rejected by " + mSession);
+ }
+ return;
+ }
+ }
+ try {
+ runnable.run(controller);
+ } catch (RemoteException e) {
+ // Currently it's TransactionTooLargeException or DeadSystemException.
+ // We'd better to leave log for those cases because
+ // - TransactionTooLargeException means that we may need to fix our code.
+ // (e.g. add pagination or special way to deliver Bitmap)
+ // - DeadSystemException means that errors around it can be ignored.
+ Log.w(TAG, "Exception in " + controller.toString(), e);
+ }
+ }
+ });
+ }
+
+ private void onControllerClosed(IMediaControllerCallback iController) {
+ ControllerInfo controller;
+ synchronized (mLock) {
+ controller = mControllers.remove(iController.asBinder());
+ if (DEBUG) {
+ Log.d(TAG, "releasing " + controller);
+ }
+ }
+ if (controller == null) {
+ return;
+ }
+ final ControllerInfo removedController = controller;
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ mSession.getCallback().onDisconnected(mSession.getInstance(), removedController);
+ }
+ });
+ }
+
+ private ControllerInfo createControllerInfo(Bundle extras) {
+ IMediaControllerCallback callback = IMediaControllerCallback.Stub.asInterface(
+ extras.getBinder(ARGUMENT_ICONTROLLER_CALLBACK));
+ String packageName = extras.getString(ARGUMENT_PACKAGE_NAME);
+ int uid = extras.getInt(ARGUMENT_UID);
+ int pid = extras.getInt(ARGUMENT_PID);
+ // TODO: sanity check for packageName, uid, and pid.
+
+ return new ControllerInfo(mContext, uid, pid, packageName, callback);
+ }
+
+ private void connect(Bundle extras, final ResultReceiver cb) {
+ final ControllerInfo controllerInfo = createControllerInfo(extras);
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ synchronized (mLock) {
+ // Keep connecting controllers.
+ // This helps sessions to call APIs in the onConnect()
+ // (e.g. setCustomLayout()) instead of pending them.
+ mConnectingControllers.add(controllerInfo.getId());
+ }
+ SessionCommandGroup2 allowedCommands = mSession.getCallback().onConnect(
+ mSession.getInstance(), controllerInfo);
+ // Don't reject connection for the request from trusted app.
+ // Otherwise server will fail to retrieve session's information to dispatch
+ // media keys to.
+ boolean accept = allowedCommands != null || controllerInfo.isTrusted();
+ if (accept) {
+ if (DEBUG) {
+ Log.d(TAG, "Accepting connection, controllerInfo=" + controllerInfo
+ + " allowedCommands=" + allowedCommands);
+ }
+ if (allowedCommands == null) {
+ // For trusted apps, send non-null allowed commands to keep
+ // connection.
+ allowedCommands = new SessionCommandGroup2();
+ }
+ synchronized (mLock) {
+ mConnectingControllers.remove(controllerInfo.getId());
+ mControllers.put(controllerInfo.getId(), controllerInfo);
+ mAllowedCommandGroupMap.put(controllerInfo, allowedCommands);
+ }
+ // If connection is accepted, notify the current state to the
+ // controller. It's needed because we cannot call synchronous calls
+ // between session/controller.
+ // Note: We're doing this after the onConnectionChanged(), but there's
+ // no guarantee that events here are notified after the
+ // onConnected() because IMediaController2 is oneway (i.e. async
+ // call) and Stub will use thread poll for incoming calls.
+ final Bundle resultData = new Bundle();
+ resultData.putBundle(ARGUMENT_ALLOWED_COMMANDS,
+ allowedCommands.toBundle());
+ resultData.putInt(ARGUMENT_PLAYER_STATE, mSession.getPlayerState());
+ resultData.putInt(ARGUMENT_BUFFERING_STATE, mSession.getBufferingState());
+ resultData.putParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT,
+ mSession.getPlaybackStateCompat());
+ resultData.putInt(ARGUMENT_REPEAT_MODE, mSession.getRepeatMode());
+ resultData.putInt(ARGUMENT_SHUFFLE_MODE, mSession.getShuffleMode());
+ final List<MediaItem2> playlist = allowedCommands.hasCommand(
+ COMMAND_CODE_PLAYLIST_GET_LIST) ? mSession.getPlaylist() : null;
+ if (playlist != null) {
+ resultData.putParcelableArray(ARGUMENT_PLAYLIST,
+ MediaUtils2.toMediaItem2ParcelableArray(playlist));
+ }
+ final MediaItem2 currentMediaItem =
+ allowedCommands.hasCommand(COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM)
+ ? mSession.getCurrentMediaItem() : null;
+ if (currentMediaItem != null) {
+ resultData.putBundle(ARGUMENT_MEDIA_ITEM, currentMediaItem.toBundle());
+ }
+ resultData.putBundle(ARGUMENT_PLAYBACK_INFO,
+ mSession.getPlaybackInfo().toBundle());
+ final MediaMetadata2 playlistMetadata = mSession.getPlaylistMetadata();
+ if (playlistMetadata != null) {
+ resultData.putBundle(ARGUMENT_PLAYLIST_METADATA,
+ playlistMetadata.toBundle());
+ }
+ // Double check if session is still there, because close() can be
+ // called in another thread.
+ if (mSession.isClosed()) {
+ return;
+ }
+ cb.send(CONNECT_RESULT_CONNECTED, resultData);
+ } else {
+ synchronized (mLock) {
+ mConnectingControllers.remove(controllerInfo.getId());
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Rejecting connection, controllerInfo=" + controllerInfo);
+ }
+ cb.send(CONNECT_RESULT_DISCONNECTED, null);
+ }
+ }
+ });
+ }
+
+ private void disconnect(Bundle extras) {
+ final ControllerInfo controllerInfo = createControllerInfo(extras);
+ mSession.getCallbackExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.isClosed()) {
+ return;
+ }
+ mSession.getCallback().onDisconnected(mSession.getInstance(), controllerInfo);
+ }
+ });
+ }
+
+ @FunctionalInterface
+ private interface Session2Runnable {
+ void run(ControllerInfo controller) throws RemoteException;
+ }
+}
diff --git a/androidx/media/MediaSession2Test.java b/androidx/media/MediaSession2Test.java
new file mode 100644
index 00000000..5e7ed0e9
--- /dev/null
+++ b/androidx/media/MediaSession2Test.java
@@ -0,0 +1,1053 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static android.media.AudioAttributes.CONTENT_TYPE_MUSIC;
+
+import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+import static androidx.media.VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.ResultReceiver;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaController2.PlaybackInfo;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaSession2}.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaSession2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaSession2Test";
+
+ private MediaSession2 mSession;
+ private MockPlayer mPlayer;
+ private MockPlaylistAgent mMockAgent;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mPlayer = new MockPlayer(0);
+ mMockAgent = new MockPlaylistAgent();
+
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() == controller.getUid()) {
+ return super.onConnect(session, controller);
+ }
+ return null;
+ }
+ }).build();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ mSession.close();
+ }
+
+ @Test
+ public void testBuilder() {
+ prepareLooper();
+ MediaSession2.Builder builder = new MediaSession2.Builder(mContext);
+ try {
+ builder.setPlayer(null);
+ fail("null player shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ try {
+ builder.setId(null);
+ fail("null id shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ }
+
+ @Test
+ public void testPlayerStateChange() throws Exception {
+ prepareLooper();
+ final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaSession2 session,
+ MediaPlayerBase player, int state) {
+ assertEquals(targetState, state);
+ latchForSessionCallback.countDown();
+ }
+ }).build();
+ }
+ });
+
+ final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaController2 controllerOut, int state) {
+ assertEquals(targetState, state);
+ latchForControllerCallback.countDown();
+ }
+ });
+
+ mPlayer.notifyPlaybackState(targetState);
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(targetState, controller.getPlayerState());
+ }
+
+ @Test
+ public void testBufferingStateChange() throws Exception {
+ prepareLooper();
+ final List<MediaItem2> playlist = TestUtils.createPlaylist(5);
+
+ final MediaItem2 targetItem = playlist.get(3);
+ final int targetBufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mMockAgent.setPlaylist(playlist, null);
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onBufferingStateChanged(MediaSession2 session,
+ MediaPlayerBase player, MediaItem2 item, int state) {
+ assertEquals(targetItem, item);
+ assertEquals(targetBufferingState, state);
+ latchForSessionCallback.countDown();
+ }
+ }).build();
+ }
+ });
+
+ final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller,
+ MediaItem2 item, int state) {
+ assertEquals(targetItem, item);
+ assertEquals(targetBufferingState, state);
+ latchForControllerCallback.countDown();
+ }
+ });
+
+ mPlayer.notifyBufferingState(targetItem, targetBufferingState);
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(targetBufferingState, controller.getBufferingState());
+ }
+
+ @Test
+ public void testCurrentDataSourceChanged() throws Exception {
+ prepareLooper();
+ final int listSize = 5;
+ final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+ mMockAgent.setPlaylist(list, null);
+
+ final MediaItem2 currentItem = list.get(3);
+ mMockAgent.mCurrentMediaItem = currentItem;
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testCurrentDataSourceChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onCurrentMediaItemChanged(MediaSession2 session,
+ MediaPlayerBase player, MediaItem2 itemOut) {
+ assertSame(currentItem, itemOut);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onCurrentMediaItemChanged(MediaController2 controller,
+ MediaItem2 item) {
+ assertEquals(currentItem, item);
+ latchForControllerCallback.countDown();
+ }
+ });
+
+ mPlayer.notifyCurrentDataSourceChanged(currentItem.getDataSourceDesc());
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(currentItem, controller.getCurrentMediaItem());
+ }
+ }
+
+ @Test
+ public void testMediaPrepared() throws Exception {
+ prepareLooper();
+ final int listSize = 5;
+ final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+ mMockAgent.setPlaylist(list, null);
+
+ final MediaItem2 currentItem = list.get(3);
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testMediaPrepared")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onMediaPrepared(MediaSession2 session, MediaPlayerBase player,
+ MediaItem2 itemOut) {
+ assertSame(currentItem, itemOut);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ mPlayer.notifyMediaPrepared(currentItem.getDataSourceDesc());
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ // TODO(jaewan): Test that controllers are also notified. (b/74505936)
+ }
+ }
+
+ @Test
+ public void testBufferingStateChanged() throws Exception {
+ prepareLooper();
+ final int listSize = 5;
+ final List<MediaItem2> list = TestUtils.createPlaylist(listSize);
+ mMockAgent.setPlaylist(list, null);
+
+ final MediaItem2 currentItem = list.get(3);
+ final int buffState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_COMPLETE;
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testBufferingStateChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onBufferingStateChanged(MediaSession2 session,
+ MediaPlayerBase player, MediaItem2 itemOut, int stateOut) {
+ assertSame(currentItem, itemOut);
+ assertEquals(buffState, stateOut);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ mPlayer.notifyBufferingStateChanged(currentItem.getDataSourceDesc(), buffState);
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ // TODO(jaewan): Test that controllers are also notified. (b/74505936)
+ }
+ }
+
+ /**
+ * This also tests {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}
+ * and {@link MediaController2#getPlaybackSpeed()}.
+ */
+ @Test
+ public void testPlaybackSpeedChanged() throws Exception {
+ prepareLooper();
+ final float speed = 1.5f;
+ mPlayer.setPlaybackSpeed(speed);
+
+ final CountDownLatch latchForSessionCallback = new CountDownLatch(1);
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testPlaybackSpeedChanged")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onPlaybackSpeedChanged(MediaSession2 session,
+ MediaPlayerBase player, float speedOut) {
+ assertEquals(speed, speedOut, 0.0f);
+ latchForSessionCallback.countDown();
+ }
+ }).build()) {
+
+ final CountDownLatch latchForControllerCallback = new CountDownLatch(1);
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, new ControllerCallback() {
+ @Override
+ public void onPlaybackSpeedChanged(MediaController2 controller,
+ float speedOut) {
+ assertEquals(speed, speedOut, 0.0f);
+ latchForControllerCallback.countDown();
+ }
+ });
+
+ mPlayer.notifyPlaybackSpeedChanged(speed);
+ assertTrue(latchForSessionCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(latchForControllerCallback.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(speed, controller.getPlaybackSpeed(), 0.0f);
+ }
+ }
+
+ @Test
+ public void testUpdatePlayer() throws Exception {
+ prepareLooper();
+ final int targetState = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ final CountDownLatch latch = new CountDownLatch(1);
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaSession2 session,
+ MediaPlayerBase player, int state) {
+ assertEquals(targetState, state);
+ latch.countDown();
+ }
+ }).build();
+ }
+ });
+
+ MockPlayer player = new MockPlayer(0);
+
+ // Test if setPlayer doesn't crash with various situations.
+ mSession.updatePlayer(mPlayer, null, null);
+ assertEquals(mPlayer, mSession.getPlayer());
+ MediaPlaylistAgent agent = mSession.getPlaylistAgent();
+ assertNotNull(agent);
+
+ mSession.updatePlayer(player, null, null);
+ assertEquals(player, mSession.getPlayer());
+ assertNotNull(mSession.getPlaylistAgent());
+ assertNotEquals(agent, mSession.getPlaylistAgent());
+
+ player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PLAYING);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSetPlayer_playbackInfo() throws Exception {
+ prepareLooper();
+ MockPlayer player = new MockPlayer(0);
+ final AudioAttributesCompat attrs = new AudioAttributesCompat.Builder()
+ .setContentType(CONTENT_TYPE_MUSIC)
+ .build();
+ player.setAudioAttributes(attrs);
+
+ final int maxVolume = 100;
+ final int currentVolume = 23;
+ final int volumeControlType = VOLUME_CONTROL_ABSOLUTE;
+ VolumeProviderCompat volumeProvider = new VolumeProviderCompat(
+ volumeControlType, maxVolume, currentVolume) { };
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller, PlaybackInfo info) {
+ Assert.assertEquals(PlaybackInfo.PLAYBACK_TYPE_REMOTE, info.getPlaybackType());
+ assertEquals(attrs, info.getAudioAttributes());
+ assertEquals(volumeControlType, info.getPlaybackType());
+ assertEquals(maxVolume, info.getMaxVolume());
+ assertEquals(currentVolume, info.getCurrentVolume());
+ latch.countDown();
+ }
+ };
+
+ mSession.updatePlayer(player, null, null);
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ PlaybackInfo info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(PlaybackInfo.PLAYBACK_TYPE_LOCAL, info.getPlaybackType());
+ assertEquals(attrs, info.getAudioAttributes());
+ AudioManager manager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+ int localVolumeControlType = VOLUME_CONTROL_ABSOLUTE;
+ if (Build.VERSION.SDK_INT >= 21 && manager.isVolumeFixed()) {
+ localVolumeControlType = VOLUME_CONTROL_FIXED;
+ }
+ assertEquals(localVolumeControlType, info.getControlType());
+ assertEquals(manager.getStreamMaxVolume(AudioManager.STREAM_MUSIC), info.getMaxVolume());
+ assertEquals(manager.getStreamVolume(AudioManager.STREAM_MUSIC), info.getCurrentVolume());
+
+ mSession.updatePlayer(player, null, volumeProvider);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+
+ info = controller.getPlaybackInfo();
+ assertNotNull(info);
+ assertEquals(PlaybackInfo.PLAYBACK_TYPE_REMOTE, info.getPlaybackType());
+ assertEquals(attrs, info.getAudioAttributes());
+ assertEquals(volumeControlType, info.getControlType());
+ assertEquals(maxVolume, info.getMaxVolume());
+ assertEquals(currentVolume, info.getCurrentVolume());
+ }
+
+ @Test
+ public void testPlay() throws Exception {
+ prepareLooper();
+ mSession.play();
+ assertTrue(mPlayer.mPlayCalled);
+ }
+
+ @Test
+ public void testPause() throws Exception {
+ prepareLooper();
+ mSession.pause();
+ assertTrue(mPlayer.mPauseCalled);
+ }
+
+ @Test
+ public void testReset() throws Exception {
+ prepareLooper();
+ mSession.reset();
+ assertTrue(mPlayer.mResetCalled);
+ }
+
+ @Test
+ public void testPrepare() throws Exception {
+ prepareLooper();
+ mSession.prepare();
+ assertTrue(mPlayer.mPrepareCalled);
+ }
+
+ @Test
+ public void testSeekTo() throws Exception {
+ prepareLooper();
+ final long pos = 1004L;
+ mSession.seekTo(pos);
+ assertTrue(mPlayer.mSeekToCalled);
+ assertEquals(pos, mPlayer.mSeekPosition);
+ }
+
+ @Test
+ public void testSetPlaybackSpeed() throws Exception {
+ prepareLooper();
+ final float speed = 1.5f;
+ mSession.setPlaybackSpeed(speed);
+ assertTrue(mPlayer.mSetPlaybackSpeedCalled);
+ assertEquals(speed, mPlayer.mPlaybackSpeed, 0.0f);
+ }
+
+ @Test
+ public void testGetPlaybackSpeed() throws Exception {
+ prepareLooper();
+ final float speed = 1.5f;
+ mPlayer.setPlaybackSpeed(speed);
+ assertEquals(speed, mSession.getPlaybackSpeed(), 0.0f);
+ }
+
+ @Test
+ public void testGetCurrentMediaItem() {
+ prepareLooper();
+ MediaItem2 item = TestUtils.createMediaItemWithMetadata();
+ mMockAgent.mCurrentMediaItem = item;
+ assertEquals(item, mSession.getCurrentMediaItem());
+ }
+
+ @Test
+ public void testSkipToPreviousItem() {
+ prepareLooper();
+ mSession.skipToPreviousItem();
+ assertTrue(mMockAgent.mSkipToPreviousItemCalled);
+ }
+
+ @Test
+ public void testSkipToNextItem() throws Exception {
+ prepareLooper();
+ mSession.skipToNextItem();
+ assertTrue(mMockAgent.mSkipToNextItemCalled);
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws Exception {
+ prepareLooper();
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.skipToPlaylistItem(testMediaItem);
+ assertTrue(mMockAgent.mSkipToPlaylistItemCalled);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testGetPlayerState() {
+ prepareLooper();
+ final int state = MediaPlayerBase.PLAYER_STATE_PLAYING;
+ mPlayer.mLastPlayerState = state;
+ assertEquals(state, mSession.getPlayerState());
+ }
+
+ @Test
+ public void testGetBufferingState() {
+ prepareLooper();
+ final int bufferingState = MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_PLAYABLE;
+ mPlayer.mLastBufferingState = bufferingState;
+ assertEquals(bufferingState, mSession.getBufferingState());
+ }
+
+ @Test
+ public void testGetPosition() {
+ prepareLooper();
+ final long position = 150000;
+ mPlayer.mCurrentPosition = position;
+ assertEquals(position, mSession.getCurrentPosition());
+ }
+
+ @Test
+ public void testGetBufferedPosition() {
+ prepareLooper();
+ final long bufferedPosition = 900000;
+ mPlayer.mBufferedPosition = bufferedPosition;
+ assertEquals(bufferedPosition, mSession.getBufferedPosition());
+ }
+
+ @Test
+ public void testSetPlaylist() {
+ prepareLooper();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mSession.setPlaylist(list, null);
+ assertTrue(mMockAgent.mSetPlaylistCalled);
+ assertSame(list, mMockAgent.mPlaylist);
+ assertNull(mMockAgent.mMetadata);
+ }
+
+ @Test
+ public void testGetPlaylist() {
+ prepareLooper();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ mMockAgent.mPlaylist = list;
+ assertEquals(list, mSession.getPlaylist());
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() {
+ prepareLooper();
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mSession.updatePlaylistMetadata(testMetadata);
+ assertTrue(mMockAgent.mUpdatePlaylistMetadataCalled);
+ assertSame(testMetadata, mMockAgent.mMetadata);
+ }
+
+ @Test
+ public void testGetPlaylistMetadata() {
+ prepareLooper();
+ final MediaMetadata2 testMetadata = TestUtils.createMetadata();
+ mMockAgent.mMetadata = testMetadata;
+ assertEquals(testMetadata, mSession.getPlaylistMetadata());
+ }
+
+ @Test
+ public void testSessionCallback_onPlaylistChanged() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ final CountDownLatch latch = new CountDownLatch(1);
+ mMockAgent.setPlaylist(list, null);
+
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onPlaylistChanged(MediaSession2 session, MediaPlaylistAgent playlistAgent,
+ List<MediaItem2> playlist, MediaMetadata2 metadata) {
+ assertEquals(mMockAgent, playlistAgent);
+ assertEquals(list, playlist);
+ assertNull(metadata);
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testSessionCallback")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ mMockAgent.notifyPlaylistChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testAddPlaylistItem() {
+ prepareLooper();
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.addPlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mAddPlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testRemovePlaylistItem() {
+ prepareLooper();
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.removePlaylistItem(testMediaItem);
+ assertTrue(mMockAgent.mRemovePlaylistItemCalled);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final int testIndex = 12;
+ final MediaItem2 testMediaItem = TestUtils.createMediaItemWithMetadata();
+ mSession.replacePlaylistItem(testIndex, testMediaItem);
+ assertTrue(mMockAgent.mReplacePlaylistItemCalled);
+ assertEquals(testIndex, mMockAgent.mIndex);
+ assertSame(testMediaItem, mMockAgent.mItem);
+ }
+
+ /**
+ * This also tests {@link SessionCallback#onShuffleModeChanged}
+ */
+ @Test
+ public void testGetShuffleMode() throws InterruptedException {
+ prepareLooper();
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ mMockAgent.setShuffleMode(testShuffleMode);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onShuffleModeChanged(MediaSession2 session,
+ MediaPlaylistAgent playlistAgent, int shuffleMode) {
+ assertEquals(mMockAgent, playlistAgent);
+ assertEquals(testShuffleMode, shuffleMode);
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testGetShuffleMode")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ mMockAgent.notifyShuffleModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetShuffleMode() {
+ prepareLooper();
+ final int testShuffleMode = MediaPlaylistAgent.SHUFFLE_MODE_GROUP;
+ mSession.setShuffleMode(testShuffleMode);
+ assertTrue(mMockAgent.mSetShuffleModeCalled);
+ assertEquals(testShuffleMode, mMockAgent.mShuffleMode);
+ }
+
+ /**
+ * This also tests {@link SessionCallback#onShuffleModeChanged}
+ */
+ @Test
+ public void testGetRepeatMode() throws InterruptedException {
+ prepareLooper();
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ mMockAgent.setRepeatMode(testRepeatMode);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public void onRepeatModeChanged(MediaSession2 session, MediaPlaylistAgent playlistAgent,
+ int repeatMode) {
+ assertEquals(mMockAgent, playlistAgent);
+ assertEquals(testRepeatMode, repeatMode);
+ latch.countDown();
+ }
+ };
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setPlaylistAgent(mMockAgent)
+ .setId("testGetRepeatMode")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ mMockAgent.notifyRepeatModeChanged();
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetRepeatMode() {
+ prepareLooper();
+ final int testRepeatMode = MediaPlaylistAgent.REPEAT_MODE_GROUP;
+ mSession.setRepeatMode(testRepeatMode);
+ assertTrue(mMockAgent.mSetRepeatModeCalled);
+ assertEquals(testRepeatMode, mMockAgent.mRepeatMode);
+ }
+
+ // TODO(jaewan): Revisit
+ @Ignore
+ @Test
+ public void testBadPlayer() throws InterruptedException {
+ prepareLooper();
+ // TODO(jaewan): Add equivalent tests again
+ final CountDownLatch latch = new CountDownLatch(4); // expected call + 1
+ final BadPlayer player = new BadPlayer(0);
+
+ mSession.updatePlayer(player, null, null);
+ mSession.updatePlayer(mPlayer, null, null);
+ player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_PAUSED);
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ // This bad player will keep push events to the listener that is previously
+ // registered by session.setPlayer().
+ private static class BadPlayer extends MockPlayer {
+ BadPlayer(int count) {
+ super(count);
+ }
+
+ @Override
+ public void unregisterPlayerEventCallback(
+ @NonNull MediaPlayerBase.PlayerEventCallback listener) {
+ // No-op.
+ }
+ }
+
+ @Test
+ public void testOnCommandCallback() throws InterruptedException {
+ prepareLooper();
+ final MockOnCommandCallback callback = new MockOnCommandCallback();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).build();
+ }
+ });
+ MediaController2 controller = createController(mSession.getToken());
+ controller.pause();
+ assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(1, callback.commands.size());
+ assertEquals(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE,
+ (long) callback.commands.get(0).getCommandCode());
+
+ controller.play();
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mPlayCalled);
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(2, callback.commands.size());
+ assertEquals(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY,
+ (long) callback.commands.get(1).getCommandCode());
+ }
+
+ @Test
+ public void testOnConnectCallback() throws InterruptedException {
+ prepareLooper();
+ final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ }
+ });
+ MediaController2 controller = createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testOnDisconnectCallback() throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testOnDisconnectCallback")
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public void onDisconnected(MediaSession2 session,
+ ControllerInfo controller) {
+ assertEquals(Process.myUid(), controller.getUid());
+ latch.countDown();
+ }
+ }).build()) {
+ MediaController2 controller = createController(session.getToken());
+ controller.close();
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetCustomLayout() throws InterruptedException {
+ prepareLooper();
+ final List<CommandButton> buttons = new ArrayList<>();
+ buttons.add(new CommandButton.Builder()
+ .setCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY))
+ .setDisplayName("button").build());
+ final CountDownLatch latch = new CountDownLatch(1);
+ final SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (mContext.getPackageName().equals(controller.getPackageName())) {
+ mSession.setCustomLayout(controller, buttons);
+ }
+ return super.onConnect(session, controller);
+ }
+ };
+
+ try (MediaSession2 session = new MediaSession2.Builder(mContext)
+ .setPlayer(mPlayer)
+ .setId("testSetCustomLayout")
+ .setSessionCallback(sHandlerExecutor, sessionCallback)
+ .build()) {
+ if (mSession != null) {
+ mSession.close();
+ mSession = session;
+ }
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller2,
+ List<CommandButton> layout) {
+ assertEquals(layout.size(), buttons.size());
+ for (int i = 0; i < layout.size(); i++) {
+ assertEquals(layout.get(i).getCommand(), buttons.get(i).getCommand());
+ assertEquals(layout.get(i).getDisplayName(),
+ buttons.get(i).getDisplayName());
+ }
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller =
+ createController(session.getToken(), true, callback);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testSetAllowedCommands() throws InterruptedException {
+ prepareLooper();
+ final SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY));
+ commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE));
+ commands.addCommand(new SessionCommand2(SessionCommand2.COMMAND_CODE_PLAYBACK_RESET));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commandsOut) {
+ assertNotNull(commandsOut);
+ Set<SessionCommand2> expected = commands.getCommands();
+ Set<SessionCommand2> actual = commandsOut.getCommands();
+
+ assertNotNull(actual);
+ assertEquals(expected.size(), actual.size());
+ for (SessionCommand2 command : expected) {
+ assertTrue(actual.contains(command));
+ }
+ latch.countDown();
+ }
+ };
+
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ ControllerInfo controllerInfo = getTestControllerInfo();
+ assertNotNull(controllerInfo);
+
+ mSession.setAllowedCommands(controllerInfo, commands);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testSendCustomCommand() throws InterruptedException {
+ prepareLooper();
+ final SessionCommand2 testCommand = new SessionCommand2(
+ SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE);
+ final Bundle testArgs = new Bundle();
+ testArgs.putString("args", "testSendCustomAction");
+
+ final CountDownLatch latch = new CountDownLatch(2);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ assertEquals(testCommand, command);
+ assertTrue(TestUtils.equals(testArgs, args));
+ assertNull(receiver);
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller =
+ createController(mSession.getToken(), true, callback);
+ // TODO(jaewan): Test with multiple controllers
+ mSession.sendCustomCommand(testCommand, testArgs);
+
+ ControllerInfo controllerInfo = getTestControllerInfo();
+ assertNotNull(controllerInfo);
+ // TODO(jaewan): Test receivers as well.
+ mSession.sendCustomCommand(controllerInfo, testCommand, testArgs, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testNotifyError() throws InterruptedException {
+ prepareLooper();
+ final int errorCode = MediaSession2.ERROR_CODE_NOT_AVAILABLE_IN_REGION;
+ final Bundle extras = new Bundle();
+ extras.putString("args", "testNotifyError");
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onError(MediaController2 controller, int errorCodeOut, Bundle extrasOut) {
+ assertEquals(errorCode, errorCodeOut);
+ assertTrue(TestUtils.equals(extras, extrasOut));
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ // TODO(jaewan): Test with multiple controllers
+ mSession.notifyError(errorCode, extras);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testNotifyRoutesInfoChanged() throws InterruptedException {
+ prepareLooper();
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ControllerCallback callback = new ControllerCallback() {
+ @Override
+ public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+ @Nullable List<Bundle> routes) {
+ assertNull(routes);
+ latch.countDown();
+ }
+ };
+ final MediaController2 controller = createController(mSession.getToken(), true, callback);
+ ControllerInfo controllerInfo = getTestControllerInfo();
+ mSession.notifyRoutesInfoChanged(controllerInfo, null);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private ControllerInfo getTestControllerInfo() {
+ List<ControllerInfo> controllers = mSession.getConnectedControllers();
+ assertNotNull(controllers);
+ for (int i = 0; i < controllers.size(); i++) {
+ if (Process.myUid() == controllers.get(i).getUid()) {
+ return controllers.get(i);
+ }
+ }
+ fail("Failed to get test controller info");
+ return null;
+ }
+
+ public class MockOnConnectCallback extends SessionCallback {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controllerInfo) {
+ if (Process.myUid() != controllerInfo.getUid()) {
+ return null;
+ }
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ // Reject all
+ return null;
+ }
+ }
+
+ public class MockOnCommandCallback extends SessionCallback {
+ public final ArrayList<SessionCommand2> commands = new ArrayList<>();
+
+ @Override
+ public boolean onCommandRequest(MediaSession2 session, ControllerInfo controllerInfo,
+ SessionCommand2 command) {
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ commands.add(command);
+ if (command.getCommandCode() == SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private static void assertMediaItemListEquals(List<MediaItem2> a, List<MediaItem2> b) {
+ if (a == null || b == null) {
+ assertEquals(a, b);
+ }
+ assertEquals(a.size(), b.size());
+
+ for (int i = 0; i < a.size(); i++) {
+ MediaItem2 aItem = a.get(i);
+ MediaItem2 bItem = b.get(i);
+
+ if (aItem == null || bItem == null) {
+ assertEquals(aItem, bItem);
+ continue;
+ }
+
+ assertEquals(aItem.getMediaId(), bItem.getMediaId());
+ assertEquals(aItem.getFlags(), bItem.getFlags());
+ TestUtils.equals(aItem.getMetadata().toBundle(), bItem.getMetadata().toBundle());
+
+ // Note: Here it does not check whether DataSourceDesc are equal,
+ // since there DataSourceDec is not comparable.
+ }
+ }
+}
diff --git a/androidx/media/MediaSession2TestBase.java b/androidx/media/MediaSession2TestBase.java
new file mode 100644
index 00000000..745ef3a4
--- /dev/null
+++ b/androidx/media/MediaSession2TestBase.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.InstrumentationRegistry;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.media.MediaController2.ControllerCallback;
+import androidx.media.MediaSession2.CommandButton;
+import androidx.media.TestUtils.SyncHandler;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Base class for session test.
+ * <p>
+ * For all subclasses, all individual tests should begin with the {@link #prepareLooper()}. See
+ * {@link #prepareLooper} for details.
+ */
+abstract class MediaSession2TestBase {
+ // Expected success
+ static final int WAIT_TIME_MS = 1000;
+
+ // Expected timeout
+ static final int TIMEOUT_MS = 500;
+
+ static SyncHandler sHandler;
+ static Executor sHandlerExecutor;
+
+ Context mContext;
+ private List<MediaController2> mControllers = new ArrayList<>();
+
+ interface TestControllerInterface {
+ ControllerCallback getCallback();
+ }
+
+ interface TestControllerCallbackInterface {
+ void waitForConnect(boolean expect) throws InterruptedException;
+ void waitForDisconnect(boolean expect) throws InterruptedException;
+ void setRunnableForOnCustomCommand(Runnable runnable);
+ }
+
+ /**
+ * All tests methods should start with this.
+ * <p>
+ * MediaControllerCompat, which is wrapped by the MediaSession2, can be only created by the
+ * thread whose Looper is prepared. However, when the presubmit tests runs on the server,
+ * test runs with the {@link org.junit.internal.runners.statements.FailOnTimeout} which creates
+ * dedicated thread for running test methods while methods annotated with @After or @Before
+ * runs on the different thread. This ensures that the current Looper is prepared.
+ * <p>
+ * To address the issue .
+ */
+ public static void prepareLooper() {
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ }
+
+ @BeforeClass
+ public static void setUpThread() {
+ synchronized (MediaSession2TestBase.class) {
+ if (sHandler != null) {
+ return;
+ }
+ prepareLooper();
+ HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+ handlerThread.start();
+ sHandler = new SyncHandler(handlerThread.getLooper());
+ sHandlerExecutor = new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ SyncHandler handler;
+ synchronized (MediaSession2TestBase.class) {
+ handler = sHandler;
+ }
+ if (handler != null) {
+ handler.post(runnable);
+ }
+ }
+ };
+ }
+ }
+
+ @AfterClass
+ public static void cleanUpThread() {
+ synchronized (MediaSession2TestBase.class) {
+ if (sHandler == null) {
+ return;
+ }
+ sHandler.getLooper().quitSafely();
+ sHandler = null;
+ sHandlerExecutor = null;
+ }
+ }
+
+ @CallSuper
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @CallSuper
+ public void cleanUp() throws Exception {
+ for (int i = 0; i < mControllers.size(); i++) {
+ mControllers.get(i).close();
+ }
+ }
+
+ final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+ return createController(token, true, null);
+ }
+
+ final MediaController2 createController(@NonNull SessionToken2 token,
+ boolean waitForConnect, @Nullable ControllerCallback callback)
+ throws InterruptedException {
+ TestControllerInterface instance = onCreateController(token, callback);
+ if (!(instance instanceof MediaController2)) {
+ throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+ + instance);
+ }
+ MediaController2 controller = (MediaController2) instance;
+ mControllers.add(controller);
+ if (waitForConnect) {
+ waitForConnect(controller, true);
+ }
+ return controller;
+ }
+
+ private static TestControllerCallbackInterface getTestControllerCallbackInterface(
+ MediaController2 controller) {
+ if (!(controller instanceof TestControllerInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller implemented"
+ + " TestControllerInterface but got " + controller);
+ }
+ ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+ if (!(callback instanceof TestControllerCallbackInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller with callback "
+ + " implemented TestControllerCallbackInterface but got " + controller);
+ }
+ return (TestControllerCallbackInterface) callback;
+ }
+
+ public static void waitForConnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getTestControllerCallbackInterface(controller).waitForConnect(expected);
+ }
+
+ public static void waitForDisconnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getTestControllerCallbackInterface(controller).waitForDisconnect(expected);
+ }
+
+ public static void setRunnableForOnCustomCommand(MediaController2 controller,
+ Runnable runnable) {
+ getTestControllerCallbackInterface(controller).setRunnableForOnCustomCommand(runnable);
+ }
+
+ TestControllerInterface onCreateController(final @NonNull SessionToken2 token,
+ @Nullable ControllerCallback callback) throws InterruptedException {
+ final ControllerCallback controllerCallback =
+ callback != null ? callback : new ControllerCallback() {};
+ final AtomicReference<TestControllerInterface> controller = new AtomicReference<>();
+ sHandler.postAndSync(new Runnable() {
+ @Override
+ public void run() {
+ // Create controller on the test handler, for changing MediaBrowserCompat's Handler
+ // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler
+ // and commands wouldn't be run if tests codes waits on the test handler.
+ controller.set(new TestMediaController(
+ mContext, token, new TestControllerCallback(controllerCallback)));
+ }
+ });
+ return controller.get();
+ }
+
+ // TODO(jaewan): (Can be Post-P): Deprecate this
+ public static class TestControllerCallback extends MediaController2.ControllerCallback
+ implements TestControllerCallbackInterface {
+ public final ControllerCallback mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+ @GuardedBy("this")
+ private Runnable mOnCustomCommandRunnable;
+
+ TestControllerCallback(@NonNull ControllerCallback callbackProxy) {
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("Callback proxy shouldn't be null. Test bug");
+ }
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(MediaController2 controller, SessionCommandGroup2 commands) {
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected(MediaController2 controller) {
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void onCustomCommand(MediaController2 controller, SessionCommand2 command,
+ Bundle args, ResultReceiver receiver) {
+ mCallbackProxy.onCustomCommand(controller, command, args, receiver);
+ synchronized (this) {
+ if (mOnCustomCommandRunnable != null) {
+ mOnCustomCommandRunnable.run();
+ }
+ }
+ }
+
+ @Override
+ public void onPlaybackInfoChanged(MediaController2 controller,
+ MediaController2.PlaybackInfo info) {
+ mCallbackProxy.onPlaybackInfoChanged(controller, info);
+ }
+
+ @Override
+ public void onCustomLayoutChanged(MediaController2 controller, List<CommandButton> layout) {
+ mCallbackProxy.onCustomLayoutChanged(controller, layout);
+ }
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCallbackProxy.onAllowedCommandsChanged(controller, commands);
+ }
+
+ @Override
+ public void onPlayerStateChanged(MediaController2 controller, int state) {
+ mCallbackProxy.onPlayerStateChanged(controller, state);
+ }
+
+ @Override
+ public void onSeekCompleted(MediaController2 controller, long position) {
+ mCallbackProxy.onSeekCompleted(controller, position);
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(MediaController2 controller, float speed) {
+ mCallbackProxy.onPlaybackSpeedChanged(controller, speed);
+ }
+
+ @Override
+ public void onBufferingStateChanged(MediaController2 controller, MediaItem2 item,
+ int state) {
+ mCallbackProxy.onBufferingStateChanged(controller, item, state);
+ }
+
+ @Override
+ public void onError(MediaController2 controller, int errorCode, Bundle extras) {
+ mCallbackProxy.onError(controller, errorCode, extras);
+ }
+
+ @Override
+ public void onCurrentMediaItemChanged(MediaController2 controller, MediaItem2 item) {
+ mCallbackProxy.onCurrentMediaItemChanged(controller, item);
+ }
+
+ @Override
+ public void onPlaylistChanged(MediaController2 controller,
+ List<MediaItem2> list, MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistChanged(controller, list, metadata);
+ }
+
+ @Override
+ public void onPlaylistMetadataChanged(MediaController2 controller,
+ MediaMetadata2 metadata) {
+ mCallbackProxy.onPlaylistMetadataChanged(controller, metadata);
+ }
+
+ @Override
+ public void onShuffleModeChanged(MediaController2 controller, int shuffleMode) {
+ mCallbackProxy.onShuffleModeChanged(controller, shuffleMode);
+ }
+
+ @Override
+ public void onRepeatModeChanged(MediaController2 controller, int repeatMode) {
+ mCallbackProxy.onRepeatModeChanged(controller, repeatMode);
+ }
+
+ @Override
+ public void setRunnableForOnCustomCommand(Runnable runnable) {
+ synchronized (this) {
+ mOnCustomCommandRunnable = runnable;
+ }
+ }
+
+ @Override
+ public void onRoutesInfoChanged(@NonNull MediaController2 controller,
+ @Nullable List<Bundle> routes) {
+ mCallbackProxy.onRoutesInfoChanged(controller, routes);
+ }
+ }
+
+ public class TestMediaController extends MediaController2 implements TestControllerInterface {
+ private final ControllerCallback mCallback;
+
+ TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, sHandlerExecutor, callback);
+ mCallback = callback;
+ }
+
+ @Override
+ public ControllerCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/androidx/media/MediaSession2_PermissionTest.java b/androidx/media/MediaSession2_PermissionTest.java
new file mode 100644
index 00000000..3895ea5d
--- /dev/null
+++ b/androidx/media/MediaSession2_PermissionTest.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND;
+import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME;
+import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.annotation.NonNull;
+import androidx.media.MediaSession2.ControllerInfo;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests whether {@link MediaSession2} receives commands that hasn't allowed.
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MediaSession2_PermissionTest extends MediaSession2TestBase {
+ private static final String SESSION_ID = "MediaSession2Test_permission";
+
+ private MockPlayer mPlayer;
+ private MediaSession2 mSession;
+ private MySessionCallback mCallback;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ if (mSession != null) {
+ mSession.close();
+ mSession = null;
+ }
+ mPlayer = null;
+ mCallback = null;
+ }
+
+ private MediaSession2 createSessionWithAllowedActions(final SessionCommandGroup2 commands) {
+ mPlayer = new MockPlayer(0);
+ mCallback = new MySessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(MediaSession2 session,
+ ControllerInfo controller) {
+ if (Process.myUid() != controller.getUid()) {
+ return null;
+ }
+ return commands == null ? new SessionCommandGroup2() : commands;
+ }
+ };
+ if (mSession != null) {
+ mSession.close();
+ }
+ mSession = new MediaSession2.Builder(mContext).setPlayer(mPlayer).setId(SESSION_ID)
+ .setSessionCallback(sHandlerExecutor, mCallback).build();
+ return mSession;
+ }
+
+ private SessionCommandGroup2 createCommandGroupWith(int commandCode) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addCommand(new SessionCommand2(commandCode));
+ return commands;
+ }
+
+ private SessionCommandGroup2 createCommandGroupWithout(int commandCode) {
+ SessionCommandGroup2 commands = new SessionCommandGroup2();
+ commands.addAllPredefinedCommands();
+ commands.removeCommand(new SessionCommand2(commandCode));
+ return commands;
+ }
+
+ private void testOnCommandRequest(int commandCode, PermissionTestRunnable runnable)
+ throws InterruptedException {
+ createSessionWithAllowedActions(createCommandGroupWith(commandCode));
+ runnable.run(createController(mSession.getToken()));
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnCommandRequestCalled);
+ assertEquals(commandCode, mCallback.mCommand.getCommandCode());
+
+ createSessionWithAllowedActions(createCommandGroupWithout(commandCode));
+ runnable.run(createController(mSession.getToken()));
+
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnCommandRequestCalled);
+ }
+
+ @Test
+ public void testPlay() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_PLAY, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.play();
+ }
+ });
+ }
+
+ @Test
+ public void testPause() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_PAUSE, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.pause();
+ }
+ });
+ }
+
+ @Test
+ public void testReset() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_RESET, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.reset();
+ }
+ });
+ }
+
+ @Test
+ public void testSeekTo() throws InterruptedException {
+ prepareLooper();
+ final long position = 10;
+ testOnCommandRequest(COMMAND_CODE_PLAYBACK_SEEK_TO, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.seekTo(position);
+ }
+ });
+ }
+
+ @Test
+ public void testSkipToNext() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.skipToNextItem();
+ }
+ });
+ }
+
+ @Test
+ public void testSkipToPrevious() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.skipToPreviousItem();
+ }
+ });
+ }
+
+ @Test
+ public void testSkipToPlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(
+ COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM,
+ new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.skipToPlaylistItem(testItem);
+ }
+ });
+ }
+
+ @Test
+ public void testSetPlaylist() throws InterruptedException {
+ prepareLooper();
+ final List<MediaItem2> list = TestUtils.createPlaylist(2);
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SET_LIST, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.setPlaylist(list, null);
+ }
+ });
+ }
+
+ @Test
+ public void testUpdatePlaylistMetadata() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.updatePlaylistMetadata(null);
+ }
+ });
+ }
+
+ @Test
+ public void testAddPlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_ADD_ITEM, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.addPlaylistItem(0, testItem);
+ }
+ });
+ }
+
+ @Test
+ public void testRemovePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.removePlaylistItem(testItem);
+ }
+ });
+ }
+
+ @Test
+ public void testReplacePlaylistItem() throws InterruptedException {
+ prepareLooper();
+ final MediaItem2 testItem = TestUtils.createMediaItemWithMetadata();
+ testOnCommandRequest(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.replacePlaylistItem(0, testItem);
+ }
+ });
+ }
+
+ @Test
+ public void testSetVolume() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_VOLUME_SET_VOLUME, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.setVolumeTo(0, 0);
+ }
+ });
+ }
+
+ @Test
+ public void testAdjustVolume() throws InterruptedException {
+ prepareLooper();
+ testOnCommandRequest(COMMAND_CODE_VOLUME_ADJUST_VOLUME, new PermissionTestRunnable() {
+ @Override
+ public void run(MediaController2 controller) {
+ controller.adjustVolume(0, 0);
+ }
+ });
+ }
+
+ @Test
+ public void testFastForward() throws InterruptedException {
+ prepareLooper();
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_FAST_FORWARD));
+ createController(mSession.getToken()).fastForward();
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnFastForwardCalled);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_FAST_FORWARD));
+ createController(mSession.getToken()).fastForward();
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnFastForwardCalled);
+ }
+
+ @Test
+ public void testRewind() throws InterruptedException {
+ prepareLooper();
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_REWIND));
+ createController(mSession.getToken()).rewind();
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnRewindCalled);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_REWIND));
+ createController(mSession.getToken()).rewind();
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnRewindCalled);
+ }
+
+ @Test
+ public void testPlayFromMediaId() throws InterruptedException {
+ prepareLooper();
+ final String mediaId = "testPlayFromMediaId";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID));
+ createController(mSession.getToken()).playFromMediaId(mediaId, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID));
+ createController(mSession.getToken()).playFromMediaId(mediaId, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPlayFromMediaIdCalled);
+ }
+
+ @Test
+ public void testPlayFromUri() throws InterruptedException {
+ prepareLooper();
+ final Uri uri = Uri.parse("play://from.uri");
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_URI));
+ createController(mSession.getToken()).playFromUri(uri, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPlayFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_URI));
+ createController(mSession.getToken()).playFromUri(uri, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPlayFromUriCalled);
+ }
+
+ @Test
+ public void testPlayFromSearch() throws InterruptedException {
+ prepareLooper();
+ final String query = "testPlayFromSearch";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH));
+ createController(mSession.getToken()).playFromSearch(query, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPlayFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH));
+ createController(mSession.getToken()).playFromSearch(query, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPlayFromSearchCalled);
+ }
+
+ @Test
+ public void testPrepareFromMediaId() throws InterruptedException {
+ prepareLooper();
+ final String mediaId = "testPrepareFromMediaId";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+ createController(mSession.getToken()).prepareFromMediaId(mediaId, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID));
+ createController(mSession.getToken()).prepareFromMediaId(mediaId, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPrepareFromMediaIdCalled);
+ }
+
+ @Test
+ public void testPrepareFromUri() throws InterruptedException {
+ prepareLooper();
+ final Uri uri = Uri.parse("prepare://from.uri");
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+ createController(mSession.getToken()).prepareFromUri(uri, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromUriCalled);
+ assertEquals(uri, mCallback.mUri);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_URI));
+ createController(mSession.getToken()).prepareFromUri(uri, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPrepareFromUriCalled);
+ }
+
+ @Test
+ public void testPrepareFromSearch() throws InterruptedException {
+ prepareLooper();
+ final String query = "testPrepareFromSearch";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+ createController(mSession.getToken()).prepareFromSearch(query, null);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertNull(mCallback.mExtras);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+ createController(mSession.getToken()).prepareFromSearch(query, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnPrepareFromSearchCalled);
+ }
+
+ @Test
+ public void testSetRating() throws InterruptedException {
+ prepareLooper();
+ final String mediaId = "testSetRating";
+ final Rating2 rating = Rating2.newStarRating(Rating2.RATING_5_STARS, 3.5f);
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_SET_RATING));
+ createController(mSession.getToken()).setRating(mediaId, rating);
+
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnSetRatingCalled);
+ assertEquals(mediaId, mCallback.mMediaId);
+ assertEquals(rating, mCallback.mRating);
+
+ createSessionWithAllowedActions(
+ createCommandGroupWithout(COMMAND_CODE_SESSION_SET_RATING));
+ createController(mSession.getToken()).setRating(mediaId, rating);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mCallback.mOnSetRatingCalled);
+ }
+
+ @Test
+ public void testChangingPermissionWithSetAllowedCommands() throws InterruptedException {
+ prepareLooper();
+ final String query = "testChangingPermissionWithSetAllowedCommands";
+ createSessionWithAllowedActions(
+ createCommandGroupWith(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+
+ ControllerCallbackForPermissionChange controllerCallback =
+ new ControllerCallbackForPermissionChange();
+ MediaController2 controller =
+ createController(mSession.getToken(), true, controllerCallback);
+
+ controller.prepareFromSearch(query, null);
+ assertTrue(mCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mCallback.mOnPrepareFromSearchCalled);
+ assertEquals(query, mCallback.mQuery);
+ assertNull(mCallback.mExtras);
+ mCallback.reset();
+
+ // Change allowed commands.
+ mSession.setAllowedCommands(getTestControllerInfo(),
+ createCommandGroupWithout(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH));
+ assertTrue(controllerCallback.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+
+ controller.prepareFromSearch(query, null);
+ assertFalse(mCallback.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ private ControllerInfo getTestControllerInfo() {
+ List<ControllerInfo> controllers = mSession.getConnectedControllers();
+ assertNotNull(controllers);
+ for (int i = 0; i < controllers.size(); i++) {
+ if (Process.myUid() == controllers.get(i).getUid()) {
+ return controllers.get(i);
+ }
+ }
+ fail("Failed to get test controller info");
+ return null;
+ }
+
+ @FunctionalInterface
+ private interface PermissionTestRunnable {
+ void run(@NonNull MediaController2 controller);
+ }
+
+ public class MySessionCallback extends MediaSession2.SessionCallback {
+ public CountDownLatch mCountDownLatch;
+
+ public SessionCommand2 mCommand;
+ public String mMediaId;
+ public String mQuery;
+ public Uri mUri;
+ public Bundle mExtras;
+ public Rating2 mRating;
+
+ public boolean mOnCommandRequestCalled;
+ public boolean mOnPlayFromMediaIdCalled;
+ public boolean mOnPlayFromSearchCalled;
+ public boolean mOnPlayFromUriCalled;
+ public boolean mOnPrepareFromMediaIdCalled;
+ public boolean mOnPrepareFromSearchCalled;
+ public boolean mOnPrepareFromUriCalled;
+ public boolean mOnFastForwardCalled;
+ public boolean mOnRewindCalled;
+ public boolean mOnSetRatingCalled;
+
+
+ public MySessionCallback() {
+ mCountDownLatch = new CountDownLatch(1);
+ }
+
+ public void reset() {
+ mCountDownLatch = new CountDownLatch(1);
+
+ mCommand = null;
+ mMediaId = null;
+ mQuery = null;
+ mUri = null;
+ mExtras = null;
+
+ mOnCommandRequestCalled = false;
+ mOnPlayFromMediaIdCalled = false;
+ mOnPlayFromSearchCalled = false;
+ mOnPlayFromUriCalled = false;
+ mOnPrepareFromMediaIdCalled = false;
+ mOnPrepareFromSearchCalled = false;
+ mOnPrepareFromUriCalled = false;
+ mOnFastForwardCalled = false;
+ mOnRewindCalled = false;
+ mOnSetRatingCalled = false;
+ }
+
+ @Override
+ public boolean onCommandRequest(MediaSession2 session, ControllerInfo controller,
+ SessionCommand2 command) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnCommandRequestCalled = true;
+ mCommand = command;
+ mCountDownLatch.countDown();
+ return super.onCommandRequest(session, controller, command);
+ }
+
+ @Override
+ public void onFastForward(MediaSession2 session, ControllerInfo controller) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnFastForwardCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onRewind(MediaSession2 session, ControllerInfo controller) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnRewindCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPlayFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPlayFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPlayFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPlayFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPlayFromUri(MediaSession2 session, ControllerInfo controller,
+ Uri uri, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPlayFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPrepareFromMediaId(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPrepareFromMediaIdCalled = true;
+ mMediaId = mediaId;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPrepareFromSearch(MediaSession2 session, ControllerInfo controller,
+ String query, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPrepareFromSearchCalled = true;
+ mQuery = query;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onPrepareFromUri(MediaSession2 session, ControllerInfo controller,
+ Uri uri, Bundle extras) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnPrepareFromUriCalled = true;
+ mUri = uri;
+ mExtras = extras;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void onSetRating(MediaSession2 session, ControllerInfo controller,
+ String mediaId, Rating2 rating) {
+ assertEquals(Process.myUid(), controller.getUid());
+ mOnSetRatingCalled = true;
+ mMediaId = mediaId;
+ mRating = rating;
+ mCountDownLatch.countDown();
+ }
+ }
+
+ public class ControllerCallbackForPermissionChange extends MediaController2.ControllerCallback {
+ public CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ @Override
+ public void onAllowedCommandsChanged(MediaController2 controller,
+ SessionCommandGroup2 commands) {
+ mCountDownLatch.countDown();
+ }
+ }
+}
diff --git a/androidx/media/MediaSessionManager_MediaSession2Test.java b/androidx/media/MediaSessionManager_MediaSession2Test.java
new file mode 100644
index 00000000..904b7687
--- /dev/null
+++ b/androidx/media/MediaSessionManager_MediaSession2Test.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+public class MediaSessionManager_MediaSession2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaSessionManager_MediaSession2Test";
+
+ private MediaSessionManager mManager;
+ private MediaSession2 mSession;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+ // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
+ // per test thread differs across the {@link MediaSession2} with the same TAG.
+ final MockPlayer player = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext)
+ .setPlayer(player)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() { })
+ .setId(TAG)
+ .build();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.removeCallbacksAndMessages(null);
+ mSession.close();
+ }
+
+ // TODO(jaewan): Make this host-side test to see per-user behavior.
+ @Ignore
+ @Test
+ public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
+ prepareLooper();
+ final MockPlayer player = (MockPlayer) mSession.getPlayer();
+ player.notifyPlaybackState(MediaPlayerBase.PLAYER_STATE_IDLE);
+
+ MediaController2 controller = null;
+// List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+// assertNotNull(tokens);
+// for (int i = 0; i < tokens.size(); i++) {
+// SessionToken2 token = tokens.get(i);
+// if (mContext.getPackageName().equals(token.getPackageName())
+// && TAG.equals(token.getId())) {
+// assertNull(controller);
+// controller = createController(token);
+// }
+// }
+// assertNotNull(controller);
+//
+// // Test if the found controller is correct one.
+// assertEquals(MediaPlayerBase.PLAYER_STATE_IDLE, controller.getPlayerState());
+// controller.play();
+//
+// assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+// assertTrue(player.mPlayCalled);
+ }
+
+ /**
+ * Test if server recognizes a session even if the session refuses the connection from server.
+ *
+ * @throws InterruptedException
+ */
+ @Test
+ public void testGetSessionTokens_sessionRejected() throws InterruptedException {
+ prepareLooper();
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext).setPlayer(new MockPlayer(0))
+ .setId(TAG).setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public SessionCommandGroup2 onConnect(
+ MediaSession2 session, ControllerInfo controller) {
+ // Reject all connection request.
+ return null;
+ }
+ }).build();
+
+ boolean foundSession = false;
+// List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+// assertNotNull(tokens);
+// for (int i = 0; i < tokens.size(); i++) {
+// SessionToken2 token = tokens.get(i);
+// if (mContext.getPackageName().equals(token.getPackageName())
+// && TAG.equals(token.getId())) {
+// assertFalse(foundSession);
+// foundSession = true;
+// }
+// }
+// assertTrue(foundSession);
+ }
+
+ @Test
+ public void testGetMediaSession2Tokens_sessionClosed() throws InterruptedException {
+ prepareLooper();
+ mSession.close();
+
+ // When a session is closed, it should lose binder connection between server immediately.
+ // So server will forget the session.
+// List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+// for (int i = 0; i < tokens.size(); i++) {
+// SessionToken2 token = tokens.get(i);
+// assertFalse(mContext.getPackageName().equals(token.getPackageName())
+// && TAG.equals(token.getId()));
+// }
+ }
+
+ @Test
+ public void testGetMediaSessionService2Token() throws InterruptedException {
+ prepareLooper();
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+// List<SessionToken2> tokens = mManager.getSessionServiceTokens();
+// for (int i = 0; i < tokens.size(); i++) {
+// SessionToken2 token = tokens.get(i);
+// if (mContext.getPackageName().equals(token.getPackageName())
+// && MockMediaSessionService2.ID.equals(token.getId())) {
+// assertFalse(foundTestSessionService);
+// assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+// foundTestSessionService = true;
+// } else if (mContext.getPackageName().equals(token.getPackageName())
+// && MockMediaLibraryService2.ID.equals(token.getId())) {
+// assertFalse(foundTestLibraryService);
+// assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+// foundTestLibraryService = true;
+// }
+// }
+// assertTrue(foundTestSessionService);
+// assertTrue(foundTestLibraryService);
+ }
+
+ @Test
+ public void testGetAllSessionTokens() throws InterruptedException {
+ prepareLooper();
+ boolean foundTestSession = false;
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+// List<SessionToken2> tokens = mManager.getAllSessionTokens();
+// for (int i = 0; i < tokens.size(); i++) {
+// SessionToken2 token = tokens.get(i);
+// if (!mContext.getPackageName().equals(token.getPackageName())) {
+// continue;
+// }
+// switch (token.getId()) {
+// case TAG:
+// assertFalse(foundTestSession);
+// foundTestSession = true;
+// break;
+// case MockMediaSessionService2.ID:
+// assertFalse(foundTestSessionService);
+// foundTestSessionService = true;
+// assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+// break;
+// case MockMediaLibraryService2.ID:
+// assertFalse(foundTestLibraryService);
+// assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+// foundTestLibraryService = true;
+// break;
+// default:
+// fail("Unexpected session " + token + " exists in the package");
+// }
+// }
+// assertTrue(foundTestSession);
+// assertTrue(foundTestSessionService);
+// assertTrue(foundTestLibraryService);
+ }
+
+ @Test
+ public void testAddOnSessionTokensChangedListener() throws InterruptedException {
+// prepareLooper();
+// TokensChangedListener listener = new TokensChangedListener();
+// mManager.addOnSessionTokensChangedListener(sHandlerExecutor, listener);
+//
+// listener.reset();
+// MediaSession2 session1 = new MediaSession2.Builder(mContext)
+// .setPlayer(new MockPlayer(0))
+// .setId(UUID.randomUUID().toString())
+// .build();
+// assertTrue(listener.await());
+// assertTrue(listener.findToken(session1.getToken()));
+//
+// listener.reset();
+// session1.close();
+// assertTrue(listener.await());
+// assertFalse(listener.findToken(session1.getToken()));
+//
+// listener.reset();
+// MediaSession2 session2 = new MediaSession2.Builder(mContext)
+// .setPlayer(new MockPlayer(0))
+// .setId(UUID.randomUUID().toString())
+// .build();
+// assertTrue(listener.await());
+// assertFalse(listener.findToken(session1.getToken()));
+// assertTrue(listener.findToken(session2.getToken()));
+//
+// listener.reset();
+// MediaSession2 session3 = new MediaSession2.Builder(mContext)
+// .setPlayer(new MockPlayer(0))
+// .setId(UUID.randomUUID().toString())
+// .build();
+// assertTrue(listener.await());
+// assertFalse(listener.findToken(session1.getToken()));
+// assertTrue(listener.findToken(session2.getToken()));
+// assertTrue(listener.findToken(session3.getToken()));
+//
+// listener.reset();
+// session2.close();
+// assertTrue(listener.await());
+// assertFalse(listener.findToken(session1.getToken()));
+// assertFalse(listener.findToken(session2.getToken()));
+// assertTrue(listener.findToken(session3.getToken()));
+//
+// listener.reset();
+// session3.close();
+// assertTrue(listener.await());
+// assertFalse(listener.findToken(session1.getToken()));
+// assertFalse(listener.findToken(session2.getToken()));
+// assertFalse(listener.findToken(session3.getToken()));
+//
+// mManager.removeOnSessionTokensChangedListener(listener);
+ }
+
+ @Test
+ public void testRemoveOnSessionTokensChangedListener() throws InterruptedException {
+// prepareLooper();
+// TokensChangedListener listener = new TokensChangedListener();
+// mManager.addOnSessionTokensChangedListener(sHandlerExecutor, listener);
+//
+// listener.reset();
+// MediaSession2 session1 = new MediaSession2.Builder(mContext)
+// .setPlayer(new MockPlayer(0))
+// .setId(UUID.randomUUID().toString())
+// .build();
+// assertTrue(listener.await());
+//
+// mManager.removeOnSessionTokensChangedListener(listener);
+//
+// listener.reset();
+// session1.close();
+// assertFalse(listener.await());
+//
+// listener.reset();
+// MediaSession2 session2 = new MediaSession2.Builder(mContext)
+// .setPlayer(new MockPlayer(0))
+// .setId(UUID.randomUUID().toString())
+// .build();
+// assertFalse(listener.await());
+//
+// listener.reset();
+// MediaSession2 session3 = new MediaSession2.Builder(mContext)
+// .setPlayer(new MockPlayer(0))
+// .setId(UUID.randomUUID().toString())
+// .build();
+// assertFalse(listener.await());
+//
+// listener.reset();
+// session2.close();
+// assertFalse(listener.await());
+//
+// listener.reset();
+// session3.close();
+// assertFalse(listener.await());
+ }
+
+// private class TokensChangedListener implements OnSessionTokensChangedListener {
+// private CountDownLatch mLatch;
+// private List<SessionToken2> mTokens;
+//
+// private void reset() {
+// mLatch = new CountDownLatch(1);
+// mTokens = null;
+// }
+//
+// private boolean await() throws InterruptedException {
+// return mLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+// }
+//
+// private boolean findToken(SessionToken2 token) {
+// return mTokens.contains(token);
+// }
+//
+// @Override
+// public void onSessionTokensChanged(List<SessionToken2> tokens) {
+// mTokens = tokens;
+// mLatch.countDown();
+// }
+// }
+}
diff --git a/androidx/media/MediaSessionService2.java b/androidx/media/MediaSessionService2.java
new file mode 100644
index 00000000..7bad65c5
--- /dev/null
+++ b/androidx/media/MediaSessionService2.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaBrowserServiceCompat.BrowserRoot;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.SessionToken2.TokenType;
+
+import java.util.List;
+
+/**
+ * @hide
+ * Base class for media session services, which is the service version of the {@link MediaSession2}.
+ * <p>
+ * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
+ * to keep media playback in the background.
+ * <p>
+ * Here's the benefits of using {@link MediaSessionService2} instead of
+ * {@link MediaSession2}.
+ * <ul>
+ * <li>Another app can know that your app supports {@link MediaSession2} even when your app
+ * isn't running.
+ * <li>Another app can start playback of your app even when your app isn't running.
+ * </ul>
+ * For example, user's voice command can start playback of your app even when it's not running.
+ * <p>
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;meta-data android:name="android.media.session"
+ * android:value="session_id"/&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * It's recommended for an app to have a single {@link MediaSessionService2} declared in the
+ * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another
+ * app fails to pick the right session service when it wants to start the playback this app.
+ * <p>
+ * If there's conflicts with the session ID among the services, services wouldn't be available for
+ * any controllers.
+ * <p>
+ * Topic covered here:
+ * <ol>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * </ol>
+ * <div class="special reference">
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ * <p>
+ * Session service is bounded service. When a {@link MediaController2} is created for the
+ * session service, the controller binds to the session service. {@link #onCreateSession(String)}
+ * may be called after the {@link #onCreate} if the service hasn't created yet.
+ * <p>
+ * After the binding, session's
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}
+ *
+ * will be called to accept or reject connection request from a controller. If the connection is
+ * rejected, the controller will unbind. If it's accepted, the controller will be available to use
+ * and keep binding.
+ * <p>
+ * When playback is started for this session service, {@link #onUpdateNotification()}
+ * is called and service would become a foreground service. It's needed to keep playback after the
+ * controller is destroyed. The session service becomes background service when the playback is
+ * stopped.
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>
+ * Any app can bind to the session service with controller, but the controller can be used only if
+ * the session service accepted the connection request through
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MediaSessionService2 extends Service {
+ //private final MediaSessionService2Provider mProvider;
+
+ /**
+ * This is the interface name that a service implementing a session service should say that it
+ * support -- that is, this is the action it uses for its intent filter.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2";
+
+ /**
+ * Name under which a MediaSessionService2 component publishes information about itself.
+ * This meta-data must provide a string value for the ID.
+ */
+ public static final String SERVICE_META_DATA = "android.media.session";
+
+ // Stub BrowserRoot for accepting any connction here.
+ // See MyBrowserService#onGetRoot() for detail.
+ static final BrowserRoot sDefaultBrowserRoot = new BrowserRoot(SERVICE_INTERFACE, null);
+
+ private final MediaBrowserServiceCompat mBrowserServiceCompat;
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private NotificationManager mNotificationManager;
+ @GuardedBy("mLock")
+ private Intent mStartSelfIntent;
+ @GuardedBy("mLock")
+ private boolean mIsRunningForeground;
+ @GuardedBy("mLock")
+ private MediaSession2 mSession;
+
+ public MediaSessionService2() {
+ super();
+ mBrowserServiceCompat = createBrowserServiceCompat();
+ }
+
+ MediaBrowserServiceCompat createBrowserServiceCompat() {
+ return new MyBrowserService();
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to initialize session service.
+ * <p>
+ * Override this method if you need your own initialization. Derived classes MUST call through
+ * to the super class's implementation of this method.
+ */
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBrowserServiceCompat.attachToBaseContext(this);
+ mBrowserServiceCompat.onCreate();
+ SessionToken2 token = new SessionToken2(this,
+ new ComponentName(getPackageName(), getClass().getName()));
+ if (token.getType() != getSessionType()) {
+ throw new RuntimeException("Expected session type " + getSessionType()
+ + " but was " + token.getType());
+ }
+ MediaSession2 session = onCreateSession(token.getId());
+ synchronized (mLock) {
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mStartSelfIntent = new Intent(this, getClass());
+ mSession = session;
+ if (mSession == null || !token.getId().equals(mSession.getToken().getId())) {
+ throw new RuntimeException("Expected session with id " + token.getId()
+ + ", but got " + mSession);
+ }
+ mBrowserServiceCompat.setSessionToken(mSession.getToken().getSessionCompatToken());
+ }
+ }
+
+ @TokenType int getSessionType() {
+ return SessionToken2.TYPE_SESSION_SERVICE;
+ }
+
+ /**
+ * Called when another app requested to start this service to get {@link MediaSession2}.
+ * <p>
+ * Session service will accept or reject the connection with the
+ * {@link MediaSession2.SessionCallback} in the created session.
+ * <p>
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ * <p>
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new session
+ * @see MediaSession2.Builder
+ * @see #getSession()
+ */
+ public @NonNull abstract MediaSession2 onCreateSession(String sessionId);
+
+ /**
+ * Called when the playback state of this session is changed so notification needs update.
+ * Override this method to show or cancel your own notification UI.
+ * <p>
+ * With the notification returned here, the service become foreground service when the playback
+ * is started. It becomes background service after the playback is stopped.
+ *
+ * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
+ */
+ public @Nullable MediaNotification onUpdateNotification() {
+ return null;
+ }
+
+ /**
+ * Get instance of the {@link MediaSession2} that you've previously created with the
+ * {@link #onCreateSession} for this service.
+ * <p>
+ * This may be {@code null} before the {@link #onCreate()} is finished.
+ *
+ * @return created session
+ */
+ public final @Nullable MediaSession2 getSession() {
+ synchronized (mLock) {
+ return mSession;
+ }
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to handle incoming binding
+ * request. If the request is for getting the session, the intent will have action
+ * {@link #SERVICE_INTERFACE}.
+ * <p>
+ * Override this method if this service also needs to handle binder requests other than
+ * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's
+ * implementation of this method.
+ *
+ * @param intent
+ * @return Binder
+ */
+ @CallSuper
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction())
+ || MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) {
+ // Change the intent action for browser service.
+ Intent browserServiceIntent = new Intent(intent);
+ browserServiceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
+ return mBrowserServiceCompat.onBind(intent);
+ }
+ return null;
+ }
+
+ MediaBrowserServiceCompat getServiceCompat() {
+ return mBrowserServiceCompat;
+ }
+
+ /**
+ * Returned by {@link #onUpdateNotification()} for making session service foreground service
+ * to keep playback running in the background. It's highly recommended to show media style
+ * notification here.
+ */
+ public static class MediaNotification {
+ private final int mNotificationId;
+ private final Notification mNotification;
+
+ /**
+ * Default constructor
+ *
+ * @param notificationId notification id to be used for
+ * {@link NotificationManager#notify(int, Notification)}.
+ * @param notification a notification to make session service foreground service. Media
+ * style notification is recommended here.
+ */
+ public MediaNotification(int notificationId, @NonNull Notification notification) {
+ if (notification == null) {
+ throw new IllegalArgumentException("notification shouldn't be null");
+ }
+ mNotificationId = notificationId;
+ mNotification = notification;
+ }
+
+ /**
+ * Gets the id of the id.
+ *
+ * @return the notification id
+ */
+ public int getNotificationId() {
+ return mNotificationId;
+ }
+
+ /**
+ * Gets the notification.
+ *
+ * @return the notification
+ */
+ public @NonNull Notification getNotification() {
+ return mNotification;
+ }
+ }
+
+ private static class MyBrowserService extends MediaBrowserServiceCompat {
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ // Returns *stub* root here. Here's the reason.
+ // 1. A non-null BrowserRoot should be returned here to keep the binding
+ // 2. MediaSessionService2 is defined as the simplified version of the library
+ // service with no browsing feature, so shouldn't allow MediaBrowserServiceCompat
+ // specific operations.
+ // TODO: Revisit here API not to return stub root here. The fake media ID here may be
+ // used by the browser service for real.
+ return sDefaultBrowserRoot;
+ }
+
+ @Override
+ public void onLoadChildren(String parentId, Result<List<MediaItem>> result) {
+ // Disallow loading children.
+ }
+ }
+}
diff --git a/androidx/media/MediaStubActivity.java b/androidx/media/MediaStubActivity.java
new file mode 100644
index 00000000..44d8ad96
--- /dev/null
+++ b/androidx/media/MediaStubActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import androidx.media.test.R;
+
+public class MediaStubActivity extends Activity {
+ private static final String TAG = "MediaStubActivity";
+ private SurfaceHolder mHolder;
+ private SurfaceHolder mHolder2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.mediaplayer);
+
+ SurfaceView surfaceV = (SurfaceView) findViewById(R.id.surface);
+ mHolder = surfaceV.getHolder();
+
+ SurfaceView surfaceV2 = (SurfaceView) findViewById(R.id.surface2);
+ mHolder2 = surfaceV2.getHolder();
+ }
+
+ @Override
+ protected void onResume() {
+ Log.i(TAG, "onResume");
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ Log.i(TAG, "onPause");
+ super.onPause();
+ }
+ public SurfaceHolder getSurfaceHolder() {
+ return mHolder;
+ }
+
+ public SurfaceHolder getSurfaceHolder2() {
+ return mHolder2;
+ }
+}
diff --git a/androidx/media/MediaUtils2.java b/androidx/media/MediaUtils2.java
new file mode 100644
index 00000000..657e24d2
--- /dev/null
+++ b/androidx/media/MediaUtils2.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.media.AudioAttributesCompat.CONTENT_TYPE_UNKNOWN;
+import static androidx.media.AudioAttributesCompat.USAGE_UNKNOWN;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_DESCRIPTION;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_ICON_URI;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_SUBTITLE;
+import static androidx.media.MediaMetadata2.METADATA_KEY_DISPLAY_TITLE;
+import static androidx.media.MediaMetadata2.METADATA_KEY_EXTRAS;
+import static androidx.media.MediaMetadata2.METADATA_KEY_MEDIA_ID;
+import static androidx.media.MediaMetadata2.METADATA_KEY_MEDIA_URI;
+import static androidx.media.MediaMetadata2.METADATA_KEY_TITLE;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import androidx.media.MediaSession2.CommandButton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class MediaUtils2 {
+ static final String AUDIO_ATTRIBUTES_USAGE = "androidx.media.audio_attrs.USAGE";
+ static final String AUDIO_ATTRIBUTES_CONTENT_TYPE = "androidx.media.audio_attrs.CONTENT_TYPE";
+ static final String AUDIO_ATTRIBUTES_FLAGS = "androidx.media.audio_attrs.FLAGS";
+
+ private MediaUtils2() {
+ }
+
+ /**
+ * Creates a {@link MediaItem} from the {@link MediaItem2}.
+ *
+ * @param item2 an item.
+ * @return The newly created media item.
+ */
+ static MediaItem createMediaItem(MediaItem2 item2) {
+ if (item2 == null) {
+ return null;
+ }
+ MediaDescriptionCompat descCompat;
+
+ MediaMetadata2 metadata = item2.getMetadata();
+ if (metadata == null) {
+ descCompat = new MediaDescriptionCompat.Builder()
+ .setMediaId(item2.getMediaId())
+ .build();
+ } else {
+ MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder()
+ .setMediaId(item2.getMediaId())
+ .setSubtitle(metadata.getText(METADATA_KEY_DISPLAY_SUBTITLE))
+ .setDescription(metadata.getText(METADATA_KEY_DISPLAY_DESCRIPTION))
+ .setIconBitmap(metadata.getBitmap(METADATA_KEY_DISPLAY_ICON))
+ .setExtras(metadata.getExtras());
+
+ String title = metadata.getString(METADATA_KEY_TITLE);
+ if (title != null) {
+ builder.setTitle(title);
+ } else {
+ builder.setTitle(metadata.getString(METADATA_KEY_DISPLAY_TITLE));
+ }
+
+ String displayIconUri = metadata.getString(METADATA_KEY_DISPLAY_ICON_URI);
+ if (displayIconUri != null) {
+ builder.setIconUri(Uri.parse(displayIconUri));
+ }
+
+ String mediaUri = metadata.getString(METADATA_KEY_MEDIA_URI);
+ if (mediaUri != null) {
+ builder.setMediaUri(Uri.parse(mediaUri));
+ }
+
+ descCompat = builder.build();
+ }
+ return new MediaItem(descCompat, item2.getFlags());
+ }
+
+ /**
+ * Creates a {@link MediaItem2} from the {@link MediaItem}.
+ *
+ * @param item an item.
+ * @return The newly created media item.
+ */
+ static MediaItem2 createMediaItem2(MediaItem item) {
+ if (item == null || item.getMediaId() == null) {
+ return null;
+ }
+
+ MediaMetadata2 metadata2 = createMediaMetadata2(item.getDescription());
+ return new MediaItem2.Builder(item.getFlags())
+ .setMediaId(item.getMediaId())
+ .setMetadata(metadata2)
+ .build();
+ }
+
+ /**
+ * Creates a {@link MediaMetadata2} from the {@link MediaDescriptionCompat}.
+ *
+ * @param descCompat A {@link MediaDescriptionCompat} object.
+ * @return The newly created {@link MediaMetadata2} object.
+ */
+ static MediaMetadata2 createMediaMetadata2(MediaDescriptionCompat descCompat) {
+ if (descCompat == null) {
+ return null;
+ }
+
+ MediaMetadata2.Builder metadata2Builder = new MediaMetadata2.Builder();
+ metadata2Builder.putString(METADATA_KEY_MEDIA_ID, descCompat.getMediaId());
+
+ CharSequence title = descCompat.getTitle();
+ if (title != null) {
+ metadata2Builder.putText(METADATA_KEY_DISPLAY_TITLE, title);
+ }
+
+ CharSequence description = descCompat.getDescription();
+ if (description != null) {
+ metadata2Builder.putText(METADATA_KEY_DISPLAY_DESCRIPTION, descCompat.getDescription());
+ }
+
+ CharSequence subtitle = descCompat.getSubtitle();
+ if (subtitle != null) {
+ metadata2Builder.putText(METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
+ }
+
+ Bitmap icon = descCompat.getIconBitmap();
+ if (icon != null) {
+ metadata2Builder.putBitmap(METADATA_KEY_DISPLAY_ICON, icon);
+ }
+
+ Uri iconUri = descCompat.getIconUri();
+ if (iconUri != null) {
+ metadata2Builder.putText(METADATA_KEY_DISPLAY_ICON_URI, iconUri.toString());
+ }
+
+ Bundle bundle = descCompat.getExtras();
+ if (bundle != null) {
+ metadata2Builder.setExtras(descCompat.getExtras());
+ }
+
+ Uri mediaUri = descCompat.getMediaUri();
+ if (mediaUri != null) {
+ metadata2Builder.putText(METADATA_KEY_MEDIA_URI, mediaUri.toString());
+ }
+
+ return metadata2Builder.build();
+ }
+
+ /**
+ * Creates a {@link MediaMetadata2} from the {@link MediaMetadataCompat}.
+ *
+ * @param metadataCompat A {@link MediaMetadataCompat} object.
+ * @return The newly created {@link MediaMetadata2} object.
+ */
+ MediaMetadata2 createMediaMetadata2(MediaMetadataCompat metadataCompat) {
+ if (metadataCompat == null) {
+ return null;
+ }
+ return new MediaMetadata2(metadataCompat.getBundle());
+ }
+
+ /**
+ * Creates a {@link MediaMetadataCompat} from the {@link MediaMetadata2}.
+ *
+ * @param metadata2 A {@link MediaMetadata2} object.
+ * @return The newly created {@link MediaMetadataCompat} object.
+ */
+ MediaMetadataCompat createMediaMetadataCompat(MediaMetadata2 metadata2) {
+ if (metadata2 == null) {
+ return null;
+ }
+
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+ List<String> skippedKeys = new ArrayList<>();
+ Bundle bundle = metadata2.toBundle();
+ for (String key : bundle.keySet()) {
+ Object value = bundle.get(key);
+ if (value instanceof CharSequence) {
+ builder.putText(key, (CharSequence) value);
+ } else if (value instanceof Rating2) {
+ builder.putRating(key, createRatingCompat((Rating2) value));
+ } else if (value instanceof Bitmap) {
+ builder.putBitmap(key, (Bitmap) value);
+ } else if (value instanceof Long) {
+ builder.putLong(key, (Long) value);
+ } else {
+ // There is no 'float' or 'bundle' type in MediaMetadataCompat.
+ skippedKeys.add(key);
+ }
+ }
+
+ MediaMetadataCompat result = builder.build();
+ for (String key : skippedKeys) {
+ Object value = bundle.get(key);
+ if (value instanceof Float) {
+ // Compatibility for MediaMetadata2.Builder.putFloat()
+ result.getBundle().putFloat(key, (Float) value);
+ } else if (METADATA_KEY_EXTRAS.equals(value)) {
+ // Compatibility for MediaMetadata2.Builder.setExtras()
+ result.getBundle().putBundle(key, (Bundle) value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates a {@link Rating2} from the {@link RatingCompat}.
+ *
+ * @param ratingCompat A {@link RatingCompat} object.
+ * @return The newly created {@link Rating2} object.
+ */
+ Rating2 createRating2(RatingCompat ratingCompat) {
+ if (ratingCompat == null) {
+ return null;
+ }
+ if (!ratingCompat.isRated()) {
+ return Rating2.newUnratedRating(ratingCompat.getRatingStyle());
+ }
+
+ switch (ratingCompat.getRatingStyle()) {
+ case RatingCompat.RATING_3_STARS:
+ case RatingCompat.RATING_4_STARS:
+ case RatingCompat.RATING_5_STARS:
+ return Rating2.newStarRating(
+ ratingCompat.getRatingStyle(), ratingCompat.getStarRating());
+ case RatingCompat.RATING_HEART:
+ return Rating2.newHeartRating(ratingCompat.hasHeart());
+ case RatingCompat.RATING_THUMB_UP_DOWN:
+ return Rating2.newThumbRating(ratingCompat.isThumbUp());
+ case RatingCompat.RATING_PERCENTAGE:
+ return Rating2.newPercentageRating(ratingCompat.getPercentRating());
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Creates a {@link RatingCompat} from the {@link Rating2}.
+ *
+ * @param rating2 A {@link Rating2} object.
+ * @return The newly created {@link RatingCompat} object.
+ */
+ RatingCompat createRatingCompat(Rating2 rating2) {
+ if (rating2 == null) {
+ return null;
+ }
+ if (!rating2.isRated()) {
+ return RatingCompat.newUnratedRating(rating2.getRatingStyle());
+ }
+
+ switch (rating2.getRatingStyle()) {
+ case Rating2.RATING_3_STARS:
+ case Rating2.RATING_4_STARS:
+ case Rating2.RATING_5_STARS:
+ return RatingCompat.newStarRating(
+ rating2.getRatingStyle(), rating2.getStarRating());
+ case Rating2.RATING_HEART:
+ return RatingCompat.newHeartRating(rating2.hasHeart());
+ case Rating2.RATING_THUMB_UP_DOWN:
+ return RatingCompat.newThumbRating(rating2.isThumbUp());
+ case Rating2.RATING_PERCENTAGE:
+ return RatingCompat.newPercentageRating(rating2.getPercentRating());
+ default:
+ return null;
+ }
+ }
+
+ static Parcelable[] toMediaItem2ParcelableArray(List<MediaItem2> playlist) {
+ if (playlist == null) {
+ return null;
+ }
+ List<Parcelable> parcelableList = new ArrayList<>();
+ for (int i = 0; i < playlist.size(); i++) {
+ final MediaItem2 item = playlist.get(i);
+ if (item != null) {
+ final Parcelable itemBundle = item.toBundle();
+ if (itemBundle != null) {
+ parcelableList.add(itemBundle);
+ }
+ }
+ }
+ return parcelableList.toArray(new Parcelable[0]);
+ }
+
+ static List<MediaItem2> fromMediaItem2ParcelableArray(Parcelable[] itemParcelableList) {
+ List<MediaItem2> playlist = new ArrayList<>();
+ if (itemParcelableList != null) {
+ for (int i = 0; i < itemParcelableList.length; i++) {
+ if (!(itemParcelableList[i] instanceof Bundle)) {
+ continue;
+ }
+ MediaItem2 item = MediaItem2.fromBundle((Bundle) itemParcelableList[i]);
+ if (item != null) {
+ playlist.add(item);
+ }
+ }
+ }
+ return playlist;
+ }
+
+ static Parcelable[] toCommandButtonParcelableArray(List<CommandButton> layout) {
+ if (layout == null) {
+ return null;
+ }
+ List<Bundle> layoutBundles = new ArrayList<>();
+ for (int i = 0; i < layout.size(); i++) {
+ Bundle bundle = layout.get(i).toBundle();
+ if (bundle != null) {
+ layoutBundles.add(bundle);
+ }
+ }
+ return layoutBundles.toArray(new Parcelable[0]);
+ }
+
+ static List<CommandButton> fromCommandButtonParcelableArray(Parcelable[] list) {
+ List<CommandButton> layout = new ArrayList<>();
+ if (layout != null) {
+ for (int i = 0; i < list.length; i++) {
+ if (!(list[i] instanceof Bundle)) {
+ continue;
+ }
+ CommandButton button = CommandButton.fromBundle((Bundle) list[i]);
+ if (button != null) {
+ layout.add(button);
+ }
+ }
+ }
+ return layout;
+ }
+
+ static Bundle toAudioAttributesBundle(AudioAttributesCompat attrs) {
+ if (attrs == null) {
+ return null;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putInt(AUDIO_ATTRIBUTES_USAGE, attrs.getUsage());
+ bundle.putInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, attrs.getContentType());
+ bundle.putInt(AUDIO_ATTRIBUTES_FLAGS, attrs.getFlags());
+ return bundle;
+ }
+
+ static AudioAttributesCompat fromAudioAttributesBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ return new AudioAttributesCompat.Builder()
+ .setUsage(bundle.getInt(AUDIO_ATTRIBUTES_USAGE, USAGE_UNKNOWN))
+ .setContentType(bundle.getInt(AUDIO_ATTRIBUTES_CONTENT_TYPE, CONTENT_TYPE_UNKNOWN))
+ .setFlags(bundle.getInt(AUDIO_ATTRIBUTES_FLAGS, 0))
+ .build();
+ }
+
+ static List<Bundle> toBundleList(Parcelable[] array) {
+ if (array == null) {
+ return null;
+ }
+ List<Bundle> bundleList = new ArrayList<>();
+ for (Parcelable p : array) {
+ bundleList.add((Bundle) p);
+ }
+ return bundleList;
+ }
+
+ static int createPlaybackStateCompatState(int playerState, int bufferingState) {
+ switch (playerState) {
+ case MediaPlayerBase.PLAYER_STATE_PLAYING:
+ switch (bufferingState) {
+ case MediaPlayerBase.BUFFERING_STATE_BUFFERING_AND_STARVED:
+ return PlaybackStateCompat.STATE_BUFFERING;
+ }
+ return PlaybackStateCompat.STATE_PLAYING;
+ case MediaPlayerBase.PLAYER_STATE_PAUSED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case MediaPlayerBase.PLAYER_STATE_IDLE:
+ return PlaybackStateCompat.STATE_NONE;
+ case MediaPlayerBase.PLAYER_STATE_ERROR:
+ return PlaybackStateCompat.STATE_ERROR;
+ }
+ // For unknown value
+ return PlaybackStateCompat.STATE_ERROR;
+ }
+
+ static int toPlayerState(int playbackStateCompatState) {
+ switch (playbackStateCompatState) {
+ case PlaybackStateCompat.STATE_ERROR:
+ return MediaPlayerBase.PLAYER_STATE_ERROR;
+ case PlaybackStateCompat.STATE_NONE:
+ return MediaPlayerBase.PLAYER_STATE_IDLE;
+ case PlaybackStateCompat.STATE_PAUSED:
+ case PlaybackStateCompat.STATE_STOPPED:
+ case PlaybackStateCompat.STATE_BUFFERING: // means paused for buffering.
+ return MediaPlayerBase.PLAYER_STATE_PAUSED;
+ case PlaybackStateCompat.STATE_FAST_FORWARDING:
+ case PlaybackStateCompat.STATE_PLAYING:
+ case PlaybackStateCompat.STATE_REWINDING:
+ case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT:
+ case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
+ case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
+ case PlaybackStateCompat.STATE_CONNECTING: // Note: there's no perfect match for this.
+ return MediaPlayerBase.PLAYER_STATE_PLAYING;
+ }
+ return MediaPlayerBase.PLAYER_STATE_ERROR;
+ }
+
+ static boolean isDefaultLibraryRootHint(Bundle bundle) {
+ return bundle != null && bundle.getBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, false);
+ }
+}
diff --git a/android/security/keystore/DecryptionFailedException.java b/androidx/media/MockActivity.java
index c0b52f71..df0af166 100644
--- a/android/security/keystore/DecryptionFailedException.java
+++ b/androidx/media/MockActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,9 @@
* limitations under the License.
*/
-package android.security.keystore;
+package androidx.media;
-/**
- * @deprecated Use {@link android.security.keystore.recovery.DecryptionFailedException}.
- * @hide
- */
-public class DecryptionFailedException extends RecoveryControllerException {
+import android.app.Activity;
- public DecryptionFailedException(String msg) {
- super(msg);
- }
+public class MockActivity extends Activity {
}
diff --git a/androidx/media/MockMediaLibraryService2.java b/androidx/media/MockMediaLibraryService2.java
new file mode 100644
index 00000000..8f5d5bba
--- /dev/null
+++ b/androidx/media/MockMediaLibraryService2.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Mock implementation of {@link MediaLibraryService2} for testing.
+ */
+public class MockMediaLibraryService2 extends MediaLibraryService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestLibrary";
+
+ public static final String ROOT_ID = "rootId";
+ public static final Bundle EXTRAS = new Bundle();
+
+ public static final String MEDIA_ID_GET_ITEM = "media_id_get_item";
+
+ public static final String PARENT_ID = "parent_id";
+ public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children";
+ public static final String PARENT_ID_ERROR = "parent_id_error";
+
+ public static final List<MediaItem2> GET_CHILDREN_RESULT = new ArrayList<>();
+ public static final int CHILDREN_COUNT = 100;
+
+ public static final String SEARCH_QUERY = "search_query";
+ public static final String SEARCH_QUERY_TAKES_TIME = "search_query_takes_time";
+ public static final int SEARCH_TIME_IN_MS = 5000;
+ public static final String SEARCH_QUERY_EMPTY_RESULT = "search_query_empty_result";
+
+ public static final List<MediaItem2> SEARCH_RESULT = new ArrayList<>();
+ public static final int SEARCH_RESULT_COUNT = 50;
+
+ // TODO(jaewan): Uncomment here after DataSourceDesc.builder is ready.
+// private static final DataSourceDesc DATA_SOURCE_DESC =
+// new DataSourceDesc.Builder().setDataSource(new FileDescriptor()).build();
+ private static final DataSourceDesc DATA_SOURCE_DESC = null;
+
+ private static final String TAG = "MockMediaLibrarySvc2";
+
+ static {
+ EXTRAS.putString(ROOT_ID, ROOT_ID);
+ }
+ @GuardedBy("MockMediaLibraryService2.class")
+ private static SessionToken2 sToken;
+
+ private MediaLibrarySession mSession;
+
+ public MockMediaLibraryService2() {
+ super();
+ GET_CHILDREN_RESULT.clear();
+ String getChildrenMediaIdPrefix = "get_children_media_id_";
+ for (int i = 0; i < CHILDREN_COUNT; i++) {
+ GET_CHILDREN_RESULT.add(createMediaItem(getChildrenMediaIdPrefix + i));
+ }
+
+ SEARCH_RESULT.clear();
+ String getSearchResultMediaIdPrefix = "get_search_result_media_id_";
+ for (int i = 0; i < SEARCH_RESULT_COUNT; i++) {
+ SEARCH_RESULT.add(createMediaItem(getSearchResultMediaIdPrefix + i));
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ TestServiceRegistry.getInstance().setServiceInstance(this);
+ super.onCreate();
+ }
+
+ @Override
+ public MediaLibrarySession onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ final Executor executor = new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ handler.post(runnable);
+ }
+ };
+ SessionCallback callback = TestServiceRegistry.getInstance().getSessionCallback();
+ MediaLibrarySessionCallback librarySessionCallback;
+ if (callback instanceof MediaLibrarySessionCallback) {
+ librarySessionCallback = (MediaLibrarySessionCallback) callback;
+ } else {
+ // Callback hasn't set. Use default callback
+ librarySessionCallback = new TestLibrarySessionCallback();
+ }
+ mSession = new MediaLibrarySession.Builder(MockMediaLibraryService2.this, executor,
+ librarySessionCallback).setPlayer(player).setId(sessionId).build();
+ return mSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ public static SessionToken2 getToken(Context context) {
+ synchronized (MockMediaLibraryService2.class) {
+ if (sToken == null) {
+ sToken = new SessionToken2(context, new ComponentName(
+ context.getPackageName(), MockMediaLibraryService2.class.getName()));
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, sToken.getType());
+ }
+ return sToken;
+ }
+ }
+
+ private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+ @Override
+ public LibraryRoot onGetLibraryRoot(MediaLibrarySession session, ControllerInfo controller,
+ Bundle rootHints) {
+ return new LibraryRoot(ROOT_ID, EXTRAS);
+ }
+
+ @Override
+ public MediaItem2 onGetItem(MediaLibrarySession session, ControllerInfo controller,
+ String mediaId) {
+ if (MEDIA_ID_GET_ITEM.equals(mediaId)) {
+ return createMediaItem(mediaId);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetChildren(MediaLibrarySession session,
+ ControllerInfo controller, String parentId, int page, int pageSize, Bundle extras) {
+ if (PARENT_ID.equals(parentId)) {
+ return getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize);
+ } else if (PARENT_ID_ERROR.equals(parentId)) {
+ return null;
+ }
+ // Includes the case of PARENT_ID_NO_CHILDREN.
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void onSearch(MediaLibrarySession session, final ControllerInfo controllerInfo,
+ final String query, final Bundle extras) {
+ if (SEARCH_QUERY.equals(query)) {
+ mSession.notifySearchResultChanged(controllerInfo, query, SEARCH_RESULT_COUNT,
+ extras);
+ } else if (SEARCH_QUERY_TAKES_TIME.equals(query)) {
+ // Searching takes some time. Notify after 5 seconds.
+ Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
+ @Override
+ public void run() {
+ mSession.notifySearchResultChanged(
+ controllerInfo, query, SEARCH_RESULT_COUNT, extras);
+ }
+ }, SEARCH_TIME_IN_MS, TimeUnit.MILLISECONDS);
+ } else if (SEARCH_QUERY_EMPTY_RESULT.equals(query)) {
+ mSession.notifySearchResultChanged(controllerInfo, query, 0, extras);
+ } else {
+ // TODO: For the error case, how should we notify the browser?
+ }
+ }
+
+ @Override
+ public List<MediaItem2> onGetSearchResult(MediaLibrarySession session,
+ ControllerInfo controllerInfo, String query, int page, int pageSize,
+ Bundle extras) {
+ if (SEARCH_QUERY.equals(query)) {
+ return getPaginatedResult(SEARCH_RESULT, page, pageSize);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private List<MediaItem2> getPaginatedResult(List<MediaItem2> items, int page, int pageSize) {
+ if (items == null) {
+ return null;
+ } else if (items.size() == 0) {
+ return new ArrayList<>();
+ }
+
+ final int totalItemCount = items.size();
+ int fromIndex = (page - 1) * pageSize;
+ int toIndex = Math.min(page * pageSize, totalItemCount);
+
+ List<MediaItem2> paginatedResult = new ArrayList<>();
+ try {
+ // The case of (fromIndex >= totalItemCount) will throw exception below.
+ paginatedResult = items.subList(fromIndex, toIndex);
+ } catch (IndexOutOfBoundsException | IllegalArgumentException ex) {
+ Log.d(TAG, "Result is empty for given pagination arguments: totalItemCount="
+ + totalItemCount + ", page=" + page + ", pageSize=" + pageSize, ex);
+ }
+ return paginatedResult;
+ }
+
+ private MediaItem2 createMediaItem(String mediaId) {
+ Context context = MockMediaLibraryService2.this;
+ return new MediaItem2.Builder(0 /* Flags */)
+ .setMediaId(mediaId)
+ .setDataSourceDesc(DATA_SOURCE_DESC)
+ .setMetadata(new MediaMetadata2.Builder()
+ .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId)
+ .build())
+ .build();
+ }
+}
diff --git a/androidx/media/MockMediaSessionService2.java b/androidx/media/MockMediaSessionService2.java
new file mode 100644
index 00000000..bee53577
--- /dev/null
+++ b/androidx/media/MockMediaSessionService2.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+
+import androidx.media.MediaSession2.SessionCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Mock implementation of {@link MediaSessionService2} for testing.
+ */
+public class MockMediaSessionService2 extends MediaSessionService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestSession";
+
+ private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
+ private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
+
+ private NotificationChannel mDefaultNotificationChannel;
+ private MediaSession2 mSession;
+ private NotificationManager mNotificationManager;
+
+ @Override
+ public void onCreate() {
+ TestServiceRegistry.getInstance().setServiceInstance(this);
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public MediaSession2 onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ final Executor executor = new Executor() {
+ @Override
+ public void execute(Runnable runnable) {
+ handler.post(runnable);
+ }
+ };
+ SessionCallback sessionCallback = TestServiceRegistry.getInstance().getSessionCallback();
+ if (sessionCallback == null) {
+ // Ensures non-null
+ sessionCallback = new SessionCallback() {};
+ }
+ mSession = new MediaSession2.Builder(this)
+ .setPlayer(player)
+ .setSessionCallback(executor, sessionCallback)
+ .setId(sessionId).build();
+ return mSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ @Override
+ public MediaNotification onUpdateNotification() {
+ if (mDefaultNotificationChannel == null) {
+ mDefaultNotificationChannel = new NotificationChannel(
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
+ }
+ Notification notification = new Notification.Builder(
+ this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(getPackageName())
+ .setContentText("Dummt test notification")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ return new MediaNotification(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
+ }
+}
diff --git a/androidx/media/MockPlayer.java b/androidx/media/MockPlayer.java
new file mode 100644
index 00000000..49b1a19b
--- /dev/null
+++ b/androidx/media/MockPlayer.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerBase} for testing.
+ */
+public class MockPlayer extends MediaPlayerBase {
+ public final CountDownLatch mCountDownLatch;
+
+ public boolean mPlayCalled;
+ public boolean mPauseCalled;
+ public boolean mResetCalled;
+ public boolean mPrepareCalled;
+ public boolean mSeekToCalled;
+ public boolean mSetPlaybackSpeedCalled;
+ public long mSeekPosition;
+ public long mCurrentPosition;
+ public long mBufferedPosition;
+ public float mPlaybackSpeed = 1.0f;
+ public @PlayerState int mLastPlayerState;
+ public @BuffState int mLastBufferingState;
+
+ public ArrayMap<PlayerEventCallback, Executor> mCallbacks = new ArrayMap<>();
+
+ private AudioAttributesCompat mAudioAttributes;
+
+ public MockPlayer(int count) {
+ mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public void reset() {
+ mResetCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void play() {
+ mPlayCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void pause() {
+ mPauseCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void prepare() {
+ mPrepareCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ mSeekToCalled = true;
+ mSeekPosition = pos;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToNext() {
+ // No-op. This skipToNext() means 'skip to next item in the setNextDataSources()'
+ }
+
+ @Override
+ public int getPlayerState() {
+ return mLastPlayerState;
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ return mCurrentPosition;
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ return mBufferedPosition;
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ return mPlaybackSpeed;
+ }
+
+ @Override
+ public int getBufferingState() {
+ return mLastBufferingState;
+ }
+
+ @Override
+ public void registerPlayerEventCallback(@NonNull Executor executor,
+ @NonNull PlayerEventCallback callback) {
+ if (callback == null || executor == null) {
+ throw new IllegalArgumentException("callback=" + callback + " executor=" + executor);
+ }
+ mCallbacks.put(callback, executor);
+ }
+
+ @Override
+ public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ public void notifyPlaybackState(final int state) {
+ mLastPlayerState = state;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlayerStateChanged(MockPlayer.this, state);
+ }
+ });
+ }
+ }
+
+ public void notifyBufferingState(final MediaItem2 item, final int bufferingState) {
+ mLastBufferingState = bufferingState;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onBufferingStateChanged(
+ MockPlayer.this, item.getDataSourceDesc(), bufferingState);
+ }
+ });
+ }
+ }
+
+ public void notifyCurrentDataSourceChanged(final DataSourceDesc dsd) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onCurrentDataSourceChanged(MockPlayer.this, dsd);
+ }
+ });
+ }
+ }
+
+ public void notifyMediaPrepared(final DataSourceDesc dsd) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onMediaPrepared(MockPlayer.this, dsd);
+ }
+ });
+ }
+ }
+
+ public void notifyBufferingStateChanged(final DataSourceDesc dsd,
+ final @BuffState int buffState) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onBufferingStateChanged(MockPlayer.this, dsd, buffState);
+ }
+ });
+ }
+ }
+
+ public void notifyPlaybackSpeedChanged(final float speed) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPlaybackSpeedChanged(MockPlayer.this, speed);
+ }
+ });
+ }
+ }
+
+ public void notifyError(int what) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ final PlayerEventCallback callback = mCallbacks.keyAt(i);
+ final Executor executor = mCallbacks.valueAt(i);
+ // TODO: Uncomment or remove
+ //executor.execute(() -> callback.onError(null, what, 0));
+ }
+ }
+
+ @Override
+ public void setAudioAttributes(AudioAttributesCompat attributes) {
+ mAudioAttributes = attributes;
+ }
+
+ @Override
+ public AudioAttributesCompat getAudioAttributes() {
+ return mAudioAttributes;
+ }
+
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dsd) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+ // TODO: Implement this
+ }
+
+ @Override
+ public DataSourceDesc getCurrentDataSource() {
+ // TODO: Implement this
+ return null;
+ }
+
+ @Override
+ public void loopCurrent(boolean loop) {
+ // TODO: implement this
+ }
+
+ @Override
+ public void setPlaybackSpeed(float speed) {
+ mSetPlaybackSpeedCalled = true;
+ mPlaybackSpeed = speed;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void setPlayerVolume(float volume) {
+ // TODO: implement this
+ }
+
+ @Override
+ public float getPlayerVolume() {
+ // TODO: implement this
+ return -1;
+ }
+}
diff --git a/androidx/media/MockPlaylistAgent.java b/androidx/media/MockPlaylistAgent.java
new file mode 100644
index 00000000..2f9d70a2
--- /dev/null
+++ b/androidx/media/MockPlaylistAgent.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A mock implementation of {@link MediaPlaylistAgent} for testing.
+ * <p>
+ * Do not use mockito for {@link MediaPlaylistAgent}. Instead, use this.
+ * Mocks created from mockito should not be shared across different threads.
+ */
+public class MockPlaylistAgent extends MediaPlaylistAgent {
+ public final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+
+ public List<MediaItem2> mPlaylist;
+ public MediaMetadata2 mMetadata;
+ public MediaItem2 mCurrentMediaItem;
+ public MediaItem2 mItem;
+ public int mIndex = -1;
+ public @RepeatMode int mRepeatMode = -1;
+ public @ShuffleMode int mShuffleMode = -1;
+
+ public boolean mSetPlaylistCalled;
+ public boolean mUpdatePlaylistMetadataCalled;
+ public boolean mAddPlaylistItemCalled;
+ public boolean mRemovePlaylistItemCalled;
+ public boolean mReplacePlaylistItemCalled;
+ public boolean mSkipToPlaylistItemCalled;
+ public boolean mSkipToPreviousItemCalled;
+ public boolean mSkipToNextItemCalled;
+ public boolean mSetRepeatModeCalled;
+ public boolean mSetShuffleModeCalled;
+
+ @Override
+ public List<MediaItem2> getPlaylist() {
+ return mPlaylist;
+ }
+
+ @Override
+ public void setPlaylist(List<MediaItem2> list, MediaMetadata2 metadata) {
+ mSetPlaylistCalled = true;
+ mPlaylist = list;
+ mMetadata = metadata;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mMetadata;
+ }
+
+ @Override
+ public void updatePlaylistMetadata(MediaMetadata2 metadata) {
+ mUpdatePlaylistMetadataCalled = true;
+ mMetadata = metadata;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ return mCurrentMediaItem;
+ }
+
+ @Override
+ public void addPlaylistItem(int index, MediaItem2 item) {
+ mAddPlaylistItemCalled = true;
+ mIndex = index;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void removePlaylistItem(MediaItem2 item) {
+ mRemovePlaylistItemCalled = true;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, MediaItem2 item) {
+ mReplacePlaylistItemCalled = true;
+ mIndex = index;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToPlaylistItem(MediaItem2 item) {
+ mSkipToPlaylistItemCalled = true;
+ mItem = item;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ mSkipToPreviousItemCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public void skipToNextItem() {
+ mSkipToNextItemCalled = true;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ @Override
+ public void setRepeatMode(int repeatMode) {
+ mSetRepeatModeCalled = true;
+ mRepeatMode = repeatMode;
+ mCountDownLatch.countDown();
+ }
+
+ @Override
+ public int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ @Override
+ public void setShuffleMode(int shuffleMode) {
+ mSetShuffleModeCalled = true;
+ mShuffleMode = shuffleMode;
+ mCountDownLatch.countDown();
+ }
+}
diff --git a/androidx/media/Rating2.java b/androidx/media/Rating2.java
new file mode 100644
index 00000000..8c81331e
--- /dev/null
+++ b/androidx/media/Rating2.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.ObjectsCompat;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * 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.
+ */
+// New version of Rating with following change
+// - Don't implement Parcelable for updatable support.
+public final class Rating2 {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({RATING_NONE, RATING_HEART, RATING_THUMB_UP_DOWN, RATING_3_STARS, RATING_4_STARS,
+ RATING_5_STARS, RATING_PERCENTAGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Style {}
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({RATING_3_STARS, RATING_4_STARS, RATING_5_STARS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StarStyle {}
+
+ /**
+ * Indicates a rating style is not supported. A Rating2 will never have this
+ * type, but can be used by other classes to indicate they do not support
+ * Rating2.
+ */
+ public static final 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 static final int RATING_HEART = 1;
+
+ /**
+ * A rating style for "thumb up" vs "thumb down".
+ */
+ public static final int RATING_THUMB_UP_DOWN = 2;
+
+ /**
+ * A rating style with 0 to 3 stars.
+ */
+ public static final int RATING_3_STARS = 3;
+
+ /**
+ * A rating style with 0 to 4 stars.
+ */
+ public static final int RATING_4_STARS = 4;
+
+ /**
+ * A rating style with 0 to 5 stars.
+ */
+ public static final int RATING_5_STARS = 5;
+
+ /**
+ * A rating style expressed as a percentage.
+ */
+ public static final int RATING_PERCENTAGE = 6;
+
+ private static final String TAG = "Rating2";
+
+ private static final float RATING_NOT_RATED = -1.0f;
+ private static final String KEY_STYLE = "android.media.rating2.style";
+ private static final String KEY_VALUE = "android.media.rating2.value";
+
+ private final int mRatingStyle;
+ private final float mRatingValue;
+
+ private Rating2(@Style int ratingStyle, float rating) {
+ mRatingStyle = ratingStyle;
+ mRatingValue = rating;
+ }
+
+ @Override
+ public String toString() {
+ return "Rating2:style=" + mRatingStyle + " rating="
+ + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Rating2)) {
+ return false;
+ }
+ Rating2 other = (Rating2) obj;
+ return mRatingStyle == other.mRatingStyle && mRatingValue == other.mRatingValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectsCompat.hash(mRatingStyle, mRatingValue);
+ }
+
+ /**
+ * Create an instance from bundle object, previously created by {@link #toBundle()}
+ *
+ * @param bundle bundle
+ * @return new Rating2 instance or {@code null} for error
+ */
+ public static Rating2 fromBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+ }
+
+ /**
+ * Return bundle for this object to share across the process.
+ * @return bundle of this object
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_STYLE, mRatingStyle);
+ bundle.putFloat(KEY_VALUE, mRatingValue);
+ return bundle;
+ }
+
+ /**
+ * Return a Rating2 instance with no rating.
+ * Create and return a new Rating2 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 Rating2 instance otherwise.
+ */
+ public static @Nullable Rating2 newUnratedRating(@Style 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 Rating2(ratingStyle, RATING_NOT_RATED);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Return a Rating2 instance with a heart-based rating.
+ * Create and return a new Rating2 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 Rating2 instance.
+ */
+ public static @Nullable Rating2 newHeartRating(boolean hasHeart) {
+ return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating2 instance with a thumb-based rating.
+ * Create and return a new Rating2 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 Rating2 instance.
+ */
+ public static @Nullable Rating2 newThumbRating(boolean thumbIsUp) {
+ return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating2 instance with a star-based rating.
+ * Create and return a new Rating2 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 Rating2 instance otherwise.
+ */
+ public static @Nullable Rating2 newStarRating(@StarStyle int starRatingStyle,
+ float starRating) {
+ float maxRating;
+ 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 Rating2(starRatingStyle, starRating);
+ }
+
+ /**
+ * Return a Rating2 instance with a percentage-based rating.
+ * Create and return a new Rating2 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 Rating2 instance otherwise.
+ */
+ public static @Nullable Rating2 newPercentageRating(float percent) {
+ if ((percent < 0.0f) || (percent > 100.0f)) {
+ Log.e(TAG, "Invalid percentage-based rating value");
+ return null;
+ } else {
+ return new Rating2(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 @Style 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() {
+ return mRatingStyle == RATING_HEART && 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() {
+ return mRatingStyle == RATING_THUMB_UP_DOWN && 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;
+ }
+ // Fall through
+ 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;
+ }
+ }
+}
diff --git a/androidx/media/SessionCommand2.java b/androidx/media/SessionCommand2.java
new file mode 100644
index 00000000..f0179412
--- /dev/null
+++ b/androidx/media/SessionCommand2.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.media.MediaSession2.ControllerInfo;
+import androidx.media.MediaSession2.SessionCallback;
+
+import java.util.List;
+
+/**
+ * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
+ * <p>
+ * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
+ * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
+ * {@link #getCustomCommand()} shouldn't be {@code null}.
+ */
+public final class SessionCommand2 {
+ /**
+ * Command code for the custom command which can be defined by string action in the
+ * {@link SessionCommand2}.
+ */
+ public static final int COMMAND_CODE_CUSTOM = 0;
+
+ /**
+ * Command code for {@link MediaController2#play()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_PLAY = 1;
+
+ /**
+ * Command code for {@link MediaController2#pause()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
+
+ /**
+ * Command code for {@link MediaController2#reset()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_RESET = 3;
+
+ /**
+ * Command code for {@link MediaController2#skipToNextItem()}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM = 4;
+
+ /**
+ * Command code for {@link MediaController2#skipToPreviousItem()}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the {@link SessionCallback#onCommandRequest(
+ * MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM = 5;
+
+ /**
+ * Command code for {@link MediaController2#prepare()}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+
+ /**
+ * Command code for {@link MediaController2#fastForward()}.
+ */
+ public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 7;
+
+ /**
+ * Command code for {@link MediaController2#rewind()}.
+ */
+ public static final int COMMAND_CODE_SESSION_REWIND = 8;
+
+ /**
+ * Command code for {@link MediaController2#seekTo(long)}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+
+ /**
+ * Command code for both {@link MediaController2#setVolumeTo(int, int)}.
+ * <p>
+ * Command would set the device volume or send to the volume provider directly if the session
+ * doesn't reject the request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_VOLUME_SET_VOLUME = 10;
+
+ /**
+ * Command code for both {@link MediaController2#adjustVolume(int, int)}.
+ * <p>
+ * Command would adjust the device volume or send to the volume provider directly if the session
+ * doesn't reject the request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_VOLUME_ADJUST_VOLUME = 11;
+
+ /**
+ * Command code for {@link MediaController2#skipToPlaylistItem(MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM = 12;
+
+ /**
+ * Command code for {@link MediaController2#setShuffleMode(int)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE = 13;
+
+ /**
+ * Command code for {@link MediaController2#setRepeatMode(int)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE = 14;
+
+ /**
+ * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_ADD_ITEM = 15;
+
+ /**
+ * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_REMOVE_ITEM = 16;
+
+ /**
+ * Command code for {@link MediaController2#replacePlaylistItem(int, MediaItem2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_REPLACE_ITEM = 17;
+
+ /**
+ * Command code for {@link MediaController2#getPlaylist()}. This will expose metadata
+ * information to the controller.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18;
+
+ /**
+ * Command code for {@link MediaController2#setPlaylist(List, MediaMetadata2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_LIST = 19;
+
+ /**
+ * Command code for {@link MediaController2#getPlaylistMetadata()}. This will expose
+ * metadata information to the controller.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20;
+
+ /**
+ * Command code for {@link MediaController2#updatePlaylistMetadata(MediaMetadata2)}.
+ * <p>
+ * Command would be sent directly to the playlist agent if the session doesn't reject the
+ * request through the
+ * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21;
+
+ /**
+ * Command code for {@link MediaController2#getCurrentMediaItem()}. This will expose
+ * metadata information to the controller.
+ */
+ public static final int COMMAND_CODE_PLAYLIST_GET_CURRENT_MEDIA_ITEM = 20;
+
+ /**
+ * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22;
+
+ /**
+ * Command code for {@link MediaController2#playFromUri(Uri, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 23;
+
+ /**
+ * Command code for {@link MediaController2#playFromSearch(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 24;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 25;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 26;
+
+ /**
+ * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}.
+ */
+ public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 27;
+
+ /**
+ * Command code for {@link MediaController2#setRating(String, Rating2)}.
+ */
+ public static final int COMMAND_CODE_SESSION_SET_RATING = 28;
+
+ /**
+ * Command code for {@link MediaController2#subscribeRoutesInfo()}
+ */
+ public static final int COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO = 36;
+
+ /**
+ * Command code for {@link MediaController2#unsubscribeRoutesInfo()}
+ */
+ public static final int COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO = 37;
+
+ /**
+ * Command code for {@link MediaController2#selectRoute(Bundle)}}
+ */
+ public static final int COMMAND_CODE_SESSION_SELECT_ROUTE = 38;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#getItem(String)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#getLibraryRoot(Bundle)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#search(String, Bundle)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#subscribe(String, Bundle)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
+
+ /**
+ * @hide
+ * Command code for {@link MediaBrowser2#unsubscribe(String)}.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
+
+ /**
+ * Command code for {@link MediaController2#setPlaybackSpeed(float)}}.
+ * <p>
+ * Command would be sent directly to the player if the session doesn't reject the request
+ * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+ * SessionCommand2)}.
+ */
+ public static final int COMMAND_CODE_PLAYBACK_SET_SPEED = 39;
+
+ private static final String KEY_COMMAND_CODE =
+ "android.media.media_session2.command.command_code";
+ private static final String KEY_COMMAND_CUSTOM_COMMAND =
+ "android.media.media_session2.command.custom_command";
+ private static final String KEY_COMMAND_EXTRAS =
+ "android.media.media_session2.command.extras";
+
+ private final int mCommandCode;
+ // Nonnull if it's custom command
+ private final String mCustomCommand;
+ private final Bundle mExtras;
+
+ /**
+ * Constructor for creating a predefined command.
+ *
+ * @param commandCode A command code for predefined command.
+ */
+ public SessionCommand2(int commandCode) {
+ if (commandCode == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM");
+ }
+ mCommandCode = commandCode;
+ mCustomCommand = null;
+ mExtras = null;
+ }
+
+ /**
+ * Constructor for creating a custom command.
+ *
+ * @param action The action of this custom command.
+ * @param extras An extra bundle for this custom command.
+ */
+ public SessionCommand2(@NonNull String action, @Nullable Bundle extras) {
+ if (action == null) {
+ throw new IllegalArgumentException("action shouldn't be null");
+ }
+ mCommandCode = COMMAND_CODE_CUSTOM;
+ mCustomCommand = action;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the command code of a predefined command.
+ * This will return {@link #COMMAND_CODE_CUSTOM} for a custom command.
+ */
+ public int getCommandCode() {
+ return mCommandCode;
+ }
+
+ /**
+ * Gets the action of a custom command.
+ * This will return {@code null} for a predefined command.
+ */
+ public @Nullable String getCustomCommand() {
+ return mCustomCommand;
+ }
+
+ /**
+ * Gets the extra bundle of a custom command.
+ * This will return {@code null} for a predefined command.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * @return a new {@link Bundle} instance from the command
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
+ bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
+ bundle.putBundle(KEY_COMMAND_EXTRAS, mExtras);
+ return bundle;
+ }
+
+ /**
+ * @return a new {@link SessionCommand2} instance from the Bundle
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static SessionCommand2 fromBundle(@NonNull Bundle command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ int code = command.getInt(KEY_COMMAND_CODE);
+ if (code != COMMAND_CODE_CUSTOM) {
+ return new SessionCommand2(code);
+ } else {
+ String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
+ if (customCommand == null) {
+ return null;
+ }
+ return new SessionCommand2(customCommand, command.getBundle(KEY_COMMAND_EXTRAS));
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SessionCommand2)) {
+ return false;
+ }
+ SessionCommand2 other = (SessionCommand2) obj;
+ return mCommandCode == other.mCommandCode
+ && TextUtils.equals(mCustomCommand, other.mCustomCommand);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
+ }
+}
diff --git a/androidx/media/SessionCommandGroup2.java b/androidx/media/SessionCommandGroup2.java
new file mode 100644
index 00000000..691eb70a
--- /dev/null
+++ b/androidx/media/SessionCommandGroup2.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.media.SessionCommand2.COMMAND_CODE_CUSTOM;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A set of {@link SessionCommand2} which represents a command group.
+ */
+public final class SessionCommandGroup2 {
+
+ private static final String TAG = "SessionCommandGroup2";
+ private static final String KEY_COMMANDS = "android.media.mediasession2.commandgroup.commands";
+ // Prefix for all command codes
+ private static final String PREFIX_COMMAND_CODE = "COMMAND_CODE_";
+ // Prefix for command codes that will be sent directly to the MediaPlayerBase
+ private static final String PREFIX_COMMAND_CODE_PLAYBACK = "COMMAND_CODE_PLAYBACK_";
+ // Prefix for command codes that will be sent directly to the MediaPlaylistAgent
+ private static final String PREFIX_COMMAND_CODE_PLAYLIST = "COMMAND_CODE_PLAYLIST_";
+ // Prefix for command codes that will be sent directly to AudioManager or VolumeProvider.
+ private static final String PREFIX_COMMAND_CODE_VOLUME = "COMMAND_CODE_VOLUME_";
+
+ private Set<SessionCommand2> mCommands = new HashSet<>();
+
+ /**
+ * Default Constructor.
+ */
+ public SessionCommandGroup2() { }
+
+ /**
+ * Creates a new SessionCommandGroup2 with commands copied from another object.
+ *
+ * @param other The SessionCommandGroup2 instance to copy.
+ */
+ public SessionCommandGroup2(@Nullable SessionCommandGroup2 other) {
+ if (other != null) {
+ mCommands.addAll(other.mCommands);
+ }
+ }
+
+ /**
+ * Adds a command to this command group.
+ *
+ * @param command A command to add. Shouldn't be {@code null}.
+ */
+ public void addCommand(@NonNull SessionCommand2 command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mCommands.add(command);
+ }
+
+ /**
+ * Adds a predefined command with given {@code commandCode} to this command group.
+ *
+ * @param commandCode A command code to add.
+ * Shouldn't be {@link SessionCommand2#COMMAND_CODE_CUSTOM}.
+ */
+ public void addCommand(int commandCode) {
+ if (commandCode == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mCommands.add(new SessionCommand2(commandCode));
+ }
+
+ /**
+ * Adds all predefined commands to this command group.
+ */
+ public void addAllPredefinedCommands() {
+ addCommandsWithPrefix(PREFIX_COMMAND_CODE);
+ }
+
+ void addAllPlaybackCommands() {
+ addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYBACK);
+ }
+
+ void addAllPlaylistCommands() {
+ addCommandsWithPrefix(PREFIX_COMMAND_CODE_PLAYLIST);
+ }
+
+ void addAllVolumeCommands() {
+ addCommandsWithPrefix(PREFIX_COMMAND_CODE_VOLUME);
+ }
+
+ private void addCommandsWithPrefix(String prefix) {
+ final Field[] fields = SessionCommand2.class.getFields();
+ if (fields != null) {
+ for (int i = 0; i < fields.length; i++) {
+ if (fields[i].getName().startsWith(prefix)
+ && !fields[i].getName().equals("COMMAND_CODE_CUSTOM")) {
+ try {
+ mCommands.add(new SessionCommand2(fields[i].getInt(null)));
+ } catch (IllegalAccessException e) {
+ Log.w(TAG, "Unexpected " + fields[i] + " in MediaSession2");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a command from this group which matches given {@code command}.
+ *
+ * @param command A command to find. Shouldn't be {@code null}.
+ */
+ public void removeCommand(@NonNull SessionCommand2 command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ mCommands.remove(command);
+ }
+
+ /**
+ * Removes a command from this group which matches given {@code commandCode}.
+ *
+ * @param commandCode A command code to find.
+ * Shouldn't be {@link SessionCommand2#COMMAND_CODE_CUSTOM}.
+ */
+ public void removeCommand(int commandCode) {
+ if (commandCode == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("commandCode shouldn't be COMMAND_CODE_CUSTOM");
+ }
+ mCommands.remove(new SessionCommand2(commandCode));
+ }
+
+ /**
+ * Checks whether this command group has a command that matches given {@code command}.
+ *
+ * @param command A command to find. Shouldn't be {@code null}.
+ */
+ public boolean hasCommand(@NonNull SessionCommand2 command) {
+ if (command == null) {
+ throw new IllegalArgumentException("command shouldn't be null");
+ }
+ return mCommands.contains(command);
+ }
+
+ /**
+ * Checks whether this command group has a command that matches given {@code commandCode}.
+ *
+ * @param commandCode A command code to find.
+ * Shouldn't be {@link SessionCommand2#COMMAND_CODE_CUSTOM}.
+ */
+ public boolean hasCommand(int commandCode) {
+ if (commandCode == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
+ }
+ for (SessionCommand2 command : mCommands) {
+ if (command.getCommandCode() == commandCode) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets all commands of this command group.
+ */
+ public @NonNull Set<SessionCommand2> getCommands() {
+ return new HashSet<>(mCommands);
+ }
+
+ /**
+ * @return A new {@link Bundle} instance from the SessionCommandGroup2.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public @NonNull Bundle toBundle() {
+ ArrayList<Bundle> list = new ArrayList<>();
+ for (SessionCommand2 command : mCommands) {
+ list.add(command.toBundle());
+ }
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(KEY_COMMANDS, list);
+ return bundle;
+ }
+
+ /**
+ * @return A new {@link SessionCommandGroup2} instance from the bundle.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static @Nullable SessionCommandGroup2 fromBundle(Bundle commands) {
+ if (commands == null) {
+ return null;
+ }
+ List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
+ if (list == null) {
+ return null;
+ }
+ SessionCommandGroup2 commandGroup = new SessionCommandGroup2();
+ for (int i = 0; i < list.size(); i++) {
+ Parcelable parcelable = list.get(i);
+ if (!(parcelable instanceof Bundle)) {
+ continue;
+ }
+ Bundle commandBundle = (Bundle) parcelable;
+ SessionCommand2 command = SessionCommand2.fromBundle(commandBundle);
+ if (command != null) {
+ commandGroup.addCommand(command);
+ }
+ }
+ return commandGroup;
+ }
+}
diff --git a/androidx/media/SessionPlaylistAgentImplBase.java b/androidx/media/SessionPlaylistAgentImplBase.java
new file mode 100644
index 00000000..431b188e
--- /dev/null
+++ b/androidx/media/SessionPlaylistAgentImplBase.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.util.ArrayMap;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.media.MediaPlayerBase.PlayerEventCallback;
+import androidx.media.MediaSession2.OnDataSourceMissingHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@TargetApi(Build.VERSION_CODES.KITKAT)
+class SessionPlaylistAgentImplBase extends MediaPlaylistAgent {
+ @VisibleForTesting
+ static final int END_OF_PLAYLIST = -1;
+ @VisibleForTesting
+ static final int NO_VALID_ITEMS = -2;
+
+ private final PlayItem mEopPlayItem = new PlayItem(END_OF_PLAYLIST, null);
+
+ private final Object mLock = new Object();
+ private final MediaSession2ImplBase mSession;
+ private final MyPlayerEventCallback mPlayerCallback;
+
+ @GuardedBy("mLock")
+ private MediaPlayerBase mPlayer;
+ @GuardedBy("mLock")
+ private OnDataSourceMissingHelper mDsmHelper;
+ // TODO: Check if having the same item is okay (b/74090741)
+ @GuardedBy("mLock")
+ private ArrayList<MediaItem2> mPlaylist = new ArrayList<>();
+ @GuardedBy("mLock")
+ private ArrayList<MediaItem2> mShuffledList = new ArrayList<>();
+ @GuardedBy("mLock")
+ private Map<MediaItem2, DataSourceDesc> mItemDsdMap = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private MediaMetadata2 mMetadata;
+ @GuardedBy("mLock")
+ private int mRepeatMode;
+ @GuardedBy("mLock")
+ private int mShuffleMode;
+ @GuardedBy("mLock")
+ private PlayItem mCurrent;
+
+ // Called on session callback executor.
+ private class MyPlayerEventCallback extends PlayerEventCallback {
+ @Override
+ public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+ @Nullable DataSourceDesc dsd) {
+ synchronized (mLock) {
+ if (mPlayer != mpb) {
+ return;
+ }
+ if (dsd == null && mCurrent != null) {
+ mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+ updateCurrentIfNeededLocked();
+ }
+ }
+ }
+ }
+
+ private class PlayItem {
+ public int shuffledIdx;
+ public DataSourceDesc dsd;
+ public MediaItem2 mediaItem;
+
+ PlayItem(int shuffledIdx) {
+ this(shuffledIdx, null);
+ }
+
+ PlayItem(int shuffledIdx, DataSourceDesc dsd) {
+ this.shuffledIdx = shuffledIdx;
+ if (shuffledIdx >= 0) {
+ this.mediaItem = mShuffledList.get(shuffledIdx);
+ if (dsd == null) {
+ synchronized (mLock) {
+ this.dsd = retrieveDataSourceDescLocked(this.mediaItem);
+ }
+ } else {
+ this.dsd = dsd;
+ }
+ }
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ boolean isValid() {
+ if (this == mEopPlayItem) {
+ return true;
+ }
+ if (mediaItem == null) {
+ return false;
+ }
+ if (dsd == null) {
+ return false;
+ }
+ if (mediaItem.getDataSourceDesc() != null
+ && !mediaItem.getDataSourceDesc().equals(dsd)) {
+ return false;
+ }
+ synchronized (mLock) {
+ if (shuffledIdx >= mShuffledList.size()) {
+ return false;
+ }
+ if (mediaItem != mShuffledList.get(shuffledIdx)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ SessionPlaylistAgentImplBase(@NonNull MediaSession2ImplBase session,
+ @NonNull MediaPlayerBase player) {
+ super();
+ if (session == null) {
+ throw new IllegalArgumentException("sessionImpl shouldn't be null");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mSession = session;
+ mPlayer = player;
+ mPlayerCallback = new MyPlayerEventCallback();
+ mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback);
+ }
+
+ public void setPlayer(@NonNull MediaPlayerBase player) {
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (player == mPlayer) {
+ return;
+ }
+ mPlayer.unregisterPlayerEventCallback(mPlayerCallback);
+ mPlayer = player;
+ mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback);
+ updatePlayerDataSourceLocked();
+ }
+ }
+
+ public void setOnDataSourceMissingHelper(OnDataSourceMissingHelper helper) {
+ synchronized (mLock) {
+ mDsmHelper = helper;
+ }
+ }
+
+ public void clearOnDataSourceMissingHelper() {
+ synchronized (mLock) {
+ mDsmHelper = null;
+ }
+ }
+
+ @Override
+ public @Nullable List<MediaItem2> getPlaylist() {
+ synchronized (mLock) {
+ return Collections.unmodifiableList(mPlaylist);
+ }
+ }
+
+ @Override
+ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+ if (list == null) {
+ throw new IllegalArgumentException("list shouldn't be null");
+ }
+
+ synchronized (mLock) {
+ mItemDsdMap.clear();
+
+ mPlaylist.clear();
+ mPlaylist.addAll(list);
+ applyShuffleModeLocked();
+
+ mMetadata = metadata;
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ updatePlayerDataSourceLocked();
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public @Nullable MediaMetadata2 getPlaylistMetadata() {
+ synchronized (mLock) {
+ return mMetadata;
+ }
+ }
+
+ @Override
+ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+ synchronized (mLock) {
+ if (metadata == mMetadata) {
+ return;
+ }
+ mMetadata = metadata;
+ }
+ notifyPlaylistMetadataChanged();
+ }
+
+ @Override
+ public MediaItem2 getCurrentMediaItem() {
+ synchronized (mLock) {
+ return mCurrent == null ? null : mCurrent.mediaItem;
+ }
+ }
+
+ @Override
+ public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ index = clamp(index, mPlaylist.size());
+ int shuffledIdx = index;
+ mPlaylist.add(index, item);
+ if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_NONE) {
+ mShuffledList.add(index, item);
+ } else {
+ // Add the item in random position of mShuffledList.
+ shuffledIdx = (int) (Math.random() * (mShuffledList.size() + 1));
+ mShuffledList.add(shuffledIdx, item);
+ }
+ if (!hasValidItem()) {
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ updatePlayerDataSourceLocked();
+ } else {
+ updateCurrentIfNeededLocked();
+ }
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public void removePlaylistItem(@NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (!mPlaylist.remove(item)) {
+ return;
+ }
+ mShuffledList.remove(item);
+ mItemDsdMap.remove(item);
+ updateCurrentIfNeededLocked();
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (mPlaylist.size() <= 0) {
+ return;
+ }
+ index = clamp(index, mPlaylist.size() - 1);
+ int shuffledIdx = mShuffledList.indexOf(mPlaylist.get(index));
+ mItemDsdMap.remove(mShuffledList.get(shuffledIdx));
+ mShuffledList.set(shuffledIdx, item);
+ mPlaylist.set(index, item);
+ if (!hasValidItem()) {
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ updatePlayerDataSourceLocked();
+ } else {
+ updateCurrentIfNeededLocked();
+ }
+ }
+ notifyPlaylistChanged();
+ }
+
+ @Override
+ public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+ if (item == null) {
+ throw new IllegalArgumentException("item shouldn't be null");
+ }
+ synchronized (mLock) {
+ if (!hasValidItem() || item.equals(mCurrent.mediaItem)) {
+ return;
+ }
+ int shuffledIdx = mShuffledList.indexOf(item);
+ if (shuffledIdx < 0) {
+ return;
+ }
+ mCurrent = new PlayItem(shuffledIdx);
+ updateCurrentIfNeededLocked();
+ }
+ }
+
+ @Override
+ public void skipToPreviousItem() {
+ synchronized (mLock) {
+ if (!hasValidItem()) {
+ return;
+ }
+ PlayItem prev = getNextValidPlayItemLocked(mCurrent.shuffledIdx, -1);
+ if (prev != mEopPlayItem) {
+ mCurrent = prev;
+ }
+ updateCurrentIfNeededLocked();
+ }
+ }
+
+ @Override
+ public void skipToNextItem() {
+ synchronized (mLock) {
+ if (!hasValidItem() || mCurrent == mEopPlayItem) {
+ return;
+ }
+ PlayItem next = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+ if (next != mEopPlayItem) {
+ mCurrent = next;
+ }
+ updateCurrentIfNeededLocked();
+ }
+ }
+
+ @Override
+ public int getRepeatMode() {
+ synchronized (mLock) {
+ return mRepeatMode;
+ }
+ }
+
+ @Override
+ @SuppressWarnings("FallThrough")
+ public void setRepeatMode(int repeatMode) {
+ if (repeatMode < MediaPlaylistAgent.REPEAT_MODE_NONE
+ || repeatMode > MediaPlaylistAgent.REPEAT_MODE_GROUP) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mRepeatMode == repeatMode) {
+ return;
+ }
+ mRepeatMode = repeatMode;
+ switch (repeatMode) {
+ case MediaPlaylistAgent.REPEAT_MODE_ONE:
+ if (mCurrent != null && mCurrent != mEopPlayItem) {
+ mPlayer.loopCurrent(true);
+ }
+ break;
+ case MediaPlaylistAgent.REPEAT_MODE_ALL:
+ case MediaPlaylistAgent.REPEAT_MODE_GROUP:
+ if (mCurrent == mEopPlayItem) {
+ mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+ updatePlayerDataSourceLocked();
+ }
+ // Fall through
+ case MediaPlaylistAgent.REPEAT_MODE_NONE:
+ mPlayer.loopCurrent(false);
+ break;
+ }
+ }
+ notifyRepeatModeChanged();
+ }
+
+ @Override
+ public int getShuffleMode() {
+ synchronized (mLock) {
+ return mShuffleMode;
+ }
+ }
+
+ @Override
+ public void setShuffleMode(int shuffleMode) {
+ if (shuffleMode < MediaPlaylistAgent.SHUFFLE_MODE_NONE
+ || shuffleMode > MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mShuffleMode == shuffleMode) {
+ return;
+ }
+ mShuffleMode = shuffleMode;
+ applyShuffleModeLocked();
+ updateCurrentIfNeededLocked();
+ }
+ notifyShuffleModeChanged();
+ }
+
+ @Override
+ public MediaItem2 getMediaItem(DataSourceDesc dsd) {
+ // TODO: implement this
+ return null;
+ }
+
+ @VisibleForTesting
+ int getCurShuffledIndex() {
+ synchronized (mLock) {
+ return hasValidItem() ? mCurrent.shuffledIdx : NO_VALID_ITEMS;
+ }
+ }
+
+ private boolean hasValidItem() {
+ synchronized (mLock) {
+ return mCurrent != null;
+ }
+ }
+
+ @SuppressWarnings("GuardedBy")
+ private DataSourceDesc retrieveDataSourceDescLocked(MediaItem2 item) {
+ DataSourceDesc dsd = item.getDataSourceDesc();
+ if (dsd != null) {
+ mItemDsdMap.put(item, dsd);
+ return dsd;
+ }
+ dsd = mItemDsdMap.get(item);
+ if (dsd != null) {
+ return dsd;
+ }
+ OnDataSourceMissingHelper helper = mDsmHelper;
+ if (helper != null) {
+ // TODO: Do not call onDataSourceMissing with the lock (b/74090741).
+ dsd = helper.onDataSourceMissing(mSession.getInstance(), item);
+ if (dsd != null) {
+ mItemDsdMap.put(item, dsd);
+ }
+ }
+ return dsd;
+ }
+
+ // TODO: consider to call updateCurrentIfNeededLocked inside (b/74090741)
+ @SuppressWarnings("GuardedBy")
+ private PlayItem getNextValidPlayItemLocked(int curShuffledIdx, int direction) {
+ int size = mPlaylist.size();
+ if (curShuffledIdx == END_OF_PLAYLIST) {
+ curShuffledIdx = (direction > 0) ? -1 : size;
+ }
+ for (int i = 0; i < size; i++) {
+ curShuffledIdx += direction;
+ if (curShuffledIdx < 0 || curShuffledIdx >= mPlaylist.size()) {
+ if (mRepeatMode == REPEAT_MODE_NONE) {
+ return (i == size - 1) ? null : mEopPlayItem;
+ } else {
+ curShuffledIdx = curShuffledIdx < 0 ? mPlaylist.size() - 1 : 0;
+ }
+ }
+ DataSourceDesc dsd = retrieveDataSourceDescLocked(mShuffledList.get(curShuffledIdx));
+ if (dsd != null) {
+ return new PlayItem(curShuffledIdx, dsd);
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("GuardedBy")
+ private void updateCurrentIfNeededLocked() {
+ if (!hasValidItem() || mCurrent.isValid()) {
+ return;
+ }
+ int shuffledIdx = mShuffledList.indexOf(mCurrent.mediaItem);
+ if (shuffledIdx >= 0) {
+ // Added an item.
+ mCurrent.shuffledIdx = shuffledIdx;
+ return;
+ }
+
+ if (mCurrent.shuffledIdx >= mShuffledList.size()) {
+ mCurrent = getNextValidPlayItemLocked(mShuffledList.size() - 1, 1);
+ } else {
+ mCurrent.mediaItem = mShuffledList.get(mCurrent.shuffledIdx);
+ if (retrieveDataSourceDescLocked(mCurrent.mediaItem) == null) {
+ mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+ }
+ }
+ updatePlayerDataSourceLocked();
+ return;
+ }
+
+ @SuppressWarnings("GuardedBy")
+ private void updatePlayerDataSourceLocked() {
+ if (mCurrent == null || mCurrent == mEopPlayItem) {
+ return;
+ }
+ if (mPlayer.getCurrentDataSource() != mCurrent.dsd) {
+ mPlayer.setDataSource(mCurrent.dsd);
+ mPlayer.loopCurrent(mRepeatMode == MediaPlaylistAgent.REPEAT_MODE_ONE);
+ }
+ // TODO: Call setNextDataSource (b/74090741)
+ }
+
+ @SuppressWarnings("GuardedBy")
+ private void applyShuffleModeLocked() {
+ mShuffledList.clear();
+ mShuffledList.addAll(mPlaylist);
+ if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_ALL
+ || mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+ Collections.shuffle(mShuffledList);
+ }
+ }
+
+ // Clamps value to [0, size]
+ private static int clamp(int value, int size) {
+ if (value < 0) {
+ return 0;
+ }
+ return (value > size) ? size : value;
+ }
+}
diff --git a/androidx/media/SessionToken2.java b/androidx/media/SessionToken2.java
new file mode 100644
index 00000000..eb422973
--- /dev/null
+++ b/androidx/media/SessionToken2.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.text.TextUtils;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Represents an ongoing {@link MediaSession2}.
+ * <p>
+ * This may be passed to apps by the session owner to allow them to create a
+ * {@link MediaController2} to communicate with the session.
+ * <p>
+ * It can be also obtained by {@link MediaSessionManager}.
+ */
+// New version of MediaSession.Token for following reasons
+// - Stop implementing Parcelable for updatable support
+// - Represent session and library service (formerly browser service) in one class.
+// Previously MediaSession.Token was for session and ComponentName was for service.
+public final class SessionToken2 {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
+ public @interface TokenType {
+ }
+
+ /**
+ * Type for {@link MediaSession2}.
+ */
+ public static final int TYPE_SESSION = 0;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int TYPE_SESSION_SERVICE = 1;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int TYPE_LIBRARY_SERVICE = 2;
+
+ //private final SessionToken2Provider mProvider;
+
+ // From the return value of android.os.Process.getUidForName(String) when error
+ private static final int UID_UNKNOWN = -1;
+
+ private static final String KEY_UID = "android.media.token.uid";
+ private static final String KEY_TYPE = "android.media.token.type";
+ private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
+ private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
+ private static final String KEY_ID = "android.media.token.id";
+ private static final String KEY_SESSION_TOKEN = "android.media.token.session_token";
+
+ private final int mUid;
+ private final @TokenType int mType;
+ private final String mPackageName;
+ private final String mServiceName;
+ private final String mId;
+ private final MediaSessionCompat.Token mSessionCompatToken;
+ private final ComponentName mComponentName;
+
+ /**
+ * @hide
+ * Constructor for the token. You can only create token for session service or library service
+ * to use by {@link MediaController2} or {@link MediaBrowser2}.
+ *
+ * @param context The context.
+ * @param serviceComponent The component name of the media browser service.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent) {
+ this(context, serviceComponent, UID_UNKNOWN);
+ }
+
+ /**
+ * Constructor for the token. You can only create token for session service or library service
+ * to use by {@link MediaController2} or {@link MediaBrowser2}.
+ *
+ * @param context The context.
+ * @param serviceComponent The component name of the media browser service.
+ * @param uid uid of the app.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public SessionToken2(@NonNull Context context, @NonNull ComponentName serviceComponent,
+ int uid) {
+ if (serviceComponent == null) {
+ throw new IllegalArgumentException("serviceComponent shouldn't be null");
+ }
+ mComponentName = serviceComponent;
+ mPackageName = serviceComponent.getPackageName();
+ mServiceName = serviceComponent.getClassName();
+ // Calculate uid if it's not specified.
+ final PackageManager manager = context.getPackageManager();
+ if (uid < 0) {
+ try {
+ uid = manager.getApplicationInfo(mPackageName, 0).uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new IllegalArgumentException("Cannot find package " + mPackageName);
+ }
+ }
+ mUid = uid;
+
+ // Infer id and type from package name and service name
+ String id = getSessionIdFromService(manager, MediaLibraryService2.SERVICE_INTERFACE,
+ serviceComponent);
+ if (id != null) {
+ mId = id;
+ mType = TYPE_LIBRARY_SERVICE;
+ } else {
+ // retry with session service
+ mId = getSessionIdFromService(manager, MediaSessionService2.SERVICE_INTERFACE,
+ serviceComponent);
+ mType = TYPE_SESSION_SERVICE;
+ }
+ if (mId == null) {
+ throw new IllegalArgumentException("service " + mServiceName + " doesn't implement"
+ + " session service nor library service. Use service's full name.");
+ }
+ mSessionCompatToken = null;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ SessionToken2(int uid, int type, String packageName, String serviceName,
+ String id, MediaSessionCompat.Token sessionCompatToken) {
+ mUid = uid;
+ mType = type;
+ mPackageName = packageName;
+ mServiceName = serviceName;
+ mComponentName = (mType == TYPE_SESSION) ? null
+ : new ComponentName(packageName, serviceName);
+ mId = id;
+ mSessionCompatToken = sessionCompatToken;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ return mType
+ + prime * (mUid
+ + prime * (mPackageName.hashCode()
+ + prime * (mId.hashCode()
+ + prime * (mServiceName != null ? mServiceName.hashCode() : 0))));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SessionToken2)) {
+ return false;
+ }
+ SessionToken2 other = (SessionToken2) obj;
+ return mUid == other.mUid
+ && TextUtils.equals(mPackageName, other.mPackageName)
+ && TextUtils.equals(mServiceName, other.mServiceName)
+ && TextUtils.equals(mId, other.mId)
+ && mType == other.mType;
+ }
+
+ @Override
+ public String toString() {
+ return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+ + " service=" + mServiceName + " sessionCompatToken=" + mSessionCompatToken + "}";
+ }
+
+ /**
+ * @return uid of the session
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * @return package name
+ */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return service name. Can be {@code null} for TYPE_SESSION.
+ */
+ public @Nullable String getServiceName() {
+ return mServiceName;
+ }
+
+ /**
+ * @hide
+ * @return component name of this session token. Can be null for TYPE_SESSION.
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ /**
+ * @return id
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * @return type of the token
+ * @see #TYPE_SESSION
+ */
+ public @TokenType int getType() {
+ return mType;
+ }
+
+ /**
+ * Create a token from the bundle, exported by {@link #toBundle()}.
+ *
+ * @param bundle
+ * @return
+ */
+ public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final int uid = bundle.getInt(KEY_UID);
+ final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
+ final String packageName = bundle.getString(KEY_PACKAGE_NAME);
+ final String serviceName = bundle.getString(KEY_SERVICE_NAME);
+ final String id = bundle.getString(KEY_ID);
+ final MediaSessionCompat.Token token = bundle.getParcelable(KEY_SESSION_TOKEN);
+
+ // Sanity check.
+ switch (type) {
+ case TYPE_SESSION:
+ if (token == null) {
+ throw new IllegalArgumentException("Unexpected token for session,"
+ + " SessionCompat.Token=" + token);
+ }
+ break;
+ case TYPE_SESSION_SERVICE:
+ case TYPE_LIBRARY_SERVICE:
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalArgumentException("Session service needs service name");
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (TextUtils.isEmpty(packageName) || id == null) {
+ throw new IllegalArgumentException("Package name nor ID cannot be null.");
+ }
+ return new SessionToken2(uid, type, packageName, serviceName, id, token);
+ }
+
+ /**
+ * Create a {@link Bundle} from this token to share it across processes.
+ * @return Bundle
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_UID, mUid);
+ bundle.putString(KEY_PACKAGE_NAME, mPackageName);
+ bundle.putString(KEY_SERVICE_NAME, mServiceName);
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_TYPE, mType);
+ bundle.putParcelable(KEY_SESSION_TOKEN, mSessionCompatToken);
+ return bundle;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static String getSessionId(ResolveInfo resolveInfo) {
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ return null;
+ } else if (resolveInfo.serviceInfo.metaData == null) {
+ return "";
+ } else {
+ return resolveInfo.serviceInfo.metaData.getString(
+ MediaSessionService2.SERVICE_META_DATA, "");
+ }
+ }
+
+ MediaSessionCompat.Token getSessionCompatToken() {
+ return mSessionCompatToken;
+ }
+
+ private static String getSessionIdFromService(PackageManager manager, String serviceInterface,
+ ComponentName serviceComponent) {
+ Intent serviceIntent = new Intent(serviceInterface);
+ // Use queryIntentServices to find services with MediaLibraryService2.SERVICE_INTERFACE.
+ // We cannot use resolveService with intent specified class name, because resolveService
+ // ignores actions if Intent.setClassName() is specified.
+ serviceIntent.setPackage(serviceComponent.getPackageName());
+
+ List<ResolveInfo> list = manager.queryIntentServices(
+ serviceIntent, PackageManager.GET_META_DATA);
+ if (list != null) {
+ for (int i = 0; i < list.size(); i++) {
+ ResolveInfo resolveInfo = list.get(i);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ continue;
+ }
+ if (TextUtils.equals(
+ resolveInfo.serviceInfo.name, serviceComponent.getClassName())) {
+ return getSessionId(resolveInfo);
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/androidx/media/SessionToken2Test.java b/androidx/media/SessionToken2Test.java
new file mode 100644
index 00000000..22881a8f
--- /dev/null
+++ b/androidx/media/SessionToken2Test.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Process;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests {@link SessionToken2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SessionToken2Test {
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void testConstructor_sessionService() {
+ SessionToken2 token = new SessionToken2(mContext, new ComponentName(
+ mContext.getPackageName(),
+ MockMediaSessionService2.class.getCanonicalName()));
+ assertEquals(MockMediaSessionService2.ID, token.getId());
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(Process.myUid(), token.getUid());
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ }
+
+ @Test
+ public void testConstructor_libraryService() {
+ SessionToken2 token = new SessionToken2(mContext, new ComponentName(
+ mContext.getPackageName(),
+ MockMediaLibraryService2.class.getCanonicalName()));
+ assertEquals(MockMediaLibraryService2.ID, token.getId());
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(Process.myUid(), token.getUid());
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ }
+}
diff --git a/androidx/media/TestMedia2DataSource.java b/androidx/media/TestMedia2DataSource.java
new file mode 100644
index 00000000..c578beff
--- /dev/null
+++ b/androidx/media/TestMedia2DataSource.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import android.content.res.AssetFileDescriptor;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A Media2DataSource that reads from a byte array for use in tests.
+ */
+public class TestMedia2DataSource extends Media2DataSource {
+ private static final String TAG = "TestMedia2DataSource";
+
+ private byte[] mData;
+
+ private boolean mThrowFromReadAt;
+ private boolean mThrowFromGetSize;
+ private Integer mReturnFromReadAt;
+ private Long mReturnFromGetSize;
+ private boolean mIsClosed;
+
+ // Read an asset fd into a new byte array data source. Closes afd.
+ public static TestMedia2DataSource fromAssetFd(AssetFileDescriptor afd) throws IOException {
+ try {
+ InputStream in = afd.createInputStream();
+ final int size = (int) afd.getDeclaredLength();
+ byte[] data = new byte[(int) size];
+ int writeIndex = 0;
+ int numRead = 0;
+ do {
+ numRead = in.read(data, writeIndex, size - writeIndex);
+ writeIndex += numRead;
+ } while (numRead >= 0);
+ return new TestMedia2DataSource(data);
+ } finally {
+ afd.close();
+ }
+ }
+
+ public TestMedia2DataSource(byte[] data) {
+ mData = data;
+ }
+
+ @Override
+ public synchronized int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException {
+ if (mThrowFromReadAt) {
+ throw new IOException("Test exception from readAt()");
+ }
+ if (mReturnFromReadAt != null) {
+ return mReturnFromReadAt;
+ }
+
+ // Clamp reads past the end of the source.
+ if (position >= mData.length) {
+ return -1; // -1 indicates EOF
+ }
+ if (position + size > mData.length) {
+ size -= (position + size) - mData.length;
+ }
+ System.arraycopy(mData, (int) position, buffer, offset, size);
+ return size;
+ }
+
+ @Override
+ public synchronized long getSize() throws IOException {
+ if (mThrowFromGetSize) {
+ throw new IOException("Test exception from getSize()");
+ }
+ if (mReturnFromGetSize != null) {
+ return mReturnFromGetSize;
+ }
+
+ Log.v(TAG, "getSize: " + mData.length);
+ return mData.length;
+ }
+
+ // Note: it's fine to keep using this data source after closing it.
+ @Override
+ public synchronized void close() {
+ Log.v(TAG, "close()");
+ mIsClosed = true;
+ }
+
+ // Whether close() has been called.
+ public synchronized boolean isClosed() {
+ return mIsClosed;
+ }
+
+ public void throwFromReadAt() {
+ mThrowFromReadAt = true;
+ }
+
+ public void throwFromGetSize() {
+ mThrowFromGetSize = true;
+ }
+
+ public void returnFromReadAt(int numRead) {
+ mReturnFromReadAt = numRead;
+ }
+
+ public void returnFromGetSize(long size) {
+ mReturnFromGetSize = size;
+ }
+}
+
diff --git a/androidx/media/TestServiceRegistry.java b/androidx/media/TestServiceRegistry.java
new file mode 100644
index 00000000..38286aaa
--- /dev/null
+++ b/androidx/media/TestServiceRegistry.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+
+import androidx.annotation.GuardedBy;
+import androidx.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import androidx.media.TestUtils.SyncHandler;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+ @GuardedBy("TestServiceRegistry.class")
+ private static TestServiceRegistry sInstance;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaSessionService2 mService;
+ @GuardedBy("TestServiceRegistry.class")
+ private SyncHandler mHandler;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaLibrarySessionCallback mSessionCallback;
+ @GuardedBy("TestServiceRegistry.class")
+ private SessionServiceCallback mSessionServiceCallback;
+
+ /**
+ * Callback for session service's lifecyle (onCreate() / onDestroy())
+ */
+ public interface SessionServiceCallback {
+ void onCreated();
+ void onDestroyed();
+ }
+
+ public static TestServiceRegistry getInstance() {
+ synchronized (TestServiceRegistry.class) {
+ if (sInstance == null) {
+ sInstance = new TestServiceRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ public void setHandler(Handler handler) {
+ synchronized (TestServiceRegistry.class) {
+ mHandler = new SyncHandler(handler.getLooper());
+ }
+ }
+
+ public Handler getHandler() {
+ synchronized (TestServiceRegistry.class) {
+ return mHandler;
+ }
+ }
+
+ public void setSessionServiceCallback(SessionServiceCallback sessionServiceCallback) {
+ synchronized (TestServiceRegistry.class) {
+ mSessionServiceCallback = sessionServiceCallback;
+ }
+ }
+
+ public void setSessionCallback(MediaLibrarySessionCallback sessionCallback) {
+ synchronized (TestServiceRegistry.class) {
+ mSessionCallback = sessionCallback;
+ }
+ }
+
+ public MediaLibrarySessionCallback getSessionCallback() {
+ synchronized (TestServiceRegistry.class) {
+ return mSessionCallback;
+ }
+ }
+
+ public void setServiceInstance(MediaSessionService2 service) {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ fail("Previous service instance is still running. Clean up manually to ensure"
+ + " previoulsy running service doesn't break current test");
+ }
+ mService = service;
+ if (mSessionServiceCallback != null) {
+ mSessionServiceCallback.onCreated();
+ }
+ }
+ }
+
+ public MediaSessionService2 getServiceInstance() {
+ synchronized (TestServiceRegistry.class) {
+ return mService;
+ }
+ }
+
+ public void cleanUp() {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ // TODO(jaewan): Remove this, and override SessionService#onDestroy() to do this
+ mService.getSession().close();
+ // stopSelf() would not kill service while the binder connection established by
+ // bindService() exists, and close() above will do the job instead.
+ // So stopSelf() isn't really needed, but just for sure.
+ mService.stopSelf();
+ mService = null;
+ }
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ mSessionCallback = null;
+ if (mSessionServiceCallback != null) {
+ mSessionServiceCallback.onDestroyed();
+ mSessionServiceCallback = null;
+ }
+ }
+ }
+}
diff --git a/androidx/media/TestUtils.java b/androidx/media/TestUtils.java
new file mode 100644
index 00000000..1e3ba9bc
--- /dev/null
+++ b/androidx/media/TestUtils.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+ private static final int WAIT_TIME_MS = 1000;
+ private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+ /**
+ * Finds the session with id in this test package.
+ *
+ * @param context
+ * @param id
+ * @return
+ */
+ public static SessionToken2 getServiceToken(Context context, String id) {
+ switch (id) {
+ case MockMediaSessionService2.ID:
+ return new SessionToken2(context, new ComponentName(
+ context.getPackageName(), MockMediaSessionService2.class.getName()));
+ case MockMediaLibraryService2.ID:
+ return new SessionToken2(context, new ComponentName(
+ context.getPackageName(), MockMediaLibraryService2.class.getName()));
+ }
+ fail("Unknown id=" + id);
+ return null;
+ }
+
+ /**
+ * Compares contents of two bundles.
+ *
+ * @param a a bundle
+ * @param b another bundle
+ * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+ * incorrect if any bundle contains a bundle.
+ */
+ public static boolean equals(Bundle a, Bundle b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ if (!a.keySet().containsAll(b.keySet())
+ || !b.keySet().containsAll(a.keySet())) {
+ return false;
+ }
+ for (String key : a.keySet()) {
+ if (!Objects.equals(a.get(key), b.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create a playlist for testing purpose
+ * <p>
+ * Caller's method name will be used for prefix of each media item's media id.
+ *
+ * @param size lits size
+ * @return the newly created playlist
+ */
+ public static List<MediaItem2> createPlaylist(int size) {
+ final List<MediaItem2> list = new ArrayList<>();
+ String caller = Thread.currentThread().getStackTrace()[1].getMethodName();
+ for (int i = 0; i < size; i++) {
+ list.add(new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+ .setMediaId(caller + "_item_" + (size + 1))
+ .setDataSourceDesc(
+ new DataSourceDesc.Builder()
+ .setDataSource(new FileDescriptor())
+ .build())
+ .build());
+ }
+ return list;
+ }
+
+ /**
+ * Create a media item with the metadata for testing purpose.
+ *
+ * @return the newly created media item
+ * @see #createMetadata()
+ */
+ public static MediaItem2 createMediaItemWithMetadata() {
+ return new MediaItem2.Builder(MediaItem2.FLAG_PLAYABLE)
+ .setMetadata(createMetadata()).build();
+ }
+
+ /**
+ * Create a media metadata for testing purpose.
+ * <p>
+ * Caller's method name will be used for the media id.
+ *
+ * @return the newly created media item
+ */
+ public static MediaMetadata2 createMetadata() {
+ String mediaId = Thread.currentThread().getStackTrace()[1].getMethodName();
+ return new MediaMetadata2.Builder()
+ .putString(MediaMetadata2.METADATA_KEY_MEDIA_ID, mediaId).build();
+ }
+
+ /**
+ * Handler that always waits until the Runnable finishes.
+ */
+ public static class SyncHandler extends Handler {
+ public SyncHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void postAndSync(final Runnable runnable) throws InterruptedException {
+ if (getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ final CountDownLatch latch = new CountDownLatch(1);
+ post(new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+ latch.countDown();
+ }
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+}
diff --git a/androidx/paging/DataSource.java b/androidx/paging/DataSource.java
index 55cd0a9c..157750a5 100644
--- a/androidx/paging/DataSource.java
+++ b/androidx/paging/DataSource.java
@@ -16,391 +16,7 @@
package androidx.paging;
-import androidx.annotation.AnyThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.util.Function;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Base class for loading pages of snapshot data into a {@link PagedList}.
- * <p>
- * DataSource is queried to load pages of content into a {@link PagedList}. A PagedList can grow as
- * it loads more data, but the data loaded cannot be updated. If the underlying data set is
- * modified, a new PagedList / DataSource pair must be created to represent the new data.
- * <h4>Loading Pages</h4>
- * PagedList queries data from its DataSource in response to loading hints. {@link PagedListAdapter}
- * calls {@link PagedList#loadAround(int)} to load content as the user scrolls in a RecyclerView.
- * <p>
- * To control how and when a PagedList queries data from its DataSource, see
- * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
- * <h4>Updating Paged Data</h4>
- * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
- * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
- * content update occurs. A DataSource must detect that it cannot continue loading its
- * snapshot (for instance, when Database query notices a table being invalidated), and call
- * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
- * the new state of the Database query.
- * <p>
- * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
- * PagedList. For example, loading from network when the network's paging API doesn't provide
- * updates.
- * <p>
- * To page in data from a source that does provide updates, you can create a
- * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
- * data set occurs that makes the current snapshot invalid. For example, when paging a query from
- * the Database, and the table being queried inserts or removes items. You can also use a
- * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
- * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
- * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
- * DataSource.
- * <p>
- * If you have more granular update signals, such as a network API signaling an update to a single
- * item in the list, it's recommended to load data from network into memory. Then present that
- * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
- * copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the
- * snapshot can be created.
- * <h4>Implementing a DataSource</h4>
- * To implement, extend one of the subclasses: {@link PageKeyedDataSource},
- * {@link ItemKeyedDataSource}, or {@link PositionalDataSource}.
- * <p>
- * Use {@link PageKeyedDataSource} if pages you load embed keys for loading adjacent pages. For
- * example a network response that returns some items, and a next/previous page links.
- * <p>
- * Use {@link ItemKeyedDataSource} if you need to use data from item {@code N-1} to load item
- * {@code N}. For example, if requesting the backend for the next comments in the list
- * requires the ID or timestamp of the most recent loaded comment, or if querying the next users
- * from a name-sorted database query requires the name and unique ID of the previous.
- * <p>
- * Use {@link PositionalDataSource} if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. PositionalDataSource supports querying pages at
- * arbitrary positions, so can provide data to PagedLists in arbitrary order. Note that
- * PositionalDataSource is required to respect page size for efficient tiling. If you want to
- * override page size (e.g. when network page size constraints are only known at runtime), use one
- * of the other DataSource classes.
- * <p>
- * Because a {@code null} item indicates a placeholder in {@link PagedList}, DataSource may not
- * return {@code null} items in lists that it loads. This is so that users of the PagedList
- * can differentiate unloaded placeholder items from content that has been paged in.
- *
- * @param <Key> Input used to trigger initial load from the DataSource. Often an Integer position.
- * @param <Value> Value type loaded by the DataSource.
- */
-@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
-public abstract class DataSource<Key, Value> {
- /**
- * Factory for DataSources.
- * <p>
- * Data-loading systems of an application or library can implement this interface to allow
- * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
- * DataSource.Factory for a given SQL query:
- *
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- * public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
- * }
- * </pre>
- * In the above sample, {@code Integer} is used because it is the {@code Key} type of
- * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
- * page a large query with a PositionalDataSource.
- *
- * @param <Key> Key identifying items in DataSource.
- * @param <Value> Type of items in the list loaded by the DataSources.
- */
- public abstract static class Factory<Key, Value> {
- /**
- * Create a DataSource.
- * <p>
- * The DataSource should invalidate itself if the snapshot is no longer valid. If a
- * DataSource becomes invalid, the only way to query more data is to create a new DataSource
- * from the Factory.
- * <p>
- * {@link LivePagedListBuilder} for example will construct a new PagedList and DataSource
- * when the current DataSource is invalidated, and pass the new PagedList through the
- * {@code LiveData<PagedList>} to observers.
- *
- * @return the new DataSource.
- */
- public abstract DataSource<Key, Value> create();
-
- /**
- * Applies the given function to each value emitted by DataSources produced by this Factory.
- * <p>
- * Same as {@link #mapByPage(Function)}, but operates on individual items.
- *
- * @param function Function that runs on each loaded item, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource.Factory, which transforms items using the given function.
- *
- * @see #mapByPage(Function)
- * @see DataSource#map(Function)
- * @see DataSource#mapByPage(Function)
- */
- @NonNull
- public <ToValue> DataSource.Factory<Key, ToValue> map(
- @NonNull Function<Value, ToValue> function) {
- return mapByPage(createListFunction(function));
- }
-
- /**
- * Applies the given function to each value emitted by DataSources produced by this Factory.
- * <p>
- * Same as {@link #map(Function)}, but allows for batch conversions.
- *
- * @param function Function that runs on each loaded page, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource.Factory, which transforms items using the given function.
- *
- * @see #map(Function)
- * @see DataSource#map(Function)
- * @see DataSource#mapByPage(Function)
- */
- @NonNull
- public <ToValue> DataSource.Factory<Key, ToValue> mapByPage(
- @NonNull final Function<List<Value>, List<ToValue>> function) {
- return new Factory<Key, ToValue>() {
- @Override
- public DataSource<Key, ToValue> create() {
- return Factory.this.create().mapByPage(function);
- }
- };
- }
- }
-
- @NonNull
- static <X, Y> Function<List<X>, List<Y>> createListFunction(
- final @NonNull Function<X, Y> innerFunc) {
- return new Function<List<X>, List<Y>>() {
- @Override
- public List<Y> apply(@NonNull List<X> source) {
- List<Y> out = new ArrayList<>(source.size());
- for (int i = 0; i < source.size(); i++) {
- out.add(innerFunc.apply(source.get(i)));
- }
- return out;
- }
- };
- }
-
- static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
- List<B> dest = function.apply(source);
- if (dest.size() != source.size()) {
- throw new IllegalStateException("Invalid Function " + function
- + " changed return size. This is not supported.");
- }
- return dest;
- }
-
- // Since we currently rely on implementation details of two implementations,
- // prevent external subclassing, except through exposed subclasses
- DataSource() {
- }
-
- /**
- * Applies the given function to each value emitted by the DataSource.
- * <p>
- * Same as {@link #map(Function)}, but allows for batch conversions.
- *
- * @param function Function that runs on each loaded page, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource, which transforms items using the given function.
- *
- * @see #map(Function)
- * @see DataSource.Factory#map(Function)
- * @see DataSource.Factory#mapByPage(Function)
- */
- @NonNull
- public abstract <ToValue> DataSource<Key, ToValue> mapByPage(
- @NonNull Function<List<Value>, List<ToValue>> function);
-
- /**
- * Applies the given function to each value emitted by the DataSource.
- * <p>
- * Same as {@link #mapByPage(Function)}, but operates on individual items.
- *
- * @param function Function that runs on each loaded item, returning items of a potentially
- * new type.
- * @param <ToValue> Type of items produced by the new DataSource, from the passed function.
- *
- * @return A new DataSource, which transforms items using the given function.
- *
- * @see #mapByPage(Function)
- * @see DataSource.Factory#map(Function)
- * @see DataSource.Factory#mapByPage(Function)
- */
- @NonNull
- public abstract <ToValue> DataSource<Key, ToValue> map(
- @NonNull Function<Value, ToValue> function);
-
- /**
- * Returns true if the data source guaranteed to produce a contiguous set of items,
- * never producing gaps.
- */
- abstract boolean isContiguous();
-
- static class LoadCallbackHelper<T> {
- static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative");
- }
- if (data.size() + position > totalCount) {
- throw new IllegalArgumentException(
- "List size + position too large, last item in list beyond totalCount.");
- }
- if (data.size() == 0 && totalCount > 0) {
- throw new IllegalArgumentException(
- "Initial result cannot be empty if items are present in data set.");
- }
- }
-
- @PageResult.ResultType
- final int mResultType;
- private final DataSource mDataSource;
- private final PageResult.Receiver<T> mReceiver;
-
- // mSignalLock protects mPostExecutor, and mHasSignalled
- private final Object mSignalLock = new Object();
- private Executor mPostExecutor = null;
- private boolean mHasSignalled = false;
-
- LoadCallbackHelper(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
- @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- mDataSource = dataSource;
- mResultType = resultType;
- mPostExecutor = mainThreadExecutor;
- mReceiver = receiver;
- }
-
- void setPostExecutor(Executor postExecutor) {
- synchronized (mSignalLock) {
- mPostExecutor = postExecutor;
- }
- }
-
- /**
- * Call before verifying args, or dispatching actul results
- *
- * @return true if DataSource was invalid, and invalid result dispatched
- */
- boolean dispatchInvalidResultIfInvalid() {
- if (mDataSource.isInvalid()) {
- dispatchResultToReceiver(PageResult.<T>getInvalidResult());
- return true;
- }
- return false;
- }
-
- void dispatchResultToReceiver(final @NonNull PageResult<T> result) {
- Executor executor;
- synchronized (mSignalLock) {
- if (mHasSignalled) {
- throw new IllegalStateException(
- "callback.onResult already called, cannot call again.");
- }
- mHasSignalled = true;
- executor = mPostExecutor;
- }
-
- if (executor != null) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- mReceiver.onPageResult(mResultType, result);
- }
- });
- } else {
- mReceiver.onPageResult(mResultType, result);
- }
- }
- }
-
- /**
- * Invalidation callback for DataSource.
- * <p>
- * Used to signal when a DataSource a data source has become invalid, and that a new data source
- * is needed to continue loading data.
- */
- public interface InvalidatedCallback {
- /**
- * Called when the data backing the list has become invalid. This callback is typically used
- * to signal that a new data source is needed.
- * <p>
- * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
- * for the data source to invalidate itself during its load methods, or for an outside
- * source to invalidate it.
- */
- @AnyThread
- void onInvalidated();
- }
-
- private AtomicBoolean mInvalid = new AtomicBoolean(false);
-
- private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
- new CopyOnWriteArrayList<>();
-
- /**
- * Add a callback to invoke when the DataSource is first invalidated.
- * <p>
- * Once invalidated, a data source will not become valid again.
- * <p>
- * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
- * is called, on that thread.
- *
- * @param onInvalidatedCallback The callback, will be invoked on thread that
- * {@link #invalidate()} is called on.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.add(onInvalidatedCallback);
- }
-
- /**
- * Remove a previously added invalidate callback.
- *
- * @param onInvalidatedCallback The previously added callback.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
- }
-
- /**
- * Signal the data source to stop loading, and notify its callback.
- * <p>
- * If invalidate has already been called, this method does nothing.
- */
- @AnyThread
- public void invalidate() {
- if (mInvalid.compareAndSet(false, true)) {
- for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
- callback.onInvalidated();
- }
- }
- }
-
- /**
- * Returns true if the data source is invalid, and can no longer be queried for data.
- *
- * @return True if the data source is invalid, and can no longer return data.
- */
- @WorkerThread
- public boolean isInvalid() {
- return mInvalid.get();
+abstract public class DataSource<K, T> {
+ public interface Factory<Key, Value> {
}
}
diff --git a/androidx/paging/LivePagedListBuilder.java b/androidx/paging/LivePagedListBuilder.java
index f8af819b..672e40f5 100644
--- a/androidx/paging/LivePagedListBuilder.java
+++ b/androidx/paging/LivePagedListBuilder.java
@@ -30,7 +30,7 @@ import java.util.concurrent.Executor;
* {@link PagedList.Config}.
* <p>
* The required parameters are in the constructor, so you can simply construct and build, or
- * optionally enable extra features (such as initial load key, or BoundaryCallback.
+ * optionally enable extra features (such as initial load key, or BoundaryCallback).
*
* @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
* you're using PositionalDataSource.
diff --git a/androidx/paging/PositionalDataSource.java b/androidx/paging/PositionalDataSource.java
index 4f7cd3f8..ff5338aa 100644
--- a/androidx/paging/PositionalDataSource.java
+++ b/androidx/paging/PositionalDataSource.java
@@ -1,546 +1,5 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
package androidx.paging;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import androidx.arch.core.util.Function;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at
- * arbitrary page positions.
- * <p>
- * Extend PositionalDataSource if you can load pages of a requested size at arbitrary
- * positions, and provide a fixed item count. If your data source can't support loading arbitrary
- * requested page sizes (e.g. when network page size constraints are only known at runtime), use
- * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead.
- * <p>
- * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled}
- * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in
- * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}.
- * If placeholders are disabled, initialize with the two parameter
- * {@link LoadInitialCallback#onResult(List, int)}.
- * <p>
- * Room can generate a Factory of PositionalDataSources for you:
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC")
- * public abstract DataSource.Factory&lt;Integer, User> loadUsersByAgeDesc();
- * }</pre>
- *
- * @param <T> Type of items being loaded by the PositionalDataSource.
- */
public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
- /**
- * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadInitialParams {
- /**
- * Initial load position requested.
- * <p>
- * Note that this may not be within the bounds of your data set, it may need to be adjusted
- * before you execute your load.
- */
- public final int requestedStartPosition;
-
- /**
- * Requested number of items to load.
- * <p>
- * Note that this may be larger than available data.
- */
- public final int requestedLoadSize;
-
- /**
- * Defines page size acceptable for return values.
- * <p>
- * List of items passed to the callback must be an integer multiple of page size.
- */
- public final int pageSize;
-
- /**
- * Defines whether placeholders are enabled, and whether the total count passed to
- * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored.
- */
- public final boolean placeholdersEnabled;
-
- public LoadInitialParams(
- int requestedStartPosition,
- int requestedLoadSize,
- int pageSize,
- boolean placeholdersEnabled) {
- this.requestedStartPosition = requestedStartPosition;
- this.requestedLoadSize = requestedLoadSize;
- this.pageSize = pageSize;
- this.placeholdersEnabled = placeholdersEnabled;
- }
- }
-
- /**
- * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
- */
- @SuppressWarnings("WeakerAccess")
- public static class LoadRangeParams {
- /**
- * Start position of data to load.
- * <p>
- * Returned data must start at this position.
- */
- public final int startPosition;
- /**
- * Number of items to load.
- * <p>
- * Returned data must be of this size, unless at end of the list.
- */
- public final int loadSize;
-
- public LoadRangeParams(int startPosition, int loadSize) {
- this.startPosition = startPosition;
- this.loadSize = loadSize;
- }
- }
-
- /**
- * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}
- * to return data, position, and count.
- * <p>
- * A callback should be called only once, and may throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <T> Type of items being loaded.
- */
- public abstract static class LoadInitialCallback<T> {
- /**
- * Called to pass initial load state from a DataSource.
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * and inform how many placeholders should be shown before and after. If counting is cheap
- * to compute (for example, if a network load returns the information regardless), it's
- * recommended to pass the total size to the totalCount parameter. If placeholders are not
- * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead
- * call {@link #onResult(List, int)}.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be loaded from this DataSource,
- * pass {@code N}.
- * @param totalCount Total number of items that may be returned from this DataSource.
- * Includes the number in the initial {@code data} parameter
- * as well as any items that can be loaded in front or behind of
- * {@code data}.
- */
- public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
-
- /**
- * Called to pass initial load state from a DataSource without total count,
- * when placeholders aren't requested.
- * <p class="note"><strong>Note:</strong> This method can only be called when placeholders
- * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false).
- * <p>
- * Call this method from your DataSource's {@code loadInitial} function to return data,
- * if position is known but total size is not. If placeholders are requested, call the three
- * parameter variant: {@link #onResult(List, int, int)}.
- *
- * @param data List of items loaded from the DataSource. If this is empty, the DataSource
- * is treated as empty, and no further loads will occur.
- * @param position Position of the item at the front of the list. If there are {@code N}
- * items before the items in data that can be provided by this DataSource,
- * pass {@code N}.
- */
- public abstract void onResult(@NonNull List<T> data, int position);
- }
-
- /**
- * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)}
- * to return data.
- * <p>
- * A callback should be called only once, and may throw if called again.
- * <p>
- * It is always valid for a DataSource loading method that takes a callback to stash the
- * callback and call it later. This enables DataSources to be fully asynchronous, and to handle
- * temporary, recoverable error states (such as a network error that can be retried).
- *
- * @param <T> Type of items being loaded.
- */
- public abstract static class LoadRangeCallback<T> {
- /**
- * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}.
- *
- * @param data List of items loaded from the DataSource. Must be same size as requested,
- * unless at end of list.
- */
- public abstract void onResult(@NonNull List<T> data);
- }
-
- static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
- final LoadCallbackHelper<T> mCallbackHelper;
- private final boolean mCountingEnabled;
- private final int mPageSize;
-
- LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled,
- int pageSize, PageResult.Receiver<T> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(dataSource, PageResult.INIT, null, receiver);
- mCountingEnabled = countingEnabled;
- mPageSize = pageSize;
- if (mPageSize < 1) {
- throw new IllegalArgumentException("Page size must be non-negative");
- }
- }
-
- @Override
- public void onResult(@NonNull List<T> data, int position, int totalCount) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- LoadCallbackHelper.validateInitialLoadParams(data, position, totalCount);
- if (position + data.size() != totalCount
- && data.size() % mPageSize != 0) {
- throw new IllegalArgumentException("PositionalDataSource requires initial load"
- + " size to be a multiple of page size to support internal tiling."
- + " loadSize " + data.size() + ", position " + position
- + ", totalCount " + totalCount + ", pageSize " + mPageSize);
- }
-
- if (mCountingEnabled) {
- int trailingUnloadedCount = totalCount - position - data.size();
- mCallbackHelper.dispatchResultToReceiver(
- new PageResult<>(data, position, trailingUnloadedCount, 0));
- } else {
- // Only occurs when wrapped as contiguous
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- @Override
- public void onResult(@NonNull List<T> data, int position) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative");
- }
- if (data.isEmpty() && position != 0) {
- throw new IllegalArgumentException(
- "Initial result cannot be empty if items are present in data set.");
- }
- if (mCountingEnabled) {
- throw new IllegalStateException("Placeholders requested, but totalCount not"
- + " provided. Please call the three-parameter onResult method, or"
- + " disable placeholders in the PagedList.Config");
- }
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(data, position));
- }
- }
- }
-
- static class LoadRangeCallbackImpl<T> extends LoadRangeCallback<T> {
- private LoadCallbackHelper<T> mCallbackHelper;
- private final int mPositionOffset;
- LoadRangeCallbackImpl(@NonNull PositionalDataSource dataSource,
- @PageResult.ResultType int resultType, int positionOffset,
- Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
- mCallbackHelper = new LoadCallbackHelper<>(
- dataSource, resultType, mainThreadExecutor, receiver);
- mPositionOffset = positionOffset;
- }
-
- @Override
- public void onResult(@NonNull List<T> data) {
- if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
- mCallbackHelper.dispatchResultToReceiver(new PageResult<>(
- data, 0, 0, mPositionOffset));
- }
- }
- }
-
- final void dispatchLoadInitial(boolean acceptCount,
- int requestedStartPosition, int requestedLoadSize, int pageSize,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- LoadInitialCallbackImpl<T> callback =
- new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
-
- LoadInitialParams params = new LoadInitialParams(
- requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
- loadInitial(params, callback);
-
- // If initialLoad's callback is not called within the body, we force any following calls
- // to post to the UI thread. This constructor may be run on a background thread, but
- // after constructor, mutation must happen on UI thread.
- callback.mCallbackHelper.setPostExecutor(mainThreadExecutor);
- }
-
- final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
- int count, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<T> receiver) {
- LoadRangeCallback<T> callback = new LoadRangeCallbackImpl<>(
- this, resultType, startPosition, mainThreadExecutor, receiver);
- if (count == 0) {
- callback.onResult(Collections.<T>emptyList());
- } else {
- loadRange(new LoadRangeParams(startPosition, count), callback);
- }
- }
-
- /**
- * Load initial list data.
- * <p>
- * This method is called to load the initial page(s) from the DataSource.
- * <p>
- * Result list must be a multiple of pageSize to enable efficient tiling.
- *
- * @param params Parameters for initial load, including requested start position, load size, and
- * page size.
- * @param callback Callback that receives initial load data, including
- * position and total data set size.
- */
- @WorkerThread
- public abstract void loadInitial(
- @NonNull LoadInitialParams params,
- @NonNull LoadInitialCallback<T> callback);
-
- /**
- * Called to load a range of data from the DataSource.
- * <p>
- * This method is called to load additional pages from the DataSource after the
- * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList.
- * <p>
- * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return
- * the number of items requested, at the position requested.
- *
- * @param params Parameters for load, including start position and load size.
- * @param callback Callback that receives loaded data.
- */
- @WorkerThread
- public abstract void loadRange(@NonNull LoadRangeParams params,
- @NonNull LoadRangeCallback<T> callback);
-
- @Override
- boolean isContiguous() {
- return false;
- }
-
- @NonNull
- ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() {
- return new ContiguousWithoutPlaceholdersWrapper<>(this);
- }
-
- /**
- * Helper for computing an initial position in
- * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
- * computed ahead of loading.
- * <p>
- * The value computed by this function will do bounds checking, page alignment, and positioning
- * based on initial load size requested.
- * <p>
- * Example usage in a PositionalDataSource subclass:
- * <pre>
- * class ItemDataSource extends PositionalDataSource&lt;Item> {
- * private int computeCount() {
- * // actual count code here
- * }
- *
- * private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
- * // actual load code here
- * }
- *
- * {@literal @}Override
- * public void loadInitial({@literal @}NonNull LoadInitialParams params,
- * {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
- * int totalCount = computeCount();
- * int position = computeInitialLoadPosition(params, totalCount);
- * int loadSize = computeInitialLoadSize(params, position, totalCount);
- * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
- * }
- *
- * {@literal @}Override
- * public void loadRange({@literal @}NonNull LoadRangeParams params,
- * {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
- * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
- * }
- * }</pre>
- *
- * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
- * including page size, and requested start/loadSize.
- * @param totalCount Total size of the data set.
- * @return Position to start loading at.
- *
- * @see #computeInitialLoadSize(LoadInitialParams, int, int)
- */
- public static int computeInitialLoadPosition(@NonNull LoadInitialParams params,
- int totalCount) {
- int position = params.requestedStartPosition;
- int initialLoadSize = params.requestedLoadSize;
- int pageSize = params.pageSize;
-
- int roundedPageStart = Math.round(position / pageSize) * pageSize;
-
- // maximum start pos is that which will encompass end of list
- int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize;
- roundedPageStart = Math.min(maximumLoadPage, roundedPageStart);
-
- // minimum start position is 0
- roundedPageStart = Math.max(0, roundedPageStart);
-
- return roundedPageStart;
- }
-
- /**
- * Helper for computing an initial load size in
- * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be
- * computed ahead of loading.
- * <p>
- * This function takes the requested load size, and bounds checks it against the value returned
- * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}.
- * <p>
- * Example usage in a PositionalDataSource subclass:
- * <pre>
- * class ItemDataSource extends PositionalDataSource&lt;Item> {
- * private int computeCount() {
- * // actual count code here
- * }
- *
- * private List&lt;Item> loadRangeInternal(int startPosition, int loadCount) {
- * // actual load code here
- * }
- *
- * {@literal @}Override
- * public void loadInitial({@literal @}NonNull LoadInitialParams params,
- * {@literal @}NonNull LoadInitialCallback&lt;Item> callback) {
- * int totalCount = computeCount();
- * int position = computeInitialLoadPosition(params, totalCount);
- * int loadSize = computeInitialLoadSize(params, position, totalCount);
- * callback.onResult(loadRangeInternal(position, loadSize), position, totalCount);
- * }
- *
- * {@literal @}Override
- * public void loadRange({@literal @}NonNull LoadRangeParams params,
- * {@literal @}NonNull LoadRangeCallback&lt;Item> callback) {
- * callback.onResult(loadRangeInternal(params.startPosition, params.loadSize));
- * }
- * }</pre>
- *
- * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)},
- * including page size, and requested start/loadSize.
- * @param initialLoadPosition Value returned by
- * {@link #computeInitialLoadPosition(LoadInitialParams, int)}
- * @param totalCount Total size of the data set.
- * @return Number of items to load.
- *
- * @see #computeInitialLoadPosition(LoadInitialParams, int)
- */
- @SuppressWarnings("WeakerAccess")
- public static int computeInitialLoadSize(@NonNull LoadInitialParams params,
- int initialLoadPosition, int totalCount) {
- return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize);
- }
-
- @SuppressWarnings("deprecation")
- static class ContiguousWithoutPlaceholdersWrapper<Value>
- extends ContiguousDataSource<Integer, Value> {
-
- @NonNull
- final PositionalDataSource<Value> mPositionalDataSource;
-
- ContiguousWithoutPlaceholdersWrapper(
- @NonNull PositionalDataSource<Value> positionalDataSource) {
- mPositionalDataSource = positionalDataSource;
- }
-
- @NonNull
- @Override
- public <ToValue> DataSource<Integer, ToValue> mapByPage(
- @NonNull Function<List<Value>, List<ToValue>> function) {
- throw new UnsupportedOperationException(
- "Inaccessible inner type doesn't support map op");
- }
-
- @NonNull
- @Override
- public <ToValue> DataSource<Integer, ToValue> map(
- @NonNull Function<Value, ToValue> function) {
- throw new UnsupportedOperationException(
- "Inaccessible inner type doesn't support map op");
- }
-
- @Override
- void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize,
- boolean enablePlaceholders, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- final int convertPosition = position == null ? 0 : position;
-
- // Note enablePlaceholders will be false here, but we don't have a way to communicate
- // this to PositionalDataSource. This is fine, because only the list and its position
- // offset will be consumed by the LoadInitialCallback.
- mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize,
- pageSize, mainThreadExecutor, receiver);
- }
-
- @Override
- void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize,
- @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
- int startIndex = currentEndIndex + 1;
- mPositionalDataSource.dispatchLoadRange(
- PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
- }
-
- @Override
- void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem,
- int pageSize, @NonNull Executor mainThreadExecutor,
- @NonNull PageResult.Receiver<Value> receiver) {
-
- int startIndex = currentBeginIndex - 1;
- if (startIndex < 0) {
- // trigger empty list load
- mPositionalDataSource.dispatchLoadRange(
- PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
- } else {
- int loadSize = Math.min(pageSize, startIndex + 1);
- startIndex = startIndex - loadSize + 1;
- mPositionalDataSource.dispatchLoadRange(
- PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
- }
- }
-
- @Override
- Integer getKey(int position, Value item) {
- return position;
- }
-
- }
-
- @NonNull
- @Override
- public final <V> PositionalDataSource<V> mapByPage(
- @NonNull Function<List<T>, List<V>> function) {
- return new WrapperPositionalDataSource<>(this, function);
- }
-
- @NonNull
- @Override
- public final <V> PositionalDataSource<V> map(@NonNull Function<T, V> function) {
- return mapByPage(createListFunction(function));
- }
-}
+} \ No newline at end of file
diff --git a/androidx/paging/RxPagedListBuilder.java b/androidx/paging/RxPagedListBuilder.java
new file mode 100644
index 00000000..f98bc1ec
--- /dev/null
+++ b/androidx/paging/RxPagedListBuilder.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.ArchTaskExecutor;
+
+import java.util.concurrent.Executor;
+
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+import io.reactivex.Observable;
+import io.reactivex.ObservableEmitter;
+import io.reactivex.ObservableOnSubscribe;
+import io.reactivex.Scheduler;
+import io.reactivex.functions.Cancellable;
+import io.reactivex.schedulers.Schedulers;
+
+/**
+ * Builder for {@code Observable<PagedList>} or {@code Flowable<PagedList>}, given a
+ * {@link DataSource.Factory} and a {@link PagedList.Config}.
+ * <p>
+ * The required parameters are in the constructor, so you can simply construct and build, or
+ * optionally enable extra features (such as initial load key, or BoundaryCallback).
+ * <p>
+ * The returned observable/flowable will already be subscribed on the
+ * {@link #setFetchScheduler(Scheduler)}, and will perform all loading on that scheduler. It will
+ * already be observed on {@link #setNotifyScheduler(Scheduler)}, and will dispatch new PagedLists,
+ * as well as their updates to that scheduler.
+ *
+ * @param <Key> Type of input valued used to load data from the DataSource. Must be integer if
+ * you're using PositionalDataSource.
+ * @param <Value> Item type being presented.
+ */
+public final class RxPagedListBuilder<Key, Value> {
+ private Key mInitialLoadKey;
+ private PagedList.Config mConfig;
+ private DataSource.Factory<Key, Value> mDataSourceFactory;
+ private PagedList.BoundaryCallback mBoundaryCallback;
+ private Executor mNotifyExecutor;
+ private Executor mFetchExecutor;
+ private Scheduler mFetchScheduler;
+ private Scheduler mNotifyScheduler;
+
+ /**
+ * Creates a RxPagedListBuilder with required parameters.
+ *
+ * @param dataSourceFactory DataSource factory providing DataSource generations.
+ * @param config Paging configuration.
+ */
+ public RxPagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+ @NonNull PagedList.Config config) {
+ //noinspection ConstantConditions
+ if (config == null) {
+ throw new IllegalArgumentException("PagedList.Config must be provided");
+ }
+ //noinspection ConstantConditions
+ if (dataSourceFactory == null) {
+ throw new IllegalArgumentException("DataSource.Factory must be provided");
+ }
+ mDataSourceFactory = dataSourceFactory;
+ mConfig = config;
+ }
+
+ /**
+ * Creates a RxPagedListBuilder with required parameters.
+ * <p>
+ * This method is a convenience for:
+ * <pre>
+ * RxPagedListBuilder(dataSourceFactory,
+ * new PagedList.Config.Builder().setPageSize(pageSize).build())
+ * </pre>
+ *
+ * @param dataSourceFactory DataSource.Factory providing DataSource generations.
+ * @param pageSize Size of pages to load.
+ */
+ @SuppressWarnings("unused")
+ public RxPagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+ int pageSize) {
+ this(dataSourceFactory, new PagedList.Config.Builder().setPageSize(pageSize).build());
+ }
+
+ /**
+ * First loading key passed to the first PagedList/DataSource.
+ * <p>
+ * When a new PagedList/DataSource pair is created after the first, it acquires a load key from
+ * the previous generation so that data is loaded around the position already being observed.
+ *
+ * @param key Initial load key passed to the first PagedList/DataSource.
+ * @return this
+ */
+ @SuppressWarnings("unused")
+ @NonNull
+ public RxPagedListBuilder<Key, Value> setInitialLoadKey(@Nullable Key key) {
+ mInitialLoadKey = key;
+ return this;
+ }
+
+ /**
+ * Sets a {@link PagedList.BoundaryCallback} on each PagedList created, typically used to load
+ * additional data from network when paging from local storage.
+ * <p>
+ * Pass a BoundaryCallback to listen to when the PagedList runs out of data to load. If this
+ * method is not called, or {@code null} is passed, you will not be notified when each
+ * DataSource runs out of data to provide to its PagedList.
+ * <p>
+ * If you are paging from a DataSource.Factory backed by local storage, you can set a
+ * BoundaryCallback to know when there is no more information to page from local storage.
+ * This is useful to page from the network when local storage is a cache of network data.
+ * <p>
+ * Note that when using a BoundaryCallback with a {@code Observable<PagedList>}, method calls
+ * on the callback may be dispatched multiple times - one for each PagedList/DataSource
+ * pair. If loading network data from a BoundaryCallback, you should prevent multiple
+ * dispatches of the same method from triggering multiple simultaneous network loads.
+ *
+ * @param boundaryCallback The boundary callback for listening to PagedList load state.
+ * @return this
+ */
+ @SuppressWarnings("unused")
+ @NonNull
+ public RxPagedListBuilder<Key, Value> setBoundaryCallback(
+ @Nullable PagedList.BoundaryCallback<Value> boundaryCallback) {
+ mBoundaryCallback = boundaryCallback;
+ return this;
+ }
+
+ /**
+ * Sets scheduler which will be used for observing new PagedLists, as well as loading updates
+ * within the PagedLists.
+ * <p>
+ * The built observable will be {@link Observable#observeOn(Scheduler) observed on} this
+ * scheduler, so that the thread receiving PagedLists will also receive the internal updates to
+ * the PagedList.
+ *
+ * @param scheduler Scheduler for background DataSource loading.
+ * @return this
+ */
+ public RxPagedListBuilder<Key, Value> setNotifyScheduler(
+ final @NonNull Scheduler scheduler) {
+ mNotifyScheduler = scheduler;
+ final Scheduler.Worker worker = scheduler.createWorker();
+ mNotifyExecutor = new Executor() {
+ @Override
+ public void execute(@NonNull Runnable command) {
+ // We use a worker here since the page load notifications
+ // should not be dispatched in parallel
+ worker.schedule(command);
+ }
+ };
+ return this;
+ }
+
+ /**
+ * Sets scheduler which will be used for background fetching of PagedLists, as well as on-demand
+ * fetching of pages inside.
+ *
+ * @param scheduler Scheduler for background DataSource loading.
+ * @return this
+ */
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ @NonNull
+ public RxPagedListBuilder<Key, Value> setFetchScheduler(
+ final @NonNull Scheduler scheduler) {
+ mFetchExecutor = new Executor() {
+ @Override
+ public void execute(@NonNull Runnable command) {
+ // We use scheduleDirect since the page loads that use
+ // executor are intentionally parallel.
+ scheduler.scheduleDirect(command);
+ }
+ };
+ mFetchScheduler = scheduler;
+ return this;
+ }
+
+ /**
+ * Constructs a {@code Observable<PagedList>}.
+ * <p>
+ * The returned Observable will already be observed on the
+ * {@link #setNotifyScheduler(Scheduler) notify scheduler}, and subscribed on the
+ * {@link #setFetchScheduler(Scheduler) fetch scheduler}.
+ *
+ * @return The Observable of PagedLists
+ */
+ @NonNull
+ public Observable<PagedList<Value>> buildObservable() {
+ if (mNotifyExecutor == null) {
+ mNotifyExecutor = ArchTaskExecutor.getMainThreadExecutor();
+ mNotifyScheduler = Schedulers.from(mNotifyExecutor);
+ }
+ if (mFetchExecutor == null) {
+ mFetchExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ mFetchScheduler = Schedulers.from(mFetchExecutor);
+ }
+ return Observable.create(new PagingObservableOnSubscribe<>(
+ mInitialLoadKey,
+ mConfig,
+ mBoundaryCallback,
+ mDataSourceFactory,
+ mNotifyExecutor,
+ mFetchExecutor))
+ .observeOn(mNotifyScheduler)
+ .subscribeOn(mFetchScheduler);
+ }
+
+ /**
+ * Constructs a {@code Flowable<PagedList>}.
+ *
+ * The returned Observable will already be observed on the
+ * {@link #setNotifyScheduler(Scheduler) notify scheduler}, and subscribed on the
+ * {@link #setFetchScheduler(Scheduler) fetch scheduler}.
+ *
+ * @param backpressureStrategy BackpressureStrategy for the Flowable to use.
+ * @return The Flowable of PagedLists
+ */
+ @NonNull
+ public Flowable<PagedList<Value>> buildFlowable(BackpressureStrategy backpressureStrategy) {
+ return buildObservable()
+ .toFlowable(backpressureStrategy);
+ }
+
+ static class PagingObservableOnSubscribe<Key, Value>
+ implements ObservableOnSubscribe<PagedList<Value>>, DataSource.InvalidatedCallback,
+ Cancellable,
+ Runnable {
+
+ @Nullable
+ private final Key mInitialLoadKey;
+ @NonNull
+ private final PagedList.Config mConfig;
+ @Nullable
+ private final PagedList.BoundaryCallback mBoundaryCallback;
+ @NonNull
+ private final DataSource.Factory<Key, Value> mDataSourceFactory;
+ @NonNull
+ private final Executor mNotifyExecutor;
+ @NonNull
+ private final Executor mFetchExecutor;
+
+ @Nullable
+ private PagedList<Value> mList;
+ @Nullable
+ private DataSource<Key, Value> mDataSource;
+
+ private ObservableEmitter<PagedList<Value>> mEmitter;
+
+ private PagingObservableOnSubscribe(@Nullable Key initialLoadKey,
+ @NonNull PagedList.Config config,
+ @Nullable PagedList.BoundaryCallback boundaryCallback,
+ @NonNull DataSource.Factory<Key, Value> dataSourceFactory,
+ @NonNull Executor notifyExecutor,
+ @NonNull Executor fetchExecutor) {
+ mInitialLoadKey = initialLoadKey;
+ mConfig = config;
+ mBoundaryCallback = boundaryCallback;
+ mDataSourceFactory = dataSourceFactory;
+ mNotifyExecutor = notifyExecutor;
+ mFetchExecutor = fetchExecutor;
+ }
+
+ @Override
+ public void subscribe(ObservableEmitter<PagedList<Value>> emitter)
+ throws Exception {
+ mEmitter = emitter;
+ mEmitter.setCancellable(this);
+
+ // known that subscribe is already on fetchScheduler
+ mEmitter.onNext(createPagedList());
+ }
+
+ @Override
+ public void cancel() throws Exception {
+ if (mDataSource != null) {
+ mDataSource.removeInvalidatedCallback(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ // fetch data, run on fetchExecutor
+ mEmitter.onNext(createPagedList());
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (!mEmitter.isDisposed()) {
+ mFetchExecutor.execute(this);
+ }
+ }
+
+ private PagedList<Value> createPagedList() {
+ @Nullable Key initializeKey = mInitialLoadKey;
+ if (mList != null) {
+ //noinspection unchecked
+ initializeKey = (Key) mList.getLastKey();
+ }
+
+ do {
+ if (mDataSource != null) {
+ mDataSource.removeInvalidatedCallback(this);
+ }
+ mDataSource = mDataSourceFactory.create();
+ mDataSource.addInvalidatedCallback(this);
+
+ mList = new PagedList.Builder<>(mDataSource, mConfig)
+ .setNotifyExecutor(mNotifyExecutor)
+ .setFetchExecutor(mFetchExecutor)
+ .setBoundaryCallback(mBoundaryCallback)
+ .setInitialKey(initializeKey)
+ .build();
+ } while (mList.isDetached());
+ return mList;
+ }
+ }
+}
diff --git a/androidx/paging/WrapperItemKeyedDataSource.java b/androidx/paging/WrapperItemKeyedDataSource.java
index 72a6d5cd..1583f9d5 100644
--- a/androidx/paging/WrapperItemKeyedDataSource.java
+++ b/androidx/paging/WrapperItemKeyedDataSource.java
@@ -25,13 +25,6 @@ import java.util.List;
class WrapperItemKeyedDataSource<K, A, B> extends ItemKeyedDataSource<K, B> {
private final ItemKeyedDataSource<K, A> mSource;
private final Function<List<A>, List<B>> mListFunction;
- private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- removeCallback();
- }
- };
private final IdentityHashMap<B, K> mKeyMap = new IdentityHashMap<>();
@@ -39,11 +32,26 @@ class WrapperItemKeyedDataSource<K, A, B> extends ItemKeyedDataSource<K, B> {
Function<List<A>, List<B>> listFunction) {
mSource = source;
mListFunction = listFunction;
- mSource.addInvalidatedCallback(mInvalidatedCallback);
}
- private void removeCallback() {
- mSource.removeInvalidatedCallback(mInvalidatedCallback);
+ @Override
+ public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mSource.addInvalidatedCallback(onInvalidatedCallback);
+ }
+
+ @Override
+ public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mSource.removeInvalidatedCallback(onInvalidatedCallback);
+ }
+
+ @Override
+ public void invalidate() {
+ mSource.invalidate();
+ }
+
+ @Override
+ public boolean isInvalid() {
+ return mSource.isInvalid();
}
private List<B> convertWithStashedKeys(List<A> source) {
diff --git a/androidx/paging/WrapperPageKeyedDataSource.java b/androidx/paging/WrapperPageKeyedDataSource.java
index 7675026e..4c947d26 100644
--- a/androidx/paging/WrapperPageKeyedDataSource.java
+++ b/androidx/paging/WrapperPageKeyedDataSource.java
@@ -25,23 +25,31 @@ import java.util.List;
class WrapperPageKeyedDataSource<K, A, B> extends PageKeyedDataSource<K, B> {
private final PageKeyedDataSource<K, A> mSource;
private final Function<List<A>, List<B>> mListFunction;
- private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- removeCallback();
- }
- };
WrapperPageKeyedDataSource(PageKeyedDataSource<K, A> source,
Function<List<A>, List<B>> listFunction) {
mSource = source;
mListFunction = listFunction;
- mSource.addInvalidatedCallback(mInvalidatedCallback);
}
- private void removeCallback() {
- mSource.removeInvalidatedCallback(mInvalidatedCallback);
+ @Override
+ public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mSource.addInvalidatedCallback(onInvalidatedCallback);
+ }
+
+ @Override
+ public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mSource.removeInvalidatedCallback(onInvalidatedCallback);
+ }
+
+ @Override
+ public void invalidate() {
+ mSource.invalidate();
+ }
+
+ @Override
+ public boolean isInvalid() {
+ return mSource.isInvalid();
}
@Override
diff --git a/androidx/paging/WrapperPositionalDataSource.java b/androidx/paging/WrapperPositionalDataSource.java
index 257f6c7c..3b739ea7 100644
--- a/androidx/paging/WrapperPositionalDataSource.java
+++ b/androidx/paging/WrapperPositionalDataSource.java
@@ -25,23 +25,30 @@ class WrapperPositionalDataSource<A, B> extends PositionalDataSource<B> {
private final PositionalDataSource<A> mSource;
private final Function<List<A>, List<B>> mListFunction;
- private final InvalidatedCallback mInvalidatedCallback = new DataSource.InvalidatedCallback() {
- @Override
- public void onInvalidated() {
- invalidate();
- removeCallback();
- }
- };
-
WrapperPositionalDataSource(PositionalDataSource<A> source,
Function<List<A>, List<B>> listFunction) {
mSource = source;
mListFunction = listFunction;
- mSource.addInvalidatedCallback(mInvalidatedCallback);
}
- private void removeCallback() {
- mSource.removeInvalidatedCallback(mInvalidatedCallback);
+ @Override
+ public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mSource.addInvalidatedCallback(onInvalidatedCallback);
+ }
+
+ @Override
+ public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
+ mSource.removeInvalidatedCallback(onInvalidatedCallback);
+ }
+
+ @Override
+ public void invalidate() {
+ mSource.invalidate();
+ }
+
+ @Override
+ public boolean isInvalid() {
+ return mSource.isInvalid();
}
@Override
diff --git a/androidx/preference/CollapsiblePreferenceGroupController.java b/androidx/preference/CollapsiblePreferenceGroupController.java
index bcc71ce2..3e1d7921 100644
--- a/androidx/preference/CollapsiblePreferenceGroupController.java
+++ b/androidx/preference/CollapsiblePreferenceGroupController.java
@@ -133,7 +133,8 @@ final class CollapsiblePreferenceGroupController {
private ExpandButton createExpandButton(final PreferenceGroup group,
List<Preference> collapsedPreferences) {
- final ExpandButton preference = new ExpandButton(mContext, collapsedPreferences);
+ final ExpandButton preference = new ExpandButton(mContext, collapsedPreferences,
+ group.getId());
preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
@@ -150,10 +151,16 @@ final class CollapsiblePreferenceGroupController {
* {@link PreferenceGroup}.
*/
static class ExpandButton extends Preference {
- ExpandButton(Context context, List<Preference> collapsedPreferences) {
+ private long mId;
+
+ ExpandButton(Context context, List<Preference> collapsedPreferences, long parentId) {
super(context);
initLayout();
setSummary(collapsedPreferences);
+ // Since IDs are unique, using the parentId as a reference ensures that this expand
+ // button will have a unique ID and hence transitions will be correctly animated by
+ // RecyclerView when there are multiple ExpandButtons.
+ mId = parentId + 1000000;
}
private void initLayout() {
@@ -201,6 +208,11 @@ final class CollapsiblePreferenceGroupController {
super.onBindViewHolder(holder);
holder.setDividerAllowedAbove(false);
}
+
+ @Override
+ public long getId() {
+ return mId;
+ }
}
} \ No newline at end of file
diff --git a/androidx/recyclerview/selection/BandSelectionHelper.java b/androidx/recyclerview/selection/BandSelectionHelper.java
index 883fcbfe..6aabd454 100644
--- a/androidx/recyclerview/selection/BandSelectionHelper.java
+++ b/androidx/recyclerview/selection/BandSelectionHelper.java
@@ -48,7 +48,7 @@ import java.util.Set;
* the user interacts with items using their pointer (and the band). Selectable items that intersect
* with the band, both on and off screen, are selected on pointer up.
*
- * @see SelectionTracker.Builder#withBandTooltypes(int...) for details on the specific
+ * @see SelectionTracker.Builder#withPointerTooltypes(int...) for details on the specific
* tooltypes routed to this helper.
*
* @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
diff --git a/androidx/recyclerview/selection/DefaultSelectionTracker.java b/androidx/recyclerview/selection/DefaultSelectionTracker.java
index c7be1b85..4e65be8a 100644
--- a/androidx/recyclerview/selection/DefaultSelectionTracker.java
+++ b/androidx/recyclerview/selection/DefaultSelectionTracker.java
@@ -30,6 +30,7 @@ import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.selection.Range.RangeType;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
import java.util.ArrayList;
import java.util.List;
@@ -60,6 +61,7 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
private final SelectionPredicate<K> mSelectionPredicate;
private final StorageStrategy<K> mStorage;
private final RangeCallbacks mRangeCallbacks;
+ private final AdapterObserver mAdapterObserver;
private final boolean mSingleSelect;
private final String mSelectionId;
@@ -94,6 +96,8 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
mRangeCallbacks = new RangeCallbacks();
mSingleSelect = !selectionPredicate.canSelectMultiple();
+
+ mAdapterObserver = new AdapterObserver(this);
}
@Override
@@ -344,7 +348,11 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
}
@Override
- void onDataSetChanged() {
+ AdapterDataObserver getAdapterDataObserver() {
+ return mAdapterObserver;
+ }
+
+ private void onDataSetChanged() {
mSelection.clearProvisionalSelection();
notifySelectionRefresh();
@@ -524,4 +532,41 @@ public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
}
}
}
+
+ private static final class AdapterObserver extends AdapterDataObserver {
+
+ private final DefaultSelectionTracker<?> mSelectionTracker;
+
+ AdapterObserver(@NonNull DefaultSelectionTracker<?> selectionTracker) {
+ checkArgument(selectionTracker != null);
+ mSelectionTracker = selectionTracker;
+ }
+
+ @Override
+ public void onChanged() {
+ mSelectionTracker.onDataSetChanged();
+ }
+
+ @Override
+ public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
+ if (!SelectionTracker.SELECTION_CHANGED_MARKER.equals(payload)) {
+ mSelectionTracker.onDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int startPosition, int itemCount) {
+ mSelectionTracker.endRange();
+ }
+
+ @Override
+ public void onItemRangeRemoved(int startPosition, int itemCount) {
+ mSelectionTracker.endRange();
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ mSelectionTracker.endRange();
+ }
+ }
}
diff --git a/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java b/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java
index 525c264d..f7512ca5 100644
--- a/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java
+++ b/androidx/recyclerview/selection/DefaultSelectionTrackerTest.java
@@ -303,7 +303,7 @@ public class DefaultSelectionTrackerTest {
@Test
public void testProvisionalSelection() {
- Selection s = mTracker.getSelection();
+ Selection<String> s = mTracker.getSelection();
mSelection.assertNoSelection();
// Mimicking band selection case -- BandController notifies item callback by itself.
@@ -319,7 +319,7 @@ public class DefaultSelectionTrackerTest {
@Test
public void testProvisionalSelection_Replace() {
- Selection s = mTracker.getSelection();
+ Selection<String> s = mTracker.getSelection();
// Mimicking band selection case -- BandController notifies item callback by itself.
mListener.onItemStateChanged(mItems.get(1), true);
@@ -343,7 +343,7 @@ public class DefaultSelectionTrackerTest {
@Test
public void testProvisionalSelection_IntersectsExistingProvisionalSelection() {
- Selection s = mTracker.getSelection();
+ Selection<String> s = mTracker.getSelection();
// Mimicking band selection case -- BandController notifies item callback by itself.
mListener.onItemStateChanged(mItems.get(1), true);
@@ -365,7 +365,7 @@ public class DefaultSelectionTrackerTest {
@Test
public void testProvisionalSelection_Apply() {
- Selection s = mTracker.getSelection();
+ Selection<String> s = mTracker.getSelection();
// Mimicking band selection case -- BandController notifies item callback by itself.
mListener.onItemStateChanged(mItems.get(1), true);
@@ -383,7 +383,7 @@ public class DefaultSelectionTrackerTest {
public void testProvisionalSelection_Cancel() {
mTracker.select(mItems.get(1));
mTracker.select(mItems.get(2));
- Selection s = mTracker.getSelection();
+ Selection<String> s = mTracker.getSelection();
SparseBooleanArray provisional = new SparseBooleanArray();
provisional.append(3, true);
@@ -399,7 +399,7 @@ public class DefaultSelectionTrackerTest {
public void testProvisionalSelection_IntersectsAppliedSelection() {
mTracker.select(mItems.get(1));
mTracker.select(mItems.get(2));
- Selection s = mTracker.getSelection();
+ Selection<String> s = mTracker.getSelection();
// Mimicking band selection case -- BandController notifies item callback by itself.
mListener.onItemStateChanged(mItems.get(3), true);
diff --git a/androidx/recyclerview/selection/EventBridge.java b/androidx/recyclerview/selection/EventBridge.java
index e117944f..6c43f7e0 100644
--- a/androidx/recyclerview/selection/EventBridge.java
+++ b/androidx/recyclerview/selection/EventBridge.java
@@ -17,13 +17,13 @@
package androidx.recyclerview.selection;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import static androidx.core.util.Preconditions.checkArgument;
import static androidx.recyclerview.selection.Shared.VERBOSE;
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
@@ -39,7 +39,7 @@ import androidx.recyclerview.widget.RecyclerView;
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
-@VisibleForTesting
+@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public class EventBridge {
private static final String TAG = "EventsRelays";
@@ -58,49 +58,9 @@ public class EventBridge {
@NonNull SelectionTracker<K> selectionTracker,
@NonNull ItemKeyProvider<K> keyProvider) {
- // setup bridges to relay selection events.
- new AdapterToTrackerBridge(adapter, selectionTracker);
+ // setup bridges to relay selection and adapter events
new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
- }
-
- private static final class AdapterToTrackerBridge extends RecyclerView.AdapterDataObserver {
-
- private final SelectionTracker<?> mSelectionTracker;
-
- AdapterToTrackerBridge(
- @NonNull RecyclerView.Adapter<?> adapter,
- @NonNull SelectionTracker<?> selectionTracker) {
- adapter.registerAdapterDataObserver(this);
-
- checkArgument(selectionTracker != null);
- mSelectionTracker = selectionTracker;
- }
-
- @Override
- public void onChanged() {
- mSelectionTracker.onDataSetChanged();
- }
-
- @Override
- public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
- // No change in position. Ignore.
- // TODO(b/72393576): Properties of items could change. Should reevaluate selected status
- }
-
- @Override
- public void onItemRangeInserted(int startPosition, int itemCount) {
- // Uninteresting to us since selection is stable ID based.
- }
-
- @Override
- public void onItemRangeRemoved(int startPosition, int itemCount) {
- // Uninteresting to us since selection is stable ID based.
- }
-
- @Override
- public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
- // Uninteresting to us since selection is stable ID based.
- }
+ adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
}
private static final class TrackerToAdapterBridge<K>
diff --git a/androidx/recyclerview/selection/GestureSelectionHelper.java b/androidx/recyclerview/selection/GestureSelectionHelper.java
index a780bda5..ea7ec16a 100644
--- a/androidx/recyclerview/selection/GestureSelectionHelper.java
+++ b/androidx/recyclerview/selection/GestureSelectionHelper.java
@@ -75,7 +75,13 @@ final class GestureSelectionHelper implements OnItemTouchListener {
*/
void start() {
checkState(!mStarted);
- checkState(mLastStartedItemPos > -1);
+ // See: b/70518185. It appears start() is being called via onLongPress
+ // even though we never received an intial handleInterceptedDownEvent
+ // where we would usually initialize mLastStartedItemPos.
+ if (mLastStartedItemPos < 0) {
+ Log.w(TAG, "Illegal state. Can't start without valid mLastStartedItemPos.");
+ return;
+ }
// Partner code in MotionInputHandler ensures items
// are selected and range established prior to
diff --git a/androidx/recyclerview/selection/GestureSelectionHelperTest.java b/androidx/recyclerview/selection/GestureSelectionHelperTest.java
index 4c109303..9fd494d3 100644
--- a/androidx/recyclerview/selection/GestureSelectionHelperTest.java
+++ b/androidx/recyclerview/selection/GestureSelectionHelperTest.java
@@ -18,7 +18,6 @@ package androidx.recyclerview.selection;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -56,17 +55,17 @@ public class GestureSelectionHelperTest {
private GestureSelectionHelper mHelper;
private SelectionTracker<String> mSelectionTracker;
private SelectionProbe mSelection;
- private OperationMonitor mLock;
+ private OperationMonitor mMonitor;
private TestViewDelegate mView;
@Before
public void setUp() {
mSelectionTracker = SelectionTrackers.createStringTracker("gesture-selection-test", 100);
mSelection = new SelectionProbe(mSelectionTracker);
- mLock = new OperationMonitor();
+ mMonitor = new OperationMonitor();
mView = new TestViewDelegate();
mHelper = new GestureSelectionHelper(
- mSelectionTracker, mView, new TestAutoScroller(), mLock);
+ mSelectionTracker, mView, new TestAutoScroller(), mMonitor);
}
@Test
@@ -78,12 +77,10 @@ public class GestureSelectionHelperTest {
@Test
public void testNoStartOnIllegalPosition() {
+ mView.mNextPosition = RecyclerView.NO_POSITION;
mHelper.onInterceptTouchEvent(null, DOWN);
- try {
- mHelper.start();
- fail("Should have thrown.");
- } catch (Exception expected) {
- }
+ mHelper.start();
+ assertFalse(mMonitor.isStarted());
}
@Test
diff --git a/androidx/recyclerview/selection/GridModel.java b/androidx/recyclerview/selection/GridModel.java
index f496b0cc..abc21440 100644
--- a/androidx/recyclerview/selection/GridModel.java
+++ b/androidx/recyclerview/selection/GridModel.java
@@ -27,7 +27,6 @@ import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
@@ -169,7 +168,6 @@ final class GridModel<K> {
* though its
* absolute point has a higher y-value.
*/
- @VisibleForTesting
void resizeSelection(Point relativePointer) {
mPointer = mHost.createAbsolutePoint(relativePointer);
updateModel();
diff --git a/androidx/recyclerview/selection/MotionEvents.java b/androidx/recyclerview/selection/MotionEvents.java
index 90d79e64..bc47a76c 100644
--- a/androidx/recyclerview/selection/MotionEvents.java
+++ b/androidx/recyclerview/selection/MotionEvents.java
@@ -104,6 +104,16 @@ final class MotionEvents {
return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
}
+ /**
+ * Returns true if the event is a drag event (which is presumbaly, but not
+ * explicitly required to be a mouse event).
+ * @param e
+ */
+ static boolean isPointerDragEvent(MotionEvent e) {
+ return isPrimaryMouseButtonPressed(e)
+ && isActionMove(e);
+ }
+
private static boolean hasBit(int metaState, int bit) {
return (metaState & bit) != 0;
}
diff --git a/androidx/recyclerview/selection/OnDragInitiatedListener.java b/androidx/recyclerview/selection/OnDragInitiatedListener.java
index a479872b..50d8f1b3 100644
--- a/androidx/recyclerview/selection/OnDragInitiatedListener.java
+++ b/androidx/recyclerview/selection/OnDragInitiatedListener.java
@@ -16,31 +16,58 @@
package androidx.recyclerview.selection;
+import static androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+import android.content.ClipData;
import android.view.MotionEvent;
+import android.view.View;
import androidx.annotation.NonNull;
/**
- * Register an OnDragInitiatedListener to be notified of potential drag operations,
- * and to handle them.
+ * Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop
+ * operations on an item or items has been detected. Handle these events using {@link View}
+ * support for Drag and drop.
+ *
+ * <p>
+ * See {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
+ * for details.
*/
public interface OnDragInitiatedListener {
/**
- * Called when a drag is initiated. Touch input handler only considers
- * a drag to be initiated on long press on an existing selection,
- * as normal touch and drag events are strongly associated with scrolling of the view.
+ * Called when user intent to perform a drag and drop operation has been detected.
*
* <p>
- * Drag will only be initiated when the item under the event is already selected.
+ * The following circumstances are considered to be expressing drag and drop intent:
+ *
+ * <ol>
+ * <li>Long press on selected item.</li>
+ * <li>Click and drag in the {@link ItemDetails#inDragRegion(MotionEvent) drag region}
+ * of selected item with a pointer device.</li>
+ * <li>Click and drag in drag region of un-selected item with a pointer device.</li>
+ * </ol>
*
* <p>
* The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
- * to this method as there may be multiple items selected. Clients can obtain the current
- * list of selected items from {@link SelectionTracker#copySelection(MutableSelection)}.
+ * to this method as there may be multiple items selected or no items selected (as may be
+ * the case in pointer drive drag and drop.)
+ *
+ * <p>
+ * Obtain the current list of selected items from
+ * {@link SelectionTracker#copySelection(MutableSelection)}. If there is no selection
+ * get the item under the event using {@link ItemDetailsLookup#getItemDetails(MotionEvent)}.
+ *
+ * <p>
+ * Drag region used with pointer devices is specified by
+ * {@link ItemDetails#inDragRegion(MotionEvent)}
+ *
+ * <p>
+ * See {@link android.view.View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
+ * for details on drag and drop implementation.
*
* @param e the event associated with the drag.
- * @return true if the event was handled.
+ * @return true if drag and drop was initiated.
*/
boolean onDragInitiated(@NonNull MotionEvent e);
}
diff --git a/androidx/recyclerview/selection/PointerDragEventInterceptor.java b/androidx/recyclerview/selection/PointerDragEventInterceptor.java
new file mode 100644
index 00000000..46ec5ddf
--- /dev/null
+++ b/androidx/recyclerview/selection/PointerDragEventInterceptor.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.recyclerview.selection;
+
+import static androidx.core.util.Preconditions.checkArgument;
+
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
+
+/**
+ * OnItemTouchListener that delegates drag events to a drag listener,
+ * else sends event to fallback {@link OnItemTouchListener}.
+ *
+ * <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop.
+ */
+final class PointerDragEventInterceptor implements OnItemTouchListener {
+
+ private final ItemDetailsLookup mEventDetailsLookup;
+ private final OnDragInitiatedListener mDragListener;
+ private @Nullable OnItemTouchListener mDelegate;
+
+ PointerDragEventInterceptor(
+ ItemDetailsLookup eventDetailsLookup,
+ OnDragInitiatedListener dragListener,
+ @Nullable OnItemTouchListener delegate) {
+
+ checkArgument(eventDetailsLookup != null);
+ checkArgument(dragListener != null);
+
+ mEventDetailsLookup = eventDetailsLookup;
+ mDragListener = dragListener;
+ mDelegate = delegate;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
+ return mDragListener.onDragInitiated(e);
+ } else if (mDelegate != null) {
+ return mDelegate.onInterceptTouchEvent(rv, e);
+ }
+ return false;
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (mDelegate != null) {
+ mDelegate.onTouchEvent(rv, e);
+ }
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (mDelegate != null) {
+ mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ }
+}
diff --git a/androidx/recyclerview/selection/SelectionTest.java b/androidx/recyclerview/selection/SelectionTest.java
index eb2402b0..b8c65837 100644
--- a/androidx/recyclerview/selection/SelectionTest.java
+++ b/androidx/recyclerview/selection/SelectionTest.java
@@ -41,11 +41,11 @@ public class SelectionTest {
"auth|id=@53di*/f3#d"
};
- private Selection mSelection;
+ private Selection<String> mSelection;
@Before
public void setUp() throws Exception {
- mSelection = new Selection();
+ mSelection = new Selection<>();
mSelection.add(mIds[0]);
mSelection.add(mIds[1]);
mSelection.add(mIds[2]);
@@ -96,14 +96,14 @@ public class SelectionTest {
@Test
public void testIsEmpty() {
- assertTrue(new Selection().isEmpty());
+ assertTrue(new Selection<>().isEmpty());
mSelection.clear();
assertTrue(mSelection.isEmpty());
}
@Test
public void testSize() {
- Selection other = new Selection();
+ Selection<String> other = new Selection<>();
for (int i = 0; i < mSelection.size(); i++) {
other.add(mIds[i]);
}
@@ -117,7 +117,7 @@ public class SelectionTest {
@Test
public void testEqualsOther() {
- Selection other = new Selection();
+ Selection<String> other = new Selection<>();
other.add(mIds[0]);
other.add(mIds[1]);
other.add(mIds[2]);
@@ -127,7 +127,7 @@ public class SelectionTest {
@Test
public void testEqualsCopy() {
- Selection other = new Selection();
+ Selection<String> other = new Selection<>();
other.copyFrom(mSelection);
assertEquals(mSelection, other);
assertEquals(mSelection.hashCode(), other.hashCode());
@@ -135,7 +135,7 @@ public class SelectionTest {
@Test
public void testNotEquals() {
- Selection other = new Selection();
+ Selection<String> other = new Selection<>();
other.add("foobar");
assertFalse(mSelection.equals(other));
}
diff --git a/androidx/recyclerview/selection/SelectionTracker.java b/androidx/recyclerview/selection/SelectionTracker.java
index 2b35f5d7..283426f6 100644
--- a/androidx/recyclerview/selection/SelectionTracker.java
+++ b/androidx/recyclerview/selection/SelectionTracker.java
@@ -29,6 +29,8 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import java.util.Set;
@@ -119,7 +121,7 @@ public abstract class SelectionTracker<K> {
* of the selection that will not reflect future changes
* to selection.
*/
- public abstract Selection getSelection();
+ public abstract Selection<K> getSelection();
/**
* Updates {@code dest} to reflect the current selection.
@@ -175,7 +177,7 @@ public abstract class SelectionTracker<K> {
*/
public abstract boolean deselect(@NonNull K key);
- abstract void onDataSetChanged();
+ abstract AdapterDataObserver getAdapterDataObserver();
/**
* Attempts to establish a range selection at {@code position}, selecting the item
@@ -471,7 +473,7 @@ public abstract class SelectionTracker<K> {
MotionEvent.TOOL_TYPE_UNKNOWN
};
- private int[] mBandToolTypes = new int[] {
+ private int[] mPointerToolTypes = new int[] {
MotionEvent.TOOL_TYPE_MOUSE
};
@@ -637,14 +639,16 @@ public abstract class SelectionTracker<K> {
}
/**
- * Replaces default band selection tool-types. Defaults are:
+ * Replaces default pointer tool-types. Pointer tools
+ * are associated with band selection, and certain
+ * drag and drop behaviors. Defaults are:
* {@link MotionEvent#TOOL_TYPE_MOUSE}.
*
* @param toolTypes the tool types to be used
* @return this
*/
- public Builder<K> withBandTooltypes(int... toolTypes) {
- mBandToolTypes = toolTypes;
+ public Builder<K> withPointerTooltypes(int... toolTypes) {
+ mPointerToolTypes = toolTypes;
return this;
}
@@ -769,10 +773,12 @@ public abstract class SelectionTracker<K> {
mOnItemActivatedListener,
mFocusDelegate);
- for (int toolType : mBandToolTypes) {
+ for (int toolType : mPointerToolTypes) {
gestureRouter.register(toolType, mouseHandler);
}
+ @Nullable BandSelectionHelper bandHelper = null;
+
// Band selection not supported in single select mode, or when key access
// is limited to anything less than the entire corpus.
if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
@@ -782,7 +788,7 @@ public abstract class SelectionTracker<K> {
// necessarily models and caches list/grid information as the user's pointer
// interacts with the item in the RecyclerView. Selectable items that intersect
// with the band, both on and off screen, are selected.
- BandSelectionHelper bandHelper = BandSelectionHelper.create(
+ bandHelper = BandSelectionHelper.create(
mRecyclerView,
scroller,
mBandOverlayId,
@@ -792,10 +798,13 @@ public abstract class SelectionTracker<K> {
mBandPredicate,
mFocusDelegate,
mMonitor);
+ }
+
+ OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
+ mDetailsLookup, mOnDragInitiatedListener, bandHelper);
- for (int toolType : mBandToolTypes) {
- eventRouter.register(toolType, bandHelper);
- }
+ for (int toolType : mPointerToolTypes) {
+ eventRouter.register(toolType, pointerEventHandler);
}
return tracker;
diff --git a/androidx/recyclerview/selection/testing/SelectionProbe.java b/androidx/recyclerview/selection/testing/SelectionProbe.java
index e8ae6076..8ac4baf5 100644
--- a/androidx/recyclerview/selection/testing/SelectionProbe.java
+++ b/androidx/recyclerview/selection/testing/SelectionProbe.java
@@ -64,7 +64,7 @@ public final class SelectionProbe {
}
public void assertSelectionSize(int expected) {
- Selection selection = mMgr.getSelection();
+ Selection<String> selection = mMgr.getSelection();
assertEquals(selection.toString(), expected, selection.size());
mSelectionListener.assertSelectionSize(expected);
diff --git a/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index 25aa00d6..60eb6d24 100644
--- a/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -1180,7 +1180,16 @@ public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest {
adapter.addAndNotify(5 + (i % 3) * 3, 1);
Thread.sleep(25);
}
- smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
+
+ final AtomicInteger lastVisiblePosition = new AtomicInteger();
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ lastVisiblePosition.set(mLayoutManager.findLastVisibleItemPosition());
+ }
+ });
+
+ smoothScrollToPosition(lastVisiblePosition.get() + 20);
waitForAnimations(2);
getInstrumentation().waitForIdleSync();
assertEquals("Children count should add up", childCount.get(),
diff --git a/androidx/room/ForeignKey.java b/androidx/room/ForeignKey.java
index 847941a6..54b92527 100644
--- a/androidx/room/ForeignKey.java
+++ b/androidx/room/ForeignKey.java
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.room;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+
/**
* Declares a foreign key on another {@link Entity}.
* <p>
@@ -161,6 +164,7 @@ public @interface ForeignKey {
* {@link #onUpdate()}.
*/
@IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
+ @Retention(SOURCE)
@interface Action {
}
}
diff --git a/androidx/room/integration/testapp/CustomerViewModel.java b/androidx/room/integration/testapp/CustomerViewModel.java
index eca2552a..d0ac7d8d 100644
--- a/androidx/room/integration/testapp/CustomerViewModel.java
+++ b/androidx/room/integration/testapp/CustomerViewModel.java
@@ -25,6 +25,7 @@ import androidx.lifecycle.LiveData;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
+import androidx.paging.RxPagedListBuilder;
import androidx.room.Room;
import androidx.room.integration.testapp.database.Customer;
import androidx.room.integration.testapp.database.LastNameAscCustomerDataSource;
@@ -32,6 +33,9 @@ import androidx.room.integration.testapp.database.SampleDatabase;
import java.util.UUID;
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+
/**
* Sample database-backed view model of Customers
*/
@@ -48,20 +52,17 @@ public class CustomerViewModel extends AndroidViewModel {
mDatabase = Room.databaseBuilder(this.getApplication(),
SampleDatabase.class, "customerDatabase").build();
- ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
- @Override
- public void run() {
- // fill with some simple data
- int customerCount = mDatabase.getCustomerDao().countCustomers();
- if (customerCount == 0) {
- Customer[] initialCustomers = new Customer[10];
- for (int i = 0; i < 10; i++) {
- initialCustomers[i] = createCustomer();
- }
- mDatabase.getCustomerDao().insertAll(initialCustomers);
+ ArchTaskExecutor.getInstance().executeOnDiskIO(() -> {
+ // fill with some simple data
+ int customerCount = mDatabase.getCustomerDao().countCustomers();
+ if (customerCount == 0) {
+ Customer[] initialCustomers = new Customer[10];
+ for (int i = 0; i < 10; i++) {
+ initialCustomers[i] = createCustomer();
}
-
+ mDatabase.getCustomerDao().insertAll(initialCustomers);
}
+
});
}
@@ -74,12 +75,13 @@ public class CustomerViewModel extends AndroidViewModel {
}
void insertCustomer() {
- ArchTaskExecutor.getInstance().executeOnDiskIO(new Runnable() {
- @Override
- public void run() {
- mDatabase.getCustomerDao().insert(createCustomer());
- }
- });
+ ArchTaskExecutor.getInstance().executeOnDiskIO(
+ () -> mDatabase.getCustomerDao().insert(createCustomer()));
+ }
+
+ void clearAllCustomers() {
+ ArchTaskExecutor.getInstance().executeOnDiskIO(
+ () -> mDatabase.getCustomerDao().removeAll());
}
private static <K> LiveData<PagedList<Customer>> getLivePagedList(
@@ -93,6 +95,20 @@ public class CustomerViewModel extends AndroidViewModel {
.build();
}
+ private static <K> Flowable<PagedList<Customer>> getPagedListFlowable(
+ DataSource.Factory<K, Customer> dataSourceFactory) {
+ PagedList.Config config = new PagedList.Config.Builder()
+ .setPageSize(10)
+ .setEnablePlaceholders(false)
+ .build();
+ return new RxPagedListBuilder<>(dataSourceFactory, config)
+ .buildFlowable(BackpressureStrategy.LATEST);
+ }
+
+ Flowable<PagedList<Customer>> getPagedListFlowable() {
+ return getPagedListFlowable(mDatabase.getCustomerDao().loadPagedAgeOrder());
+ }
+
LiveData<PagedList<Customer>> getLivePagedList(int position) {
if (mLiveCustomerList == null) {
mLiveCustomerList =
diff --git a/androidx/room/integration/testapp/RoomPagedListRxActivity.java b/androidx/room/integration/testapp/RoomPagedListRxActivity.java
new file mode 100644
index 00000000..16d24a5d
--- /dev/null
+++ b/androidx/room/integration/testapp/RoomPagedListRxActivity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.testapp;
+
+import android.os.Bundle;
+import android.widget.Button;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.recyclerview.widget.RecyclerView;
+
+import io.reactivex.disposables.CompositeDisposable;
+
+/**
+ * Sample {@code Flowable<PagedList>} activity which uses Room.
+ */
+public class RoomPagedListRxActivity extends AppCompatActivity {
+
+ private PagedListCustomerAdapter mAdapter;
+
+ private final CompositeDisposable mDisposable = new CompositeDisposable();
+ private CustomerViewModel mViewModel;
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_recycler_view);
+ mViewModel = ViewModelProviders.of(this)
+ .get(CustomerViewModel.class);
+
+ RecyclerView recyclerView = findViewById(R.id.recyclerview);
+ mAdapter = new PagedListCustomerAdapter();
+ recyclerView.setAdapter(mAdapter);
+
+ final Button addButton = findViewById(R.id.addButton);
+ addButton.setOnClickListener(v -> mViewModel.insertCustomer());
+
+ final Button clearButton = findViewById(R.id.clearButton);
+ clearButton.setOnClickListener(v -> mViewModel.clearAllCustomers());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ mDisposable.add(mViewModel.getPagedListFlowable()
+ .subscribe(list -> mAdapter.submitList(list)));
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mDisposable.clear();
+ }
+}
diff --git a/androidx/room/integration/testapp/database/CustomerDao.java b/androidx/room/integration/testapp/database/CustomerDao.java
index 311a593c..1aa01635 100644
--- a/androidx/room/integration/testapp/database/CustomerDao.java
+++ b/androidx/room/integration/testapp/database/CustomerDao.java
@@ -44,6 +44,12 @@ public interface CustomerDao {
void insertAll(Customer[] customers);
/**
+ * Delete all customers
+ */
+ @Query("DELETE FROM customer")
+ void removeAll();
+
+ /**
* @return DataSource.Factory of customers, ordered by last name. Use
* {@link androidx.paging.LivePagedListBuilder} to get a LiveData of PagedLists.
*/
diff --git a/androidx/room/integration/testapp/test/ClearAllTablesTest.java b/androidx/room/integration/testapp/test/ClearAllTablesTest.java
index 8bffdef9..76cc06ad 100644
--- a/androidx/room/integration/testapp/test/ClearAllTablesTest.java
+++ b/androidx/room/integration/testapp/test/ClearAllTablesTest.java
@@ -16,11 +16,13 @@
package androidx.room.integration.testapp.test;
+import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
import android.content.Context;
import android.database.Cursor;
@@ -133,6 +135,29 @@ public class ClearAllTablesTest {
@Test
@SmallTest
+ public void inTransaction() {
+ mDao.insertParent(new Parent(1, "A"));
+ assertThat(mDao.countParent(), is(1));
+ // Running clearAllTables in a transaction is not recommended, but we should not crash.
+ mDatabase.runInTransaction(() -> mDatabase.clearAllTables());
+ assertThat(mDao.countParent(), is(0));
+ }
+
+ @Test
+ @SmallTest
+ public void inMainThread() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ try {
+ mDatabase.clearAllTables();
+ fail("Was expecting an exception");
+ } catch (IllegalStateException e) {
+ assertThat(e.getMessage(), containsString("main thread"));
+ }
+ });
+ }
+
+ @Test
+ @SmallTest
public void foreignKey() {
mDao.insertParent(new Parent(1, "A"));
mDao.insertChild(new Child(1, "a", 1));
@@ -187,12 +212,12 @@ public class ClearAllTablesTest {
db.dao().insertParent(new Parent(1, uuid));
assertThat(queryEncoding(db), is(equalTo("UTF-8")));
db.close();
- assertThat(containsString(file, uuid), is(true));
+ assertThat(fileContainsString(file, uuid), is(true));
db = Room.databaseBuilder(context, ClearAllTablesDatabase.class, dbName)
.setJournalMode(journalMode).build();
db.clearAllTables();
db.close();
- assertThat(containsString(file, uuid), is(false));
+ assertThat(fileContainsString(file, uuid), is(false));
}
private String queryEncoding(RoomDatabase db) {
@@ -208,7 +233,7 @@ public class ClearAllTablesTest {
}
}
- private boolean containsString(File file, String s) throws IOException {
+ private boolean fileContainsString(File file, String s) throws IOException {
final byte[] content = new byte[(int) file.length()];
final FileInputStream stream = new FileInputStream(file);
//noinspection TryFinallyCanBeTryWithResources
diff --git a/androidx/room/integration/testapp/test/RxJava2Test.java b/androidx/room/integration/testapp/test/RxJava2Test.java
index 9878cd27..5ea9efc7 100644
--- a/androidx/room/integration/testapp/test/RxJava2Test.java
+++ b/androidx/room/integration/testapp/test/RxJava2Test.java
@@ -40,6 +40,9 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Predicate;
import io.reactivex.observers.TestObserver;
@@ -135,6 +138,42 @@ public class RxJava2Test extends TestDatabaseTest {
}
@Test
+ public void maybeUsers_keepMaybeReference() throws InterruptedException {
+ User[] users = TestUtil.createUsersArray(1, 2);
+ mUserDao.insertAll(users);
+ TestObserver<User> testObserver1 = new TestObserver<>();
+ Maybe<User> maybe1 = mUserDao.maybeUserById(1);
+ Disposable disposable1 = maybe1.observeOn(mTestScheduler)
+ .subscribeWith(testObserver1);
+ drain();
+ testObserver1.assertComplete();
+ // since this is a clean db, it is ok to rely on the order for the test.
+ testObserver1.assertValue(users[0]);
+
+ TestObserver<User> testObserver2 = new TestObserver<>();
+ Maybe<User> maybe2 = mUserDao.maybeUserById(2);
+ Disposable disposable2 = maybe2.observeOn(mTestScheduler)
+ .subscribeWith(testObserver2);
+ drain();
+ testObserver2.assertComplete();
+ // since this is a clean db, it is ok to rely on the order for the test.
+ testObserver2.assertValue(users[1]);
+
+ TestObserver<User> testObserver3 = new TestObserver<>();
+
+ Disposable disposable3 = maybe1.observeOn(mTestScheduler)
+ .subscribeWith(testObserver3);
+ drain();
+ testObserver3.assertComplete();
+ // since this is a clean db, it is ok to rely on the order for the test.
+ testObserver3.assertValue(users[0]);
+
+ disposable1.dispose();
+ disposable2.dispose();
+ disposable3.dispose();
+ }
+
+ @Test
public void singleUser_Empty() throws InterruptedException {
TestObserver<User> testObserver = new TestObserver<>();
Disposable disposable = mUserDao.singleUserById(3).observeOn(mTestScheduler)
@@ -186,6 +225,40 @@ public class RxJava2Test extends TestDatabaseTest {
}
@Test
+ public void singleUser_keepSingleReference() throws InterruptedException {
+ User[] users = TestUtil.createUsersArray(1, 2);
+ mUserDao.insertAll(users);
+ TestObserver<User> testObserver1 = new TestObserver<>();
+ Single<User> userSingle1 = mUserDao.singleUserById(1);
+ Disposable disposable1 = userSingle1.observeOn(mTestScheduler)
+ .subscribeWith(testObserver1);
+ drain();
+ testObserver1.assertComplete();
+ testObserver1.assertValue(users[0]);
+ disposable1.dispose();
+
+ // how get single for 2
+ TestObserver<User> testObserver2 = new TestObserver<>();
+ Single<User> userSingle2 = mUserDao.singleUserById(2);
+ Disposable disposable2 = userSingle2.observeOn(mTestScheduler)
+ .subscribeWith(testObserver2);
+ drain();
+ testObserver2.assertComplete();
+ testObserver2.assertValue(users[1]);
+ disposable2.dispose();
+
+ // now re-use the first single
+ TestObserver<User> testObserver3 = new TestObserver<>();
+ Disposable disposable3 = userSingle1.observeOn(mTestScheduler)
+ .subscribeWith(testObserver3);
+ drain();
+ testObserver3.assertComplete();
+ testObserver3.assertValue(users[0]);
+ disposable3.dispose();
+ }
+
+
+ @Test
public void observeOnce() throws InterruptedException {
User user = TestUtil.createUser(3);
mUserDao.insert(user);
@@ -239,6 +312,33 @@ public class RxJava2Test extends TestDatabaseTest {
}
@Test
+ public void observeFlowable_keepReference() throws InterruptedException {
+ User[] users = TestUtil.createUsersArray(1, 2);
+ mUserDao.insertAll(users);
+ drain();
+
+ TestSubscriber<User> consumer1 = new TestSubscriber<>();
+ Flowable<User> flowable1 = mUserDao.flowableUserById(1);
+ Disposable disposable1 = flowable1.subscribeWith(consumer1);
+ drain();
+ consumer1.assertValue(users[0]);
+
+ TestSubscriber<User> consumer2 = new TestSubscriber<>();
+ Disposable disposable2 = mUserDao.flowableUserById(2).subscribeWith(consumer2);
+ drain();
+ consumer2.assertValue(users[1]);
+
+ TestSubscriber<User> consumer3 = new TestSubscriber<>();
+ Disposable disposable3 = flowable1.subscribeWith(consumer3);
+ drain();
+ consumer3.assertValue(users[0]);
+
+ disposable1.dispose();
+ disposable2.dispose();
+ disposable3.dispose();
+ }
+
+ @Test
public void flowableCountUsers() throws InterruptedException {
TestSubscriber<Integer> consumer = new TestSubscriber<>();
mUserDao.flowableCountUsers()
diff --git a/androidx/slice/SliceManager.java b/androidx/slice/SliceManager.java
index f8b36e54..63c56e88 100644
--- a/androidx/slice/SliceManager.java
+++ b/androidx/slice/SliceManager.java
@@ -27,6 +27,7 @@ import androidx.annotation.RestrictTo;
import androidx.core.content.PermissionChecker;
import androidx.core.os.BuildCompat;
+import java.util.Collection;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -210,6 +211,18 @@ public abstract class SliceManager {
public abstract void revokeSlicePermission(@NonNull String toPackage, @NonNull Uri uri);
/**
+ * Obtains a list of slices that are descendants of the specified Uri.
+ * <p>
+ * Not all slice providers will implement this functionality, in which case,
+ * an empty collection will be returned.
+ *
+ * @param uri The uri to look for descendants under.
+ * @return All slices within the space.
+ * @see SliceProvider#onGetSliceDescendants(Uri)
+ */
+ public abstract @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri);
+
+ /**
* Class that listens to changes in {@link Slice}s.
*/
public interface SliceCallback {
diff --git a/androidx/slice/SliceManagerCompat.java b/androidx/slice/SliceManagerCompat.java
index 8abacd1e..1badbb4f 100644
--- a/androidx/slice/SliceManagerCompat.java
+++ b/androidx/slice/SliceManagerCompat.java
@@ -28,6 +28,7 @@ import androidx.annotation.RestrictTo;
import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.widget.SliceLiveData;
+import java.util.Collection;
import java.util.Set;
@@ -73,4 +74,9 @@ class SliceManagerCompat extends SliceManagerBase {
public Uri mapIntentToUri(@NonNull Intent intent) {
return SliceProviderCompat.mapIntentToUri(mContext, intent);
}
+
+ @Override
+ public Collection<Uri> getSliceDescendants(Uri uri) {
+ return SliceProviderCompat.getSliceDescendants(mContext, uri);
+ }
}
diff --git a/androidx/slice/SliceManagerTest.java b/androidx/slice/SliceManagerTest.java
index 2c6731d4..5564f723 100644
--- a/androidx/slice/SliceManagerTest.java
+++ b/androidx/slice/SliceManagerTest.java
@@ -43,6 +43,8 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.concurrent.Executor;
@RunWith(AndroidJUnit4.class)
@@ -147,6 +149,26 @@ public class SliceManagerTest {
verify(mSliceProvider).onMapIntentToUri(eq(intent));
}
+ @Test
+ public void testGetDescendants() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ Collection<Uri> collection = Arrays.asList(
+ uri,
+ uri.buildUpon().appendPath("1").build(),
+ uri.buildUpon().appendPath("2").build()
+ );
+ when(mSliceProvider.onGetSliceDescendants(any(Uri.class)))
+ .thenReturn(collection);
+
+ Collection<Uri> allUris = mManager.getSliceDescendants(uri);
+
+ assertEquals(allUris, collection);
+ verify(mSliceProvider).onGetSliceDescendants(eq(uri));
+ }
+
public static class TestSliceProvider extends SliceProvider {
public static SliceProvider sSliceProviderReceiver;
@@ -189,5 +211,13 @@ public class SliceManagerTest {
sSliceProviderReceiver.onSliceUnpinned(sliceUri);
}
}
+
+ @Override
+ public Collection<Uri> onGetSliceDescendants(Uri uri) {
+ if (sSliceProviderReceiver != null) {
+ return sSliceProviderReceiver.onGetSliceDescendants(uri);
+ }
+ return super.onGetSliceDescendants(uri);
+ }
}
}
diff --git a/androidx/slice/SliceManagerWrapper.java b/androidx/slice/SliceManagerWrapper.java
index f322a191..e0c03422 100644
--- a/androidx/slice/SliceManagerWrapper.java
+++ b/androidx/slice/SliceManagerWrapper.java
@@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -71,15 +72,18 @@ class SliceManagerWrapper extends SliceManagerBase {
@Nullable
@Override
public androidx.slice.Slice bindSlice(@NonNull Uri uri) {
- return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
- mContext.getContentResolver(), uri, mSpecs));
+ return SliceConvert.wrap(mManager.bindSlice(uri, mSpecs));
}
@Nullable
@Override
public androidx.slice.Slice bindSlice(@NonNull Intent intent) {
- return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
- mContext, intent, mSpecs));
+ return SliceConvert.wrap(mManager.bindSlice(intent, mSpecs));
+ }
+
+ @Override
+ public Collection<Uri> getSliceDescendants(Uri uri) {
+ return mManager.getSliceDescendants(uri);
}
@Nullable
diff --git a/androidx/slice/SliceMetadata.java b/androidx/slice/SliceMetadata.java
index 9a4648f8..8a4ee038 100644
--- a/androidx/slice/SliceMetadata.java
+++ b/androidx/slice/SliceMetadata.java
@@ -20,6 +20,8 @@ import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.SUBTYPE_MAX;
+import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
@@ -29,8 +31,7 @@ import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
import static androidx.slice.core.SliceHints.HINT_TTL;
-import static androidx.slice.core.SliceHints.SUBTYPE_MAX;
-import static androidx.slice.core.SliceHints.SUBTYPE_VALUE;
+import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
import static androidx.slice.widget.EventInfo.ROW_TYPE_PROGRESS;
import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
@@ -51,6 +52,8 @@ import androidx.slice.widget.ListContent;
import androidx.slice.widget.RowContent;
import androidx.slice.widget.SliceView;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
@@ -66,6 +69,7 @@ public class SliceMetadata {
@IntDef({
LOADED_NONE, LOADED_PARTIAL, LOADED_ALL
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface SliceLoadingState{}
/**
@@ -196,7 +200,7 @@ public class SliceMetadata {
* Gets the range information associated with a progress bar or input range associated with this
* slice, if it exists.
*
- * @return a pair where the first item is the current value of the range and the second item is
+ * @return a pair where the first item is the minimum value of the range and the second item is
* the maximum value of the range.
*/
@Nullable
@@ -206,15 +210,34 @@ public class SliceMetadata {
RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
SliceItem range = rc.getRange();
SliceItem maxItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
- SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
- int max = maxItem != null ? maxItem.getInt() : -1;
- int current = currentItem != null ? currentItem.getInt() : -1;
- return new Pair<>(current, max);
+ SliceItem minItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
+ int max = maxItem != null ? maxItem.getInt() : 100; // default max of range
+ int min = minItem != null ? minItem.getInt() : 0; // default min of range
+ return new Pair<>(min, max);
}
return null;
}
/**
+ * Gets the current value for a progress bar or input range associated with this slice, if it
+ * exists, -1 if unknown.
+ *
+ * @return the current value of a progress bar or input range associated with this slice.
+ */
+ @NonNull
+ public int getRangeValue() {
+ if (mTemplateType == ROW_TYPE_SLIDER
+ || mTemplateType == ROW_TYPE_PROGRESS) {
+ RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
+ SliceItem range = rc.getRange();
+ SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
+ return currentItem != null ? currentItem.getInt() : -1;
+ }
+ return -1;
+
+ }
+
+ /**
* @return the list of keywords associated with the provided slice, null if no keywords were
* specified or an empty list if the slice was specified to have no keywords.
*/
diff --git a/androidx/slice/SliceMetadataTest.java b/androidx/slice/SliceMetadataTest.java
index d378e2ed..650114e7 100644
--- a/androidx/slice/SliceMetadataTest.java
+++ b/androidx/slice/SliceMetadataTest.java
@@ -47,7 +47,6 @@ import androidx.core.util.Pair;
import androidx.slice.builders.GridRowBuilder;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceHints;
import androidx.slice.render.SliceRenderActivity;
@@ -452,15 +451,20 @@ public class SliceMetadataTest {
ListBuilder lb = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
lb.addInputRange(new ListBuilder.InputRangeBuilder(lb)
.setTitle("another title")
- .setValue(5)
+ .setValue(7)
+ .setMin(5)
.setMax(10)
- .setAction(pi));
+ .setInputAction(pi));
Slice sliderSlice = lb.build();
SliceMetadata sliderInfo = SliceMetadata.from(mContext, sliderSlice);
+
Pair<Integer, Integer> values = sliderInfo.getRange();
assertEquals(5, (int) values.first);
assertEquals(10, (int) values.second);
+
+ int currentValue = sliderInfo.getRangeValue();
+ assertEquals(7, currentValue);
}
@Test
@@ -476,9 +480,8 @@ public class SliceMetadataTest {
Slice sliderSlice = lb.build();
SliceMetadata progressInfo = SliceMetadata.from(mContext, sliderSlice);
Pair<Integer, Integer> values = progressInfo.getRange();
- assertEquals(5, (int) values.first);
+ assertEquals(0, (int) values.first);
assertEquals(10, (int) values.second);
-
}
@Test
@@ -593,7 +596,7 @@ public class SliceMetadataTest {
public void testIsPermissionSlice() {
Uri uri = Uri.parse("content://pkg/slice");
Slice permissionSlice =
- SliceProviderCompat.createPermissionSlice(mContext, uri, mContext.getPackageName());
+ SliceProvider.createPermissionSlice(mContext, uri, mContext.getPackageName());
SliceMetadata metadata = SliceMetadata.from(mContext, permissionSlice);
assertEquals(true, metadata.isPermissionSlice());
diff --git a/androidx/slice/SliceProvider.java b/androidx/slice/SliceProvider.java
index 51ffea6b..7ec9232b 100644
--- a/androidx/slice/SliceProvider.java
+++ b/androidx/slice/SliceProvider.java
@@ -15,22 +15,61 @@
*/
package androidx.slice;
+import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.SliceProvider.SLICE_TYPE;
+
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_BIND_URI;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_INTENT;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PKG;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_PROVIDER_PKG;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE;
+import static androidx.slice.compat.SliceProviderCompat.EXTRA_SLICE_DESCENDANTS;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_DESCENDANTS;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_GET_PINNED_SPECS;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_INTENT;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_MAP_ONLY_INTENT;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_PIN;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_SLICE;
+import static androidx.slice.compat.SliceProviderCompat.METHOD_UNPIN;
+import static androidx.slice.compat.SliceProviderCompat.addSpecs;
+import static androidx.slice.compat.SliceProviderCompat.getSpecs;
+import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.ProviderInfo;
+import android.content.pm.PackageManager;
import android.database.ContentObserver;
+import android.database.Cursor;
import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.os.StrictMode;
+import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.core.app.CoreComponentFactory;
import androidx.core.os.BuildCompat;
-import androidx.slice.compat.ContentProviderWrapper;
-import androidx.slice.compat.SliceProviderCompat;
+import androidx.slice.compat.CompatPinnedList;
import androidx.slice.compat.SliceProviderWrapperContainer;
+import androidx.slice.core.R;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Set;
/**
@@ -72,20 +111,21 @@ import java.util.Set;
*
* @see android.app.slice.Slice
*/
-public abstract class SliceProvider extends ContentProviderWrapper {
+public abstract class SliceProvider extends ContentProvider implements
+ CoreComponentFactory.CompatWrapped {
private static Set<SliceSpec> sSpecs;
- @Override
- public void attachInfo(Context context, ProviderInfo info) {
- ContentProvider impl;
- if (BuildCompat.isAtLeastP()) {
- impl = new SliceProviderWrapperContainer.SliceProviderWrapper(this);
- } else {
- impl = new SliceProviderCompat(this);
- }
- super.attachInfo(context, info, impl);
- }
+ private static final String TAG = "SliceProvider";
+
+ private static final String DATA_PREFIX = "slice_data_";
+ private static final long SLICE_BIND_ANR = 2000;
+
+ private static final boolean DEBUG = false;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private CompatPinnedList mPinnedList;
+
+ private String mCallback;
/**
* Implement this to initialize your slice provider on startup.
@@ -104,6 +144,228 @@ public abstract class SliceProvider extends ContentProviderWrapper {
*/
public abstract boolean onCreateSliceProvider();
+ @Override
+ public Object getWrapper() {
+ if (BuildCompat.isAtLeastP()) {
+ return new SliceProviderWrapperContainer.SliceProviderWrapper(this);
+ }
+ return null;
+ }
+
+ @Override
+ public final boolean onCreate() {
+ mPinnedList = new CompatPinnedList(getContext(),
+ DATA_PREFIX + getClass().getName());
+ return onCreateSliceProvider();
+ }
+
+ @Override
+ public final String getType(Uri uri) {
+ if (DEBUG) Log.d(TAG, "getFormat " + uri);
+ return SLICE_TYPE;
+ }
+
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ if (method.equals(METHOD_SLICE)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ if (Binder.getCallingUid() != Process.myUid()) {
+ getContext().enforceUriPermission(uri, Binder.getCallingPid(),
+ Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires write access to the uri");
+ }
+ Set<SliceSpec> specs = getSpecs(extras);
+
+ Slice s = handleBindSlice(uri, specs, getCallingPackage());
+ Bundle b = new Bundle();
+ b.putParcelable(EXTRA_SLICE, s.toBundle());
+ return b;
+ } else if (method.equals(METHOD_MAP_INTENT)) {
+ Intent intent = extras.getParcelable(EXTRA_INTENT);
+ Uri uri = onMapIntentToUri(intent);
+ Bundle b = new Bundle();
+ if (uri != null) {
+ Set<SliceSpec> specs = getSpecs(extras);
+ Slice s = handleBindSlice(uri, specs, getCallingPackage());
+ b.putParcelable(EXTRA_SLICE, s.toBundle());
+ } else {
+ b.putParcelable(EXTRA_SLICE, null);
+ }
+ return b;
+ } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
+ Intent intent = extras.getParcelable(EXTRA_INTENT);
+ Uri uri = onMapIntentToUri(intent);
+ Bundle b = new Bundle();
+ b.putParcelable(EXTRA_SLICE, uri);
+ return b;
+ } else if (method.equals(METHOD_PIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Set<SliceSpec> specs = getSpecs(extras);
+ String pkg = extras.getString(EXTRA_PKG);
+ if (mPinnedList.addPin(uri, pkg, specs)) {
+ handleSlicePinned(uri);
+ }
+ return null;
+ } else if (method.equals(METHOD_UNPIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String pkg = extras.getString(EXTRA_PKG);
+ if (mPinnedList.removePin(uri, pkg)) {
+ handleSliceUnpinned(uri);
+ }
+ return null;
+ } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Bundle b = new Bundle();
+ addSpecs(b, mPinnedList.getSpecs(uri));
+ return b;
+ } else if (method.equals(METHOD_GET_DESCENDANTS)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Bundle b = new Bundle();
+ b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
+ new ArrayList<>(handleGetDescendants(uri)));
+ return b;
+ }
+ return super.call(method, arg, extras);
+ }
+
+ private Collection<Uri> handleGetDescendants(Uri uri) {
+ mCallback = "onGetSliceDescendants";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ return onGetSliceDescendants(uri);
+ } finally {
+ mHandler.removeCallbacks(mAnr);
+ }
+ }
+
+ private void handleSlicePinned(final Uri sliceUri) {
+ mCallback = "onSlicePinned";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ onSlicePinned(sliceUri);
+ } finally {
+ mHandler.removeCallbacks(mAnr);
+ }
+ }
+
+ private void handleSliceUnpinned(final Uri sliceUri) {
+ mCallback = "onSliceUnpinned";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ onSliceUnpinned(sliceUri);
+ } finally {
+ mHandler.removeCallbacks(mAnr);
+ }
+ }
+
+ private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
+ final String callingPkg) {
+ // This can be removed once Slice#bindSlice is removed and everyone is using
+ // SliceManager#bindSlice.
+ String pkg = callingPkg != null ? callingPkg
+ : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (Binder.getCallingUid() != Process.myUid()) {
+ try {
+ getContext().enforceUriPermission(sliceUri,
+ Binder.getCallingPid(), Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires write access to Uri");
+ } catch (SecurityException e) {
+ return createPermissionSlice(getContext(), sliceUri, pkg);
+ }
+ }
+ return onBindSliceStrict(sliceUri, specs);
+ }
+
+ /**
+ * Generate a slice that contains a permission request.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static Slice createPermissionSlice(Context context, Uri sliceUri,
+ String callingPackage) {
+ Slice.Builder parent = new Slice.Builder(sliceUri);
+
+ Slice.Builder action = new Slice.Builder(parent)
+ .addHints(HINT_TITLE, HINT_SHORTCUT)
+ .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+ new Slice.Builder(parent).build(), null);
+
+ parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+ .addText(getPermissionString(context, callingPackage), null)
+ .addSubSlice(action.build())
+ .build());
+
+ return parent.addHints(HINT_PERMISSION_REQUEST).build();
+ }
+
+ /**
+ * Create a PendingIntent pointing at the permission dialog.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+ String callingPackage) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(context.getPackageName(),
+ "androidx.slice.compat.SlicePermissionActivity"));
+ intent.putExtra(EXTRA_BIND_URI, sliceUri);
+ intent.putExtra(EXTRA_PKG, callingPackage);
+ intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+ // Unique pending intent.
+ intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+ .build());
+
+ return PendingIntent.getActivity(context, 0, intent, 0);
+ }
+
+ /**
+ * Get string describing permission request.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static CharSequence getPermissionString(Context context, String callingPackage) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ return context.getString(R.string.abc_slices_permission_request,
+ pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+ context.getApplicationInfo().loadLabel(pm));
+ } catch (PackageManager.NameNotFoundException e) {
+ // This shouldn't be possible since the caller is verified.
+ throw new RuntimeException("Unknown calling app", e);
+ }
+ }
+
+ private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+ mCallback = "onBindSlice";
+ mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
+ try {
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+ .detectAll()
+ .penaltyDeath()
+ .build());
+ SliceProvider.setSpecs(specs);
+ try {
+ return onBindSlice(sliceUri);
+ } finally {
+ SliceProvider.setSpecs(null);
+ mHandler.removeCallbacks(mAnr);
+ }
+ } finally {
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ }
+
+ private final Runnable mAnr = new Runnable() {
+ @Override
+ public void run() {
+ Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
+ Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+ }
+ };
+
/**
* Implemented to create a slice.
* <p>
@@ -166,6 +428,75 @@ public abstract class SliceProvider extends ContentProviderWrapper {
}
/**
+ * Obtains a list of slices that are descendants of the specified Uri.
+ * <p>
+ * Implementing this is optional for a SliceProvider, but does provide a good
+ * discovery mechanism for finding slice Uris.
+ *
+ * @param uri The uri to look for descendants under.
+ * @return All slices within the space.
+ * @see androidx.slice.SliceManager#getSliceDescendants(Uri)
+ */
+ public Collection<Uri> onGetSliceDescendants(Uri uri) {
+ return Collections.emptyList();
+ }
+
+ @Nullable
+ @Override
+ public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable String selection, @Nullable String[] selectionArgs,
+ @Nullable String sortOrder) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ @RequiresApi(28)
+ public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ @RequiresApi(16)
+ public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable String selection, @Nullable String[] selectionArgs,
+ @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
+ return 0;
+ }
+
+ @Override
+ public final int delete(@NonNull Uri uri, @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public final int update(@NonNull Uri uri, @Nullable ContentValues values,
+ @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ @RequiresApi(19)
+ public final Uri canonicalize(@NonNull Uri url) {
+ return null;
+ }
+
+ /**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/androidx/slice/SliceTestProvider.java b/androidx/slice/SliceTestProvider.java
index d6f16288..519e5b88 100644
--- a/androidx/slice/SliceTestProvider.java
+++ b/androidx/slice/SliceTestProvider.java
@@ -33,6 +33,7 @@ public class SliceTestProvider extends androidx.slice.SliceProvider {
@Override
public boolean onCreateSliceProvider() {
+ getContext().getPackageName();
return true;
}
diff --git a/androidx/slice/SliceUtils.java b/androidx/slice/SliceUtils.java
index 04248007..442b3240 100644
--- a/androidx/slice/SliceUtils.java
+++ b/androidx/slice/SliceUtils.java
@@ -30,6 +30,8 @@ import static androidx.slice.SliceMetadata.LOADED_NONE;
import static androidx.slice.SliceMetadata.LOADED_PARTIAL;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
@@ -43,6 +45,7 @@ import androidx.slice.core.SliceQuery;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;
@@ -116,6 +119,7 @@ public class SliceUtils {
public static final int MODE_CONVERT = 2;
@IntDef({MODE_THROW, MODE_REMOVE, MODE_CONVERT})
+ @Retention(SOURCE)
@interface FormatMode {
}
diff --git a/androidx/slice/builders/ListBuilder.java b/androidx/slice/builders/ListBuilder.java
index c0b5bc31..1c81ebd7 100644
--- a/androidx/slice/builders/ListBuilder.java
+++ b/androidx/slice/builders/ListBuilder.java
@@ -38,9 +38,10 @@ import androidx.slice.builders.impl.ListBuilderV1Impl;
import androidx.slice.builders.impl.TemplateBuilderImpl;
import androidx.slice.core.SliceHints;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
-
/**
* A slice can be constructed with ListBuilder.
* <p>
@@ -124,6 +125,7 @@ public class ListBuilder extends TemplateSliceBuilder {
@IntDef({
LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE, UNKNOWN_IMAGE
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface ImageMode{}
/**
@@ -653,6 +655,15 @@ public class ListBuilder extends TemplateSliceBuilder {
}
/**
+ * Set the lower limit of the range. The default is 0.
+ */
+ @NonNull
+ public InputRangeBuilder setMin(int min) {
+ mImpl.setMin(min);
+ return this;
+ }
+
+ /**
* Set the upper limit of the range. The default is 100.
*/
@NonNull
diff --git a/androidx/slice/builders/impl/ListBuilder.java b/androidx/slice/builders/impl/ListBuilder.java
index 084b85a8..80a477eb 100644
--- a/androidx/slice/builders/impl/ListBuilder.java
+++ b/androidx/slice/builders/impl/ListBuilder.java
@@ -141,6 +141,11 @@ public interface ListBuilder {
*/
interface RangeBuilder {
/**
+ * Set the lower limit.
+ */
+ void setMin(int min);
+
+ /**
* Set the upper limit.
*/
void setMax(int max);
diff --git a/androidx/slice/builders/impl/ListBuilderV1Impl.java b/androidx/slice/builders/impl/ListBuilderV1Impl.java
index f2ff884e..c771b624 100644
--- a/androidx/slice/builders/impl/ListBuilderV1Impl.java
+++ b/androidx/slice/builders/impl/ListBuilderV1Impl.java
@@ -27,6 +27,9 @@ import static android.app.slice.Slice.HINT_SUMMARY;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
+import static android.app.slice.Slice.SUBTYPE_MAX;
+import static android.app.slice.Slice.SUBTYPE_RANGE;
+import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
@@ -36,10 +39,8 @@ import static androidx.slice.builders.ListBuilder.LARGE_IMAGE;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
import static androidx.slice.core.SliceHints.HINT_TTL;
-import static androidx.slice.core.SliceHints.SUBTYPE_MAX;
import static androidx.slice.core.SliceHints.SUBTYPE_MILLIS;
-import static androidx.slice.core.SliceHints.SUBTYPE_RANGE;
-import static androidx.slice.core.SliceHints.SUBTYPE_VALUE;
+import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
import android.app.PendingIntent;
import android.net.Uri;
@@ -158,6 +159,7 @@ public class ListBuilderV1Impl extends TemplateBuilderImpl implements ListBuilde
* Builder to construct an input row.
*/
public static class RangeBuilderImpl extends TemplateBuilderImpl implements RangeBuilder {
+ private int mMin = 0;
private int mMax = 100;
private int mValue = 0;
private CharSequence mTitle;
@@ -170,6 +172,11 @@ public class ListBuilderV1Impl extends TemplateBuilderImpl implements ListBuilde
}
@Override
+ public void setMin(int min) {
+ mMin = min;
+ }
+
+ @Override
public void setMax(int max) {
mMax = max;
}
@@ -216,6 +223,7 @@ public class ListBuilderV1Impl extends TemplateBuilderImpl implements ListBuilde
builder.addSubSlice(mPrimaryAction.buildSlice(sb), null /* subtype */);
}
builder.addHints(HINT_LIST_ITEM)
+ .addInt(mMin, SUBTYPE_MIN)
.addInt(mMax, SUBTYPE_MAX)
.addInt(mValue, SUBTYPE_VALUE);
}
diff --git a/androidx/slice/compat/ContentProviderWrapper.java b/androidx/slice/compat/ContentProviderWrapper.java
deleted file mode 100644
index 45c6ffc7..00000000
--- a/androidx/slice/compat/ContentProviderWrapper.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.slice.compat;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.pm.ProviderInfo;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.RestrictTo.Scope;
-
-/**
- * @hide
- */
-// TODO: Remove as soon as we have better systems in place for this.
-@RestrictTo(Scope.LIBRARY)
-public class ContentProviderWrapper extends ContentProvider {
-
- private ContentProvider mImpl;
-
- /**
- * Triggers an attach with the object to wrap.
- */
- public void attachInfo(Context context, ProviderInfo info, ContentProvider impl) {
- mImpl = impl;
- super.attachInfo(context, info);
- mImpl.attachInfo(context, info);
- }
-
- @Override
- public final boolean onCreate() {
- return mImpl.onCreate();
- }
-
- @Nullable
- @Override
- public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
- @Nullable String selection, @Nullable String[] selectionArgs,
- @Nullable String sortOrder) {
- return mImpl.query(uri, projection, selection, selectionArgs, sortOrder);
- }
-
- @Nullable
- @Override
- @RequiresApi(28)
- public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
- @Nullable Bundle queryArgs, @Nullable CancellationSignal cancellationSignal) {
- return mImpl.query(uri, projection, queryArgs, cancellationSignal);
- }
-
- @Nullable
- @Override
- @RequiresApi(16)
- public final Cursor query(@NonNull Uri uri, @Nullable String[] projection,
- @Nullable String selection, @Nullable String[] selectionArgs,
- @Nullable String sortOrder, @Nullable CancellationSignal cancellationSignal) {
- return mImpl.query(uri, projection, selection, selectionArgs, sortOrder,
- cancellationSignal);
- }
-
- @Nullable
- @Override
- public final String getType(@NonNull Uri uri) {
- return mImpl.getType(uri);
- }
-
- @Nullable
- @Override
- public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
- return mImpl.insert(uri, values);
- }
-
- @Override
- public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
- return mImpl.bulkInsert(uri, values);
- }
-
- @Override
- public final int delete(@NonNull Uri uri, @Nullable String selection,
- @Nullable String[] selectionArgs) {
- return mImpl.delete(uri, selection, selectionArgs);
- }
-
- @Override
- public final int update(@NonNull Uri uri, @Nullable ContentValues values,
- @Nullable String selection, @Nullable String[] selectionArgs) {
- return mImpl.update(uri, values, selection, selectionArgs);
- }
-
- @Nullable
- @Override
- public final Bundle call(@NonNull String method, @Nullable String arg,
- @Nullable Bundle extras) {
- return mImpl.call(method, arg, extras);
- }
-
- @Nullable
- @Override
- @RequiresApi(19)
- public final Uri canonicalize(@NonNull Uri url) {
- return mImpl.canonicalize(url);
- }
-}
diff --git a/androidx/slice/compat/SliceProviderCompat.java b/androidx/slice/compat/SliceProviderCompat.java
index 182b3c25..61bc65ec 100644
--- a/androidx/slice/compat/SliceProviderCompat.java
+++ b/androidx/slice/compat/SliceProviderCompat.java
@@ -15,48 +15,33 @@
*/
package androidx.slice.compat;
-import static android.app.slice.Slice.HINT_SHORTCUT;
-import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.SliceProvider.SLICE_TYPE;
-import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
-
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
-import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.database.Cursor;
import android.net.Uri;
-import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
import android.os.Parcelable;
-import android.os.Process;
import android.os.RemoteException;
-import android.os.StrictMode;
-import android.os.StrictMode.ThreadPolicy;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.collection.ArraySet;
import androidx.core.util.Preconditions;
import androidx.slice.Slice;
-import androidx.slice.SliceProvider;
import androidx.slice.SliceSpec;
-import androidx.slice.core.R;
import androidx.slice.core.SliceHints;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -64,9 +49,8 @@ import java.util.Set;
* @hide
*/
@RestrictTo(Scope.LIBRARY)
-public class SliceProviderCompat extends ContentProvider {
-
- private static final String TAG = "SliceProvider";
+public class SliceProviderCompat {
+ private static final String TAG = "SliceProviderCompat";
public static final String EXTRA_BIND_URI = "slice_uri";
public static final String METHOD_SLICE = "bind_slice";
@@ -75,6 +59,7 @@ public class SliceProviderCompat extends ContentProvider {
public static final String METHOD_UNPIN = "unpin_slice";
public static final String METHOD_GET_PINNED_SPECS = "get_specs";
public static final String METHOD_MAP_ONLY_INTENT = "map_only";
+ public static final String METHOD_GET_DESCENDANTS = "get_descendants";
public static final String EXTRA_INTENT = "slice_intent";
public static final String EXTRA_SLICE = "slice";
@@ -82,244 +67,7 @@ public class SliceProviderCompat extends ContentProvider {
public static final String EXTRA_SUPPORTED_SPECS_REVS = "revs";
public static final String EXTRA_PKG = "pkg";
public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
- private static final String DATA_PREFIX = "slice_data_";
-
- private static final long SLICE_BIND_ANR = 2000;
-
- private static final boolean DEBUG = false;
- private final Handler mHandler = new Handler(Looper.getMainLooper());
- private SliceProvider mSliceProvider;
- private CompatPinnedList mPinnedList;
-
- private String mCallback;
-
- public SliceProviderCompat(SliceProvider provider) {
- mSliceProvider = provider;
- }
-
- @Override
- public boolean onCreate() {
- mPinnedList = new CompatPinnedList(getContext(),
- DATA_PREFIX + mSliceProvider.getClass().getName());
- return mSliceProvider.onCreateSliceProvider();
- }
-
- @Override
- public final int update(Uri uri, ContentValues values, String selection,
- String[] selectionArgs) {
- if (DEBUG) Log.d(TAG, "update " + uri);
- return 0;
- }
-
- @Override
- public final int delete(Uri uri, String selection, String[] selectionArgs) {
- if (DEBUG) Log.d(TAG, "delete " + uri);
- return 0;
- }
-
- @Override
- public final Cursor query(Uri uri, String[] projection, String selection,
- String[] selectionArgs, String sortOrder) {
- if (DEBUG) Log.d(TAG, "query " + uri);
- return null;
- }
-
- @Override
- public final Cursor query(Uri uri, String[] projection, String selection, String[]
- selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
- if (DEBUG) Log.d(TAG, "query " + uri);
- return null;
- }
-
- @Override
- public final Cursor query(Uri uri, String[] projection, Bundle queryArgs,
- CancellationSignal cancellationSignal) {
- if (DEBUG) Log.d(TAG, "query " + uri);
- return null;
- }
-
- @Override
- public final Uri insert(Uri uri, ContentValues values) {
- if (DEBUG) Log.d(TAG, "insert " + uri);
- return null;
- }
-
- @Override
- public final String getType(Uri uri) {
- if (DEBUG) Log.d(TAG, "getFormat " + uri);
- return SLICE_TYPE;
- }
-
- @Override
- public Bundle call(String method, String arg, Bundle extras) {
- if (method.equals(METHOD_SLICE)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- if (Binder.getCallingUid() != Process.myUid()) {
- getContext().enforceUriPermission(uri, Binder.getCallingPid(),
- Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires the permission BIND_SLICE");
- }
- Set<SliceSpec> specs = getSpecs(extras);
-
- Slice s = handleBindSlice(uri, specs, getCallingPackage());
- Bundle b = new Bundle();
- b.putParcelable(EXTRA_SLICE, s.toBundle());
- return b;
- } else if (method.equals(METHOD_MAP_INTENT)) {
- Intent intent = extras.getParcelable(EXTRA_INTENT);
- Uri uri = mSliceProvider.onMapIntentToUri(intent);
- Bundle b = new Bundle();
- if (uri != null) {
- Set<SliceSpec> specs = getSpecs(extras);
- Slice s = handleBindSlice(uri, specs, getCallingPackage());
- b.putParcelable(EXTRA_SLICE, s.toBundle());
- } else {
- b.putParcelable(EXTRA_SLICE, null);
- }
- return b;
- } else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
- Intent intent = extras.getParcelable(EXTRA_INTENT);
- Uri uri = mSliceProvider.onMapIntentToUri(intent);
- Bundle b = new Bundle();
- b.putParcelable(EXTRA_SLICE, uri);
- return b;
- } else if (method.equals(METHOD_PIN)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- Set<SliceSpec> specs = getSpecs(extras);
- String pkg = extras.getString(EXTRA_PKG);
- if (mPinnedList.addPin(uri, pkg, specs)) {
- handleSlicePinned(uri);
- }
- return null;
- } else if (method.equals(METHOD_UNPIN)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- String pkg = extras.getString(EXTRA_PKG);
- if (mPinnedList.removePin(uri, pkg)) {
- handleSliceUnpinned(uri);
- }
- return null;
- } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
- Uri uri = extras.getParcelable(EXTRA_BIND_URI);
- Bundle b = new Bundle();
- addSpecs(b, mPinnedList.getSpecs(uri));
- return b;
- }
- return super.call(method, arg, extras);
- }
-
- private void handleSlicePinned(final Uri sliceUri) {
- mCallback = "onSlicePinned";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- mSliceProvider.onSlicePinned(sliceUri);
- } finally {
- mHandler.removeCallbacks(mAnr);
- }
- }
-
- private void handleSliceUnpinned(final Uri sliceUri) {
- mCallback = "onSliceUnpinned";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- mSliceProvider.onSliceUnpinned(sliceUri);
- } finally {
- mHandler.removeCallbacks(mAnr);
- }
- }
-
- private Slice handleBindSlice(final Uri sliceUri, final Set<SliceSpec> specs,
- final String callingPkg) {
- // This can be removed once Slice#bindSlice is removed and everyone is using
- // SliceManager#bindSlice.
- String pkg = callingPkg != null ? callingPkg
- : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
- if (Binder.getCallingUid() != Process.myUid()) {
- try {
- getContext().enforceUriPermission(sliceUri,
- Binder.getCallingPid(), Binder.getCallingUid(),
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
- "Slice binding requires write access to Uri");
- } catch (SecurityException e) {
- return createPermissionSlice(getContext(), sliceUri, pkg);
- }
- }
- return onBindSliceStrict(sliceUri, specs);
- }
-
- /**
- * Generate a slice that contains a permission request.
- */
- public static Slice createPermissionSlice(Context context, Uri sliceUri,
- String callingPackage) {
- Slice.Builder parent = new Slice.Builder(sliceUri);
-
- Slice.Builder action = new Slice.Builder(parent)
- .addHints(HINT_TITLE, HINT_SHORTCUT)
- .addAction(createPermissionIntent(context, sliceUri, callingPackage),
- new Slice.Builder(parent).build(), null);
-
- parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
- .addText(getPermissionString(context, callingPackage), null)
- .addSubSlice(action.build())
- .build());
-
- return parent.addHints(HINT_PERMISSION_REQUEST).build();
- }
-
- /**
- * Create a PendingIntent pointing at the permission dialog.
- */
- public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
- String callingPackage) {
- Intent intent = new Intent();
- intent.setComponent(new ComponentName(context.getPackageName(),
- "androidx.slice.compat.SlicePermissionActivity"));
- intent.putExtra(EXTRA_BIND_URI, sliceUri);
- intent.putExtra(EXTRA_PKG, callingPackage);
- intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
- // Unique pending intent.
- intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
- .build());
-
- return PendingIntent.getActivity(context, 0, intent, 0);
- }
-
- /**
- * Get string describing permission request.
- */
- public static CharSequence getPermissionString(Context context, String callingPackage) {
- PackageManager pm = context.getPackageManager();
- try {
- return context.getString(R.string.abc_slices_permission_request,
- pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
- context.getApplicationInfo().loadLabel(pm));
- } catch (PackageManager.NameNotFoundException e) {
- // This shouldn't be possible since the caller is verified.
- throw new RuntimeException("Unknown calling app", e);
- }
- }
-
- private Slice onBindSliceStrict(Uri sliceUri, Set<SliceSpec> specs) {
- ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
- mCallback = "onBindSlice";
- mHandler.postDelayed(mAnr, SLICE_BIND_ANR);
- try {
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
- .detectAll()
- .penaltyDeath()
- .build());
- SliceProvider.setSpecs(specs);
- try {
- return mSliceProvider.onBindSlice(sliceUri);
- } finally {
- SliceProvider.setSpecs(null);
- mHandler.removeCallbacks(mAnr);
- }
- } finally {
- StrictMode.setThreadPolicy(oldPolicy);
- }
- }
+ public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
/**
* Compat version of {@link Slice#bindSlice}.
@@ -357,7 +105,10 @@ public class SliceProviderCompat extends ContentProvider {
}
}
- private static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
+ /**
+ * Compat way to push specs through the call.
+ */
+ public static void addSpecs(Bundle extras, Set<SliceSpec> supportedSpecs) {
ArrayList<String> types = new ArrayList<>();
ArrayList<Integer> revs = new ArrayList<>();
for (SliceSpec spec : supportedSpecs) {
@@ -368,7 +119,10 @@ public class SliceProviderCompat extends ContentProvider {
extras.putIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS, revs);
}
- private static Set<SliceSpec> getSpecs(Bundle extras) {
+ /**
+ * Compat way to push specs through the call.
+ */
+ public static Set<SliceSpec> getSpecs(Bundle extras) {
ArraySet<SliceSpec> specs = new ArraySet<>();
ArrayList<String> types = extras.getStringArrayList(EXTRA_SUPPORTED_SPECS);
ArrayList<Integer> revs = extras.getIntegerArrayList(EXTRA_SUPPORTED_SPECS_REVS);
@@ -549,11 +303,22 @@ public class SliceProviderCompat extends ContentProvider {
}
}
- private final Runnable mAnr = new Runnable() {
- @Override
- public void run() {
- Process.sendSignal(Process.myPid(), Process.SIGNAL_QUIT);
- Log.wtf(TAG, "Timed out while handling slice callback " + mCallback);
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#getSliceDescendants(Uri)}
+ */
+ public static @NonNull Collection<Uri> getSliceDescendants(Context context, @NonNull Uri uri) {
+ ContentResolver resolver = context.getContentResolver();
+ try (ContentProviderClient provider = resolver.acquireContentProviderClient(uri)) {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ final Bundle res = provider.call(METHOD_GET_DESCENDANTS, null, extras);
+ return res.getParcelableArrayList(EXTRA_SLICE_DESCENDANTS);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
}
- };
+ return Collections.emptyList();
+ }
+
+ private SliceProviderCompat() {
+ }
}
diff --git a/androidx/slice/compat/SliceProviderWrapperContainer.java b/androidx/slice/compat/SliceProviderWrapperContainer.java
index 4db52f04..86415305 100644
--- a/androidx/slice/compat/SliceProviderWrapperContainer.java
+++ b/androidx/slice/compat/SliceProviderWrapperContainer.java
@@ -22,7 +22,9 @@ import android.annotation.TargetApi;
import android.app.slice.Slice;
import android.app.slice.SliceProvider;
import android.app.slice.SliceSpec;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.ProviderInfo;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -30,6 +32,7 @@ import androidx.annotation.RestrictTo;
import androidx.collection.ArraySet;
import androidx.slice.SliceConvert;
+import java.util.Collection;
import java.util.List;
/**
@@ -50,8 +53,14 @@ public class SliceProviderWrapperContainer {
}
@Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ mSliceProvider.attachInfo(context, info);
+ super.attachInfo(context, info);
+ }
+
+ @Override
public boolean onCreate() {
- return mSliceProvider.onCreateSliceProvider();
+ return true;
}
@Override
@@ -74,6 +83,11 @@ public class SliceProviderWrapperContainer {
mSliceProvider.onSliceUnpinned(sliceUri);
}
+ @Override
+ public Collection<Uri> onGetSliceDescendants(Uri uri) {
+ return mSliceProvider.onGetSliceDescendants(uri);
+ }
+
/**
* Maps intents to uris.
*/
diff --git a/androidx/slice/core/SliceHints.java b/androidx/slice/core/SliceHints.java
index e35ae35d..c29ad568 100644
--- a/androidx/slice/core/SliceHints.java
+++ b/androidx/slice/core/SliceHints.java
@@ -18,34 +18,24 @@ package androidx.slice.core;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
+import java.lang.annotation.Retention;
+
/**
* Temporary class to contain hint constants for slices to be used.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public class SliceHints {
- /**
- * Subtype to range an item representing a range.
- */
- public static final String SUBTYPE_RANGE = "range";
-
- /**
- * Subtype indicating that this content is the maximum value for a range.
- */
- public static final String SUBTYPE_MAX = "max";
-
- /**
- * Subtype indicating that this content is the current value for a range.
- */
- public static final String SUBTYPE_VALUE = "value";
/**
- * Key to retrieve an extra added to an intent when the value of an input range has changed.
+ * Subtype indicating that this content is the minimum value for a range.
*/
- public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
+ public static final String SUBTYPE_MIN = "min";
/**
* The meta-data key that allows an activity to easily be linked directly to a slice.
@@ -83,6 +73,7 @@ public class SliceHints {
@IntDef({
LARGE_IMAGE, SMALL_IMAGE, ICON_IMAGE, UNKNOWN_IMAGE
})
+ @Retention(SOURCE)
public @interface ImageMode{}
/**
diff --git a/androidx/slice/render/SliceCreator.java b/androidx/slice/render/SliceCreator.java
index e0bccfd1..2312b369 100644
--- a/androidx/slice/render/SliceCreator.java
+++ b/androidx/slice/render/SliceCreator.java
@@ -36,11 +36,11 @@ import android.text.style.ForegroundColorSpan;
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.Slice;
+import androidx.slice.SliceProvider;
import androidx.slice.builders.GridRowBuilder;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.MessagingSliceBuilder;
import androidx.slice.builders.SliceAction;
-import androidx.slice.compat.SliceProviderCompat;
import androidx.slice.view.test.R;
import java.util.Arrays;
@@ -126,7 +126,7 @@ public class SliceCreator {
private Slice createWeather(Uri sliceUri) {
SliceAction primaryAction = new SliceAction(getBroadcastIntent(ACTION_TOAST,
"open weather app"),
- IconCompat.createWithResource(getContext(), R.drawable.weather_1),
+ IconCompat.createWithResource(getContext(), R.drawable.weather_1), SMALL_IMAGE,
"Weather is happening!");
ListBuilder b = new ListBuilder(getContext(), sliceUri, -TimeUnit.HOURS.toMillis(8));
GridRowBuilder gb = new GridRowBuilder(b);
@@ -486,9 +486,10 @@ public class SliceCreator {
.setTitle("Star rating")
.setSubtitle("Pick a rating from 0 to 5")
.setThumb(icon)
+ .setMin(5)
.setInputAction(getBroadcastIntent(ACTION_TOAST, "range changed"))
- .setMax(5)
- .setValue(3)
+ .setMax(10)
+ .setValue(7)
.setPrimaryAction(primaryAction)
.setContentDescription("Slider for star ratings"))
.build();
@@ -512,7 +513,7 @@ public class SliceCreator {
}
private Slice createPermissionSlice(Uri uri) {
- return SliceProviderCompat.createPermissionSlice(getContext(), uri,
+ return SliceProvider.createPermissionSlice(getContext(), uri,
getContext().getPackageName());
}
diff --git a/androidx/slice/widget/EventInfo.java b/androidx/slice/widget/EventInfo.java
index 377fbd6f..9b3af246 100644
--- a/androidx/slice/widget/EventInfo.java
+++ b/androidx/slice/widget/EventInfo.java
@@ -19,6 +19,9 @@ package androidx.slice.widget;
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Represents information associated with a logged event on {@link SliceView}.
*/
@@ -37,6 +40,7 @@ public class EventInfo {
ROW_TYPE_SLIDER,
ROW_TYPE_PROGRESS,
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface SliceRowType {}
/**
@@ -76,6 +80,7 @@ public class EventInfo {
ACTION_TYPE_TOGGLE, ACTION_TYPE_BUTTON, ACTION_TYPE_SLIDER, ACTION_TYPE_CONTENT,
ACTION_TYPE_SEE_MORE
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface SliceActionType{}
/**
@@ -109,6 +114,7 @@ public class EventInfo {
@IntDef({
POSITION_START, POSITION_END, POSITION_CELL
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface SliceButtonPosition{}
/**
diff --git a/androidx/slice/widget/GridRowView.java b/androidx/slice/widget/GridRowView.java
index 2335aaa1..4b8078c2 100644
--- a/androidx/slice/widget/GridRowView.java
+++ b/androidx/slice/widget/GridRowView.java
@@ -17,7 +17,6 @@
package androidx.slice.widget;
import static android.app.slice.Slice.HINT_LARGE;
-import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
@@ -133,16 +132,9 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
}
}
- /**
- * This is called when GridView is presented in small format.
- */
@Override
public void setSlice(Slice slice) {
- resetView();
- mRowIndex = 0;
- SliceItem item = SliceQuery.find(slice, FORMAT_SLICE, HINT_LIST_ITEM, null);
- mGridContent = new GridContent(getContext(), item);
- populateViews(mGridContent);
+ // Nothing to do
}
/**
@@ -164,7 +156,7 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
EventInfo.ROW_TYPE_GRID, mRowIndex);
Pair<SliceItem, EventInfo> tagItem = new Pair<>(gc.getContentIntent(), info);
mViewContainer.setTag(tagItem);
- makeClickable(mViewContainer);
+ makeClickable(mViewContainer, true);
}
CharSequence contentDescr = gc.getContentDescription();
if (contentDescr != null) {
@@ -222,7 +214,7 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
info.setPosition(EventInfo.POSITION_CELL, index, total);
Pair<SliceItem, EventInfo> tagItem = new Pair<>(seeMoreItem, info);
seeMoreView.setTag(tagItem);
- makeClickable(seeMoreView);
+ makeClickable(seeMoreView, true);
}
/**
@@ -301,7 +293,7 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
info.setPosition(EventInfo.POSITION_CELL, index, total);
Pair<SliceItem, EventInfo> tagItem = new Pair<>(contentIntentItem, info);
cellContainer.setTag(tagItem);
- makeClickable(cellContainer);
+ makeClickable(cellContainer, true);
}
}
}
@@ -347,10 +339,12 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
return addedView != null;
}
- private void makeClickable(View layout) {
- layout.setOnClickListener(this);
- layout.setBackground(SliceViewUtil.getDrawable(getContext(),
- android.R.attr.selectableItemBackground));
+ private void makeClickable(View layout, boolean isClickable) {
+ layout.setOnClickListener(isClickable ? this : null);
+ layout.setBackground(isClickable
+ ? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground)
+ : null);
+ layout.setClickable(isClickable);
}
@Override
@@ -365,7 +359,7 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
mObserver.onSliceAction(info, actionItem);
}
} catch (PendingIntent.CanceledException e) {
- Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
}
@@ -373,5 +367,6 @@ public class GridRowView extends SliceChildView implements View.OnClickListener
@Override
public void resetView() {
mViewContainer.removeAllViews();
+ makeClickable(mViewContainer, false);
}
}
diff --git a/androidx/slice/widget/LargeSliceAdapter.java b/androidx/slice/widget/LargeSliceAdapter.java
index 19f232f5..e09405cf 100644
--- a/androidx/slice/widget/LargeSliceAdapter.java
+++ b/androidx/slice/widget/LargeSliceAdapter.java
@@ -19,7 +19,6 @@ package androidx.slice.widget;
import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.SUBTYPE_MESSAGE;
import static android.app.slice.Slice.SUBTYPE_SOURCE;
-import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_TEXT;
@@ -29,6 +28,7 @@ import android.app.slice.Slice;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
@@ -64,15 +64,30 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
private SliceView.OnSliceActionListener mSliceObserver;
private int mColor;
private AttributeSet mAttrs;
+ private int mDefStyleAttr;
+ private int mDefStyleRes;
private List<SliceItem> mSliceActions;
private boolean mShowLastUpdated;
private long mLastUpdated;
+ private SliceView mParent;
+ private LargeTemplateView mTemplateView;
public LargeSliceAdapter(Context context) {
mContext = context;
setHasStableIds(true);
}
+ /**
+ * Sets the SliceView parent and the template parent.
+ */
+ public void setParents(SliceView parent, LargeTemplateView templateView) {
+ mParent = parent;
+ mTemplateView = templateView;
+ }
+
+ /**
+ * Sets the observer to pass down to child views.
+ */
public void setSliceObserver(SliceView.OnSliceActionListener observer) {
mSliceObserver = observer;
}
@@ -105,8 +120,10 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
/**
* Sets the attribute set to use for views in the list.
*/
- public void setStyle(AttributeSet attrs) {
+ public void setStyle(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mAttrs = attrs;
+ mDefStyleAttr = defStyleAttr;
+ mDefStyleRes = defStyleRes;
notifyDataSetChanged();
}
@@ -151,17 +168,7 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
@Override
public void onBindViewHolder(SliceViewHolder holder, int position) {
SliceWrapper slice = mSlices.get(position);
- if (holder.mSliceView != null) {
- final boolean isHeader = position == HEADER_INDEX;
- holder.mSliceView.setTint(mColor);
- holder.mSliceView.setStyle(mAttrs);
- holder.mSliceView.setSliceItem(slice.mItem, isHeader, position, mSliceObserver);
- if (isHeader && holder.mSliceView instanceof RowView) {
- holder.mSliceView.setSliceActions(mSliceActions);
- holder.mSliceView.setLastUpdated(mLastUpdated);
- holder.mSliceView.setShowLastUpdated(mShowLastUpdated);
- }
- }
+ holder.bind(slice.mItem, position);
}
private void notifyHeaderChanged() {
@@ -184,7 +191,8 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
null);
break;
}
- ((SliceChildView) v).setMode(MODE_LARGE);
+ int mode = mParent != null ? mParent.getMode() : MODE_LARGE;
+ ((SliceChildView) v).setMode(mode);
return v;
}
@@ -221,12 +229,53 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
/**
* A {@link RecyclerView.ViewHolder} for presenting slices in {@link LargeSliceAdapter}.
*/
- public static class SliceViewHolder extends RecyclerView.ViewHolder {
- public final SliceChildView mSliceView;
+ public class SliceViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener,
+ View.OnClickListener {
+ public final SliceChildView mSliceChildView;
public SliceViewHolder(View itemView) {
super(itemView);
- mSliceView = itemView instanceof SliceChildView ? (SliceChildView) itemView : null;
+ mSliceChildView = itemView instanceof SliceChildView ? (SliceChildView) itemView : null;
+ }
+
+ void bind(SliceItem item, int position) {
+ if (mSliceChildView == null || item == null) {
+ return;
+ }
+ // Click listener used to pipe click events to parent
+ mSliceChildView.setOnClickListener(this);
+ // Touch listener used to pipe events to touch feedback drawable
+ mSliceChildView.setOnTouchListener(this);
+
+ final boolean isHeader = position == HEADER_INDEX;
+ mSliceChildView.setTint(mColor);
+ mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
+ mSliceChildView.setSliceItem(item, isHeader, position, mSliceObserver);
+ if (isHeader && mSliceChildView instanceof RowView) {
+ mSliceChildView.setSliceActions(mSliceActions);
+ mSliceChildView.setLastUpdated(mLastUpdated);
+ mSliceChildView.setShowLastUpdated(mShowLastUpdated);
+ }
+ int[] info = new int[2];
+ info[0] = ListContent.getRowType(mContext, item, isHeader, mSliceActions);
+ info[1] = position;
+ mSliceChildView.setTag(info);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mParent != null) {
+ mParent.setClickInfo((int[]) v.getTag());
+ mParent.performClick();
+ }
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mTemplateView != null) {
+ mTemplateView.onForegroundActivated(event);
+ }
+ return false;
}
}
@@ -253,12 +302,8 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
while (items.hasNext()) {
SliceItem i = items.next();
builder.append(i.getFormat());
- //i.removeHint(Slice.HINT_SELECTED);
builder.append(i.getHints());
switch (i.getFormat()) {
- case FORMAT_IMAGE:
- builder.append(i.getIcon());
- break;
case FORMAT_TEXT:
builder.append(i.getText());
break;
diff --git a/androidx/slice/widget/LargeTemplateView.java b/androidx/slice/widget/LargeTemplateView.java
index 36409e81..66320973 100644
--- a/androidx/slice/widget/LargeTemplateView.java
+++ b/androidx/slice/widget/LargeTemplateView.java
@@ -16,8 +16,14 @@
package androidx.slice.widget;
+import static android.app.slice.Slice.HINT_HORIZONTAL;
+
import android.content.Context;
+import android.os.Build;
import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
import androidx.annotation.RestrictTo;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -26,6 +32,7 @@ import androidx.slice.Slice;
import androidx.slice.SliceItem;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -34,6 +41,8 @@ import java.util.List;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class LargeTemplateView extends SliceChildView {
+ private SliceView mParent;
+ private final View mForeground;
private final LargeSliceAdapter mAdapter;
private final RecyclerView mRecyclerView;
private Slice mSlice;
@@ -41,6 +50,7 @@ public class LargeTemplateView extends SliceChildView {
private ListContent mListContent;
private List<SliceItem> mDisplayedItems = new ArrayList<>();
private int mDisplayedItemsHeight = 0;
+ private int[] mLoc = new int[2];
public LargeTemplateView(Context context) {
super(context);
@@ -49,6 +59,23 @@ public class LargeTemplateView extends SliceChildView {
mAdapter = new LargeSliceAdapter(context);
mRecyclerView.setAdapter(mAdapter);
addView(mRecyclerView);
+
+ mForeground = new View(getContext());
+ mForeground.setBackground(SliceViewUtil.getDrawable(getContext(),
+ android.R.attr.selectableItemBackground));
+ addView(mForeground);
+
+ FrameLayout.LayoutParams lp = (LayoutParams) mForeground.getLayoutParams();
+ lp.width = LayoutParams.MATCH_PARENT;
+ lp.height = LayoutParams.MATCH_PARENT;
+ mForeground.setLayoutParams(lp);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mParent = (SliceView) getParent();
+ mAdapter.setParents(mParent, this);
}
@Override
@@ -61,20 +88,62 @@ public class LargeTemplateView extends SliceChildView {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
+ /**
+ * Called when the foreground view handling touch feedback should be activated.
+ * @param event the event to handle.
+ */
+ public void onForegroundActivated(MotionEvent event) {
+ if (mParent != null && !mParent.isSliceViewClickable()) {
+ // Only show highlight if clickable
+ mForeground.setPressed(false);
+ return;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ mForeground.getLocationOnScreen(mLoc);
+ final int x = (int) (event.getRawX() - mLoc[0]);
+ final int y = (int) (event.getRawY() - mLoc[1]);
+ mForeground.getBackground().setHotspot(x, y);
+ }
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mForeground.setPressed(true);
+ } else if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_MOVE) {
+ mForeground.setPressed(false);
+ }
+ }
+
+ @Override
+ public void setMode(int newMode) {
+ super.setMode(newMode);
+ updateDisplayedItems(getMeasuredHeight());
+ }
+
@Override
public int getActualHeight() {
return mDisplayedItemsHeight;
}
@Override
- public void setTint(int tint) {
- super.setTint(tint);
- populate();
+ public int getSmallHeight() {
+ if (mListContent == null || mListContent.getHeaderItem() == null) {
+ return 0;
+ }
+ SliceItem headerItem = mListContent.getHeaderItem();
+ if (headerItem.hasHint(HINT_HORIZONTAL)) {
+ GridContent gc = new GridContent(getContext(), headerItem);
+ return gc.getSmallHeight();
+ } else {
+ RowContent rc = new RowContent(getContext(), headerItem, mListContent.hasHeader());
+ return rc.getSmallHeight();
+ }
}
@Override
- public @SliceView.SliceMode int getMode() {
- return SliceView.MODE_LARGE;
+ public void setTint(int tint) {
+ super.setTint(tint);
+ populate();
}
@Override
@@ -97,9 +166,9 @@ public class LargeTemplateView extends SliceChildView {
}
@Override
- public void setStyle(AttributeSet attrs) {
- super.setStyle(attrs);
- mAdapter.setStyle(attrs);
+ public void setStyle(AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ super.setStyle(attrs, defStyleAttrs, defStyleRes);
+ mAdapter.setStyle(attrs, defStyleAttrs, defStyleRes);
}
@Override
@@ -148,7 +217,12 @@ public class LargeTemplateView extends SliceChildView {
mDisplayedItems = mListContent.getRowItems();
}
mDisplayedItemsHeight = ListContent.getListHeight(getContext(), mDisplayedItems);
- mAdapter.setSliceItems(mDisplayedItems, mTintColor);
+ if (getMode() == SliceView.MODE_LARGE) {
+ mAdapter.setSliceItems(mDisplayedItems, mTintColor);
+ } else if (getMode() == SliceView.MODE_SMALL) {
+ mAdapter.setSliceItems(
+ Collections.singletonList(mDisplayedItems.get(0)), mTintColor);
+ }
}
@Override
diff --git a/androidx/slice/widget/ListContent.java b/androidx/slice/widget/ListContent.java
index 3d92ce81..1cbf357f 100644
--- a/androidx/slice/widget/ListContent.java
+++ b/androidx/slice/widget/ListContent.java
@@ -102,12 +102,23 @@ public class ListContent {
}
/**
+ * Expects the provided list of items to be filtered (i.e. only things that can be turned into
+ * GridContent or RowContent) and in order (i.e. first item could be a header).
+ *
* @return the total height of all the rows contained in the provided list.
*/
public static int getListHeight(Context context, List<SliceItem> listItems) {
+ if (listItems == null) {
+ return 0;
+ }
int height = 0;
+ boolean hasRealHeader = false;
+ if (!listItems.isEmpty()) {
+ SliceItem maybeHeader = listItems.get(0);
+ hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
+ }
for (int i = 0; i < listItems.size(); i++) {
- height += getHeight(context, listItems.get(i), i == 0 /* isHeader */);
+ height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */);
}
return height;
}
@@ -192,6 +203,7 @@ public class ListContent {
return mSeeMoreItem;
}
+ @NonNull
public ArrayList<SliceItem> getRowItems() {
return mRowItems;
}
@@ -207,11 +219,25 @@ public class ListContent {
* @return the type of template that the header represents.
*/
public int getHeaderTemplateType() {
- if (mHeaderItem != null) {
- if (mHeaderItem.hasHint(HINT_HORIZONTAL)) {
+ return getRowType(mContext, mHeaderItem, true, mSliceActions);
+ }
+
+ /**
+ * The type of template that the provided row item represents.
+ *
+ * @param context context used for this slice.
+ * @param rowItem the row item to determine the template type of.
+ * @param isHeader whether this row item is used as a header.
+ * @param actions the actions associated with this slice, only matter if this row is the header.
+ * @return the type of template the provided row item represents.
+ */
+ public static int getRowType(Context context, SliceItem rowItem, boolean isHeader,
+ List<SliceItem> actions) {
+ if (rowItem != null) {
+ if (rowItem.hasHint(HINT_HORIZONTAL)) {
return EventInfo.ROW_TYPE_GRID;
} else {
- RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
+ RowContent rc = new RowContent(context, rowItem, isHeader);
SliceItem actionItem = rc.getPrimaryAction();
SliceAction primaryAction = null;
if (actionItem != null) {
@@ -223,9 +249,9 @@ public class ListContent {
: EventInfo.ROW_TYPE_PROGRESS;
} else if (primaryAction != null && primaryAction.isToggle()) {
return EventInfo.ROW_TYPE_TOGGLE;
- } else if (mSliceActions != null) {
- for (int i = 0; i < mSliceActions.size(); i++) {
- if (new SliceActionImpl(mSliceActions.get(i)).isToggle()) {
+ } else if (isHeader && actions != null) {
+ for (int i = 0; i < actions.size(); i++) {
+ if (new SliceActionImpl(actions.get(i)).isToggle()) {
return EventInfo.ROW_TYPE_TOGGLE;
}
}
@@ -284,9 +310,12 @@ public class ListContent {
return null;
}
- private static boolean isValidHeader(SliceItem sliceItem) {
+ /**
+ * @return whether the provided slice item is a valid header.
+ */
+ public static boolean isValidHeader(SliceItem sliceItem) {
if (FORMAT_SLICE.equals(sliceItem.getFormat()) && !sliceItem.hasAnyHints(HINT_LIST_ITEM,
- HINT_ACTIONS, HINT_KEYWORDS)) {
+ HINT_ACTIONS, HINT_KEYWORDS, HINT_SEE_MORE)) {
// Minimum valid header is a slice with text
SliceItem item = SliceQuery.find(sliceItem, FORMAT_TEXT, (String) null, null);
return item != null;
diff --git a/androidx/slice/widget/RowContent.java b/androidx/slice/widget/RowContent.java
index 728715ed..bc3826fd 100644
--- a/androidx/slice/widget/RowContent.java
+++ b/androidx/slice/widget/RowContent.java
@@ -23,6 +23,7 @@ import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.HINT_SUMMARY;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
+import static android.app.slice.Slice.SUBTYPE_RANGE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
@@ -34,7 +35,6 @@ import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
import static androidx.slice.core.SliceHints.HINT_TTL;
-import static androidx.slice.core.SliceHints.SUBTYPE_RANGE;
import android.content.Context;
import android.text.TextUtils;
@@ -75,14 +75,14 @@ public class RowContent {
private int mLineCount = 0;
private int mMaxHeight;
private int mMinHeight;
- private int mMaxRangeHeight;
+ private int mRangeHeight;
public RowContent(Context context, SliceItem rowSlice, boolean isHeader) {
populate(rowSlice, isHeader);
mMaxHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_max_height);
mMinHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_min_height);
- mMaxRangeHeight = context.getResources().getDimensionPixelSize(
- R.dimen.abc_slice_row_range_max_height);
+ mRangeHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.abc_slice_row_range_height);
}
/**
@@ -278,8 +278,8 @@ public class RowContent {
* @return the height to display a row at when it is used as a small template.
*/
public int getSmallHeight() {
- return (getRange() != null && mLineCount > 1)
- ? mMaxRangeHeight
+ return getRange() != null
+ ? getActualHeight()
: mMaxHeight;
}
@@ -290,10 +290,15 @@ public class RowContent {
if (!isValid()) {
return 0;
}
- if (getRange() != null && mLineCount > 1) {
- return mMaxRangeHeight;
+ int rowHeight = (getLineCount() > 1 || mIsHeader) ? mMaxHeight : mMinHeight;
+ if (getRange() != null) {
+ if (getLineCount() > 0) {
+ rowHeight += mRangeHeight;
+ } else {
+ rowHeight = mIsHeader ? mMaxHeight : mRangeHeight;
+ }
}
- return (getLineCount() > 1 || mIsHeader) ? mMaxHeight : mMinHeight;
+ return rowHeight;
}
private static boolean hasText(SliceItem textSlice) {
@@ -319,6 +324,7 @@ public class RowContent {
|| mTitleItem != null
|| mSubtitleItem != null
|| mEndItems.size() > 0
+ || mRange != null
|| isDefaultSeeMore();
}
diff --git a/androidx/slice/widget/RowView.java b/androidx/slice/widget/RowView.java
index dcbf354e..a7101fed 100644
--- a/androidx/slice/widget/RowView.java
+++ b/androidx/slice/widget/RowView.java
@@ -16,22 +16,26 @@
package androidx.slice.widget;
-import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
+import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SHORTCUT;
+import static android.app.slice.Slice.SUBTYPE_MAX;
import static android.app.slice.Slice.SUBTYPE_TOGGLE;
+import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TIMESTAMP;
-import static androidx.slice.core.SliceHints.EXTRA_RANGE_VALUE;
import static androidx.slice.core.SliceHints.ICON_IMAGE;
import static androidx.slice.core.SliceHints.SMALL_IMAGE;
-import static androidx.slice.core.SliceHints.SUBTYPE_MAX;
-import static androidx.slice.core.SliceHints.SUBTYPE_VALUE;
+import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
+import static androidx.slice.widget.EventInfo.ACTION_TYPE_BUTTON;
+import static androidx.slice.widget.EventInfo.ACTION_TYPE_TOGGLE;
+import static androidx.slice.widget.EventInfo.ROW_TYPE_LIST;
+import static androidx.slice.widget.EventInfo.ROW_TYPE_TOGGLE;
import static androidx.slice.widget.SliceView.MODE_SMALL;
import android.app.PendingIntent.CanceledException;
@@ -48,14 +52,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
-import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar;
-import android.widget.Switch;
import android.widget.TextView;
-import android.widget.ToggleButton;
import androidx.annotation.ColorInt;
import androidx.annotation.RestrictTo;
@@ -84,16 +85,16 @@ public class RowView extends SliceChildView implements View.OnClickListener {
// The number of items that fit on the right hand side of a small slice
private static final int MAX_END_ITEMS = 3;
+ private LinearLayout mRootView;
private LinearLayout mStartContainer;
private LinearLayout mContent;
private TextView mPrimaryText;
private TextView mSecondaryText;
private TextView mLastUpdatedText;
private View mDivider;
- private ArrayList<CompoundButton> mToggles = new ArrayList<>();
+ private ArrayList<SliceActionView> mToggles = new ArrayList<>();
private LinearLayout mEndContainer;
- private SeekBar mSeekBar;
- private ProgressBar mProgressBar;
+ private ProgressBar mRangeBar;
private View mSeeMoreView;
private int mRowIndex;
@@ -104,15 +105,16 @@ public class RowView extends SliceChildView implements View.OnClickListener {
private int mImageSize;
private int mIconSize;
- private int mPadding;
+ private int mRangeHeight;
public RowView(Context context) {
super(context);
mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_icon_size);
mImageSize = getContext().getResources().getDimensionPixelSize(
R.dimen.abc_slice_small_image_size);
- mPadding = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_padding);
- inflate(context, R.layout.abc_slice_small_template, this);
+ mRootView = (LinearLayout) LayoutInflater.from(context).inflate(
+ R.layout.abc_slice_small_template, this, false);
+ addView(mRootView);
mStartContainer = (LinearLayout) findViewById(R.id.icon_frame);
mContent = (LinearLayout) findViewById(android.R.id.content);
@@ -121,8 +123,9 @@ public class RowView extends SliceChildView implements View.OnClickListener {
mLastUpdatedText = (TextView) findViewById(R.id.last_updated);
mDivider = findViewById(R.id.divider);
mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame);
- mSeekBar = (SeekBar) findViewById(R.id.seek_bar);
- mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
+
+ mRangeHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.abc_slice_row_range_height);
}
@@ -136,6 +139,16 @@ public class RowView extends SliceChildView implements View.OnClickListener {
public int getActualHeight() {
return mRowContent != null && mRowContent.isValid() ? mRowContent.getActualHeight() : 0;
}
+ /**
+ * @return height row content (i.e. title, subtitle) without the height of the range element.
+ */
+ private int getRowContentHeight() {
+ int rowHeight = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
+ if (mRangeBar != null) {
+ rowHeight -= mRangeHeight;
+ }
+ return rowHeight;
+ }
@Override
public void setTint(@ColorInt int tintColor) {
@@ -164,9 +177,37 @@ public class RowView extends SliceChildView implements View.OnClickListener {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int height = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int totalHeight = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
+ int rowHeight = getRowContentHeight();
+ if (rowHeight != 0) {
+ // Might be gone if we have range / progress but nothing else
+ mRootView.setVisibility(View.VISIBLE);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(rowHeight, MeasureSpec.EXACTLY);
+ measureChild(mRootView, widthMeasureSpec, heightMeasureSpec);
+ } else {
+ mRootView.setVisibility(View.GONE);
+ }
+ if (mRangeBar != null) {
+ int rangeMeasureSpec = MeasureSpec.makeMeasureSpec(mRangeHeight, MeasureSpec.EXACTLY);
+ measureChild(mRangeBar, widthMeasureSpec, rangeMeasureSpec);
+ }
+
+ int totalHeightSpec = MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, totalHeightSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mRootView.layout(0, 0, mRootView.getMeasuredWidth(), getRowContentHeight());
+ if (mRangeBar != null) {
+ mRangeBar.layout(0, getRowContentHeight(), mRangeBar.getMeasuredWidth(),
+ getRowContentHeight() + mRangeHeight);
+ }
+ }
+
+ @Override
+ public void setSlice(Slice slice) {
+ // Nothing to do
}
/**
@@ -177,25 +218,12 @@ public class RowView extends SliceChildView implements View.OnClickListener {
SliceView.OnSliceActionListener observer) {
setSliceActionListener(observer);
mRowIndex = index;
- mIsHeader = isHeader;
+ mIsHeader = ListContent.isValidHeader(slice);
mHeaderActions = null;
mRowContent = new RowContent(getContext(), slice, mIsHeader);
populateViews();
}
- /**
- * This is called when RowView is being used as a small template.
- */
- @Override
- public void setSlice(Slice slice) {
- mRowIndex = 0;
- mIsHeader = true;
- mHeaderActions = null;
- ListContent lc = new ListContent(getContext(), slice);
- mRowContent = new RowContent(getContext(), lc.getHeaderItem(), true /* isHeader */);
- populateViews();
- }
-
private void populateViews() {
resetView();
if (mRowContent.isDefaultSeeMore()) {
@@ -209,11 +237,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
boolean showStart = false;
final SliceItem startItem = mRowContent.getStartItem();
if (startItem != null) {
- final EventInfo info = new EventInfo(getMode(),
- EventInfo.ACTION_TYPE_BUTTON,
- EventInfo.ROW_TYPE_LIST, mRowIndex);
- info.setPosition(EventInfo.POSITION_START, 0, 1);
- showStart = addItem(startItem, mTintColor, true /* isStart */, 0 /* padding */, info);
+ showStart = addItem(startItem, mTintColor, true /* isStart */);
}
mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
@@ -237,8 +261,8 @@ public class RowView extends SliceChildView implements View.OnClickListener {
mRowAction = new SliceActionImpl(primaryAction);
if (mRowAction.isToggle()) {
// If primary action is a toggle, add it and we're done
- addToggle(mRowAction, mTintColor, mEndContainer);
- setViewClickable(this, true);
+ addAction(mRowAction, mTintColor, mEndContainer, false /* isStart */);
+ setViewClickable(mRootView, true);
return;
}
}
@@ -246,7 +270,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
final SliceItem range = mRowContent.getRange();
if (range != null) {
if (mRowAction != null) {
- setViewClickable(mContent, true);
+ setViewClickable(mRootView, true);
}
addRange(range);
return;
@@ -260,7 +284,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
}
boolean hasRowAction = mRowAction != null;
if (endItems.isEmpty()) {
- if (hasRowAction) setViewClickable(this, true);
+ if (hasRowAction) setViewClickable(mRootView, true);
return;
}
@@ -271,12 +295,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
for (int i = 0; i < endItems.size(); i++) {
final SliceItem endItem = endItems.get(i);
if (itemCount < MAX_END_ITEMS) {
- final EventInfo info = new EventInfo(getMode(),
- EventInfo.ACTION_TYPE_BUTTON,
- EventInfo.ROW_TYPE_LIST, mRowIndex);
- info.setPosition(EventInfo.POSITION_END, i,
- Math.min(endItems.size(), MAX_END_ITEMS));
- if (addItem(endItem, mTintColor, false /* isStart */, mPadding, info)) {
+ if (addItem(endItem, mTintColor, false /* isStart */)) {
if (FORMAT_ACTION.equals(endItem.getFormat())) {
hasEndItemAction = true;
}
@@ -296,17 +315,15 @@ public class RowView extends SliceChildView implements View.OnClickListener {
if (itemCount > 0 && hasEndItemAction) {
setViewClickable(mContent, true);
} else {
- setViewClickable(this, true);
+ setViewClickable(mRootView, true);
}
- } else {
+ } else if (mRowContent.endItemsContainAction() && itemCount == 1) {
// If the only end item is an action, make the whole row clickable.
- if (mRowContent.endItemsContainAction() && itemCount == 1) {
- SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
- if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
- mRowAction = new SliceActionImpl(endItems.get(0));
- }
- setViewClickable(this, true);
+ SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
+ if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
+ mRowAction = new SliceActionImpl(endItems.get(0));
}
+ setViewClickable(mRootView, true);
}
}
@@ -343,34 +360,47 @@ public class RowView extends SliceChildView implements View.OnClickListener {
private void addRange(final SliceItem range) {
final boolean isSeekBar = FORMAT_ACTION.equals(range.getFormat());
- final ProgressBar progressBar = isSeekBar ? mSeekBar : mProgressBar;
+ final ProgressBar progressBar = isSeekBar
+ ? new SeekBar(getContext())
+ : new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal);
+ if (mTintColor != -1) {
+ Drawable drawable = DrawableCompat.wrap(progressBar.getProgressDrawable());
+ DrawableCompat.setTint(drawable, mTintColor);
+ progressBar.setProgressDrawable(drawable);
+ }
+ // TODO: Need to handle custom accessibility for min
+ SliceItem min = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
+ int minValue = 0;
+ if (min != null) {
+ minValue = min.getInt();
+ }
SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
if (max != null) {
- progressBar.setMax(max.getInt());
+ progressBar.setMax(max.getInt() - minValue);
}
SliceItem progress = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
if (progress != null) {
- progressBar.setProgress(progress.getInt());
+ progressBar.setProgress(progress.getInt() - minValue);
}
progressBar.setVisibility(View.VISIBLE);
- if (mTintColor != -1) {
- Drawable drawable = DrawableCompat.wrap(progressBar.getProgressDrawable());
- DrawableCompat.setTint(drawable, mTintColor);
- mProgressBar.setProgressDrawable(drawable);
- }
+ addView(progressBar);
+ mRangeBar = progressBar;
if (isSeekBar) {
SliceItem thumb = SliceQuery.find(range, FORMAT_IMAGE);
+ SeekBar seekBar = (SeekBar) mRangeBar;
if (thumb != null) {
- mSeekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
+ seekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
}
if (mTintColor != -1) {
- Drawable drawable = DrawableCompat.wrap(mSeekBar.getThumb());
+ Drawable drawable = DrawableCompat.wrap(seekBar.getThumb());
DrawableCompat.setTint(drawable, mTintColor);
- mSeekBar.setThumb(drawable);
+ seekBar.setThumb(drawable);
}
- mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ final int finalMinValue = minValue;
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ progress += finalMinValue;
try {
// TODO: sending this PendingIntent should be rate limited.
range.fireAction(getContext(),
@@ -388,85 +418,45 @@ public class RowView extends SliceChildView implements View.OnClickListener {
}
/**
- * Add a toggle view to container.
+ * Add an action view to the container.
*/
- private void addToggle(final SliceActionImpl actionContent, int color, ViewGroup container) {
- // Check if this is a custom toggle
- final CompoundButton toggle;
- if (actionContent.isToggle() && !actionContent.isDefaultToggle()) {
- IconCompat checkedIcon = actionContent.getIcon();
- if (color != -1) {
- // TODO - Should custom toggle buttons be tinted? What if the app wants diff
- // colors per state?
- checkedIcon.setTint(color);
- }
- toggle = new ToggleButton(getContext());
- ((ToggleButton) toggle).setTextOff("");
- ((ToggleButton) toggle).setTextOn("");
- toggle.setBackground(checkedIcon.loadDrawable(getContext()));
- container.addView(toggle);
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) toggle.getLayoutParams();
- lp.width = mIconSize;
- lp.height = mIconSize;
- } else {
- toggle = new Switch(getContext());
- container.addView(toggle);
+ private void addAction(final SliceActionImpl actionContent, int color, ViewGroup container,
+ boolean isStart) {
+ SliceActionView sav = new SliceActionView(getContext());
+ container.addView(sav);
+
+ boolean isToggle = actionContent.isToggle();
+ int actionType = isToggle ? ACTION_TYPE_TOGGLE : ACTION_TYPE_BUTTON;
+ int rowType = isToggle ? ROW_TYPE_TOGGLE : ROW_TYPE_LIST;
+ EventInfo info = new EventInfo(getMode(), actionType, rowType, mRowIndex);
+ if (isStart) {
+ info.setPosition(EventInfo.POSITION_START, 0, 1);
}
- CharSequence contentDesc = actionContent.getContentDescription();
- if (contentDesc != null) {
- toggle.setContentDescription(contentDesc);
+ sav.setAction(actionContent, info, mObserver, color);
+
+ if (isToggle) {
+ mToggles.add(sav);
}
- toggle.setChecked(actionContent.isChecked());
- toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- try {
- Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
- actionContent.getActionItem().fireAction(getContext(), i);
- if (mObserver != null) {
- final EventInfo info = new EventInfo(getMode(),
- EventInfo.ACTION_TYPE_TOGGLE,
- EventInfo.ROW_TYPE_TOGGLE, mRowIndex);
- info.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
- mObserver.onSliceAction(info, actionContent.getSliceItem());
- }
- } catch (CanceledException e) {
- toggle.setSelected(!isChecked);
- }
- }
- });
- mToggles.add(toggle);
}
/**
* Adds simple items to a container. Simple items include actions with icons, images, or
* timestamps.
*/
- private boolean addItem(SliceItem sliceItem, int color, boolean isStart, int padding,
- final EventInfo info) {
+ private boolean addItem(SliceItem sliceItem, int color, boolean isStart) {
IconCompat icon = null;
int imageMode = 0;
SliceItem timeStamp = null;
- SliceActionImpl actionContent = null;
ViewGroup container = isStart ? mStartContainer : mEndContainer;
if (FORMAT_SLICE.equals(sliceItem.getFormat())) {
- // It's an action.... let's make it so
if (sliceItem.hasHint(HINT_SHORTCUT)) {
- actionContent = new SliceActionImpl(sliceItem);
+ addAction(new SliceActionImpl(sliceItem), color, container, isStart);
+ return true;
} else {
sliceItem = sliceItem.getSlice().getItems().get(0);
}
}
- if (actionContent != null) {
- if (actionContent.isToggle()) {
- addToggle(actionContent, color, container);
- return true;
- }
- icon = actionContent.getIcon();
- if (icon != null) {
- imageMode = actionContent.getImageMode();
- }
- }
+
if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
icon = sliceItem.getIcon();
imageMode = sliceItem.hasHint(HINT_NO_TINT) ? SMALL_IMAGE : ICON_IMAGE;
@@ -475,23 +465,19 @@ public class RowView extends SliceChildView implements View.OnClickListener {
}
View addedView = null;
if (icon != null) {
+ boolean isIcon = imageMode == ICON_IMAGE;
ImageView iv = new ImageView(getContext());
iv.setImageDrawable(icon.loadDrawable(getContext()));
- int size = mImageSize;
- if (imageMode == ICON_IMAGE) {
- if (color != -1) {
- iv.setColorFilter(color);
- }
- size = mIconSize;
- }
- if (actionContent != null && actionContent.getContentDescription() != null) {
- iv.setContentDescription(actionContent.getContentDescription());
+ if (isIcon && color != -1) {
+ iv.setColorFilter(color);
}
container.addView(iv);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams();
- lp.width = size;
- lp.height = size;
- lp.setMarginStart(padding);
+ lp.width = mImageSize;
+ lp.height = mImageSize;
+ iv.setLayoutParams(lp);
+ int p = isIcon ? mIconSize / 2 : 0;
+ iv.setPadding(p, p, p, p);
addedView = iv;
} else if (timeStamp != null) {
TextView tv = new TextView(getContext());
@@ -501,24 +487,6 @@ public class RowView extends SliceChildView implements View.OnClickListener {
container.addView(tv);
addedView = tv;
}
- if (actionContent != null && addedView != null) {
- final SliceActionImpl finalAction = actionContent;
- addedView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- try {
- finalAction.getActionItem().fireAction(null, null);
- if (mObserver != null) {
- mObserver.onSliceAction(info, finalAction.getSliceItem());
- }
- } catch (CanceledException e) {
- e.printStackTrace();
- }
- }
- });
- addedView.setBackground(SliceViewUtil.getDrawable(getContext(),
- android.R.attr.selectableItemBackground));
- }
return addedView != null;
}
@@ -536,7 +504,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
}
mRowContent.getSlice().fireAction(null, null);
} catch (CanceledException e) {
- Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
});
@@ -544,7 +512,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
b.setTextColor(mTintColor);
}
mSeeMoreView = b;
- addView(mSeeMoreView);
+ mRootView.addView(mSeeMoreView);
}
@Override
@@ -559,7 +527,7 @@ public class RowView extends SliceChildView implements View.OnClickListener {
mObserver.onSliceAction(info, mRowAction.getSliceItem());
}
} catch (CanceledException e) {
- Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
} else if (mToggles.size() == 1) {
// If there is only one toggle and no row action, just toggle it.
@@ -569,14 +537,16 @@ public class RowView extends SliceChildView implements View.OnClickListener {
private void setViewClickable(View layout, boolean isClickable) {
layout.setOnClickListener(isClickable ? this : null);
- layout.setBackground(isClickable ? SliceViewUtil.getDrawable(getContext(),
- android.R.attr.selectableItemBackground) : null);
+ layout.setBackground(isClickable
+ ? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground)
+ : null);
layout.setClickable(isClickable);
}
@Override
public void resetView() {
- setViewClickable(this, false);
+ mRootView.setVisibility(View.VISIBLE);
+ setViewClickable(mRootView, false);
setViewClickable(mContent, false);
mStartContainer.removeAllViews();
mEndContainer.removeAllViews();
@@ -585,10 +555,11 @@ public class RowView extends SliceChildView implements View.OnClickListener {
mToggles.clear();
mRowAction = null;
mDivider.setVisibility(View.GONE);
- mSeekBar.setVisibility(View.GONE);
- mProgressBar.setVisibility(View.GONE);
+ if (mRangeBar != null) {
+ removeView(mRangeBar);
+ }
if (mSeeMoreView != null) {
- removeView(mSeeMoreView);
+ mRootView.removeView(mSeeMoreView);
}
}
}
diff --git a/androidx/slice/widget/ShortcutView.java b/androidx/slice/widget/ShortcutView.java
index d2005127..37ef17b8 100644
--- a/androidx/slice/widget/ShortcutView.java
+++ b/androidx/slice/widget/ShortcutView.java
@@ -39,6 +39,7 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.net.Uri;
+import android.util.Log;
import android.widget.ImageView;
import androidx.annotation.RestrictTo;
@@ -131,7 +132,7 @@ public class ShortcutView extends SliceChildView {
mObserver.onSliceAction(ei, interactedItem);
}
} catch (CanceledException e) {
- e.printStackTrace();
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
return true;
diff --git a/androidx/slice/widget/SliceActionView.java b/androidx/slice/widget/SliceActionView.java
new file mode 100644
index 00000000..ffe45874
--- /dev/null
+++ b/androidx/slice/widget/SliceActionView.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.slice.widget;
+
+import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.slice.core.SliceHints.ICON_IMAGE;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.CompoundButton;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.slice.core.SliceActionImpl;
+import androidx.slice.view.R;
+
+/**
+ * Supports displaying {@link androidx.slice.core.SliceActionImpl}s.
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class SliceActionView extends FrameLayout implements View.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+ private static final String TAG = "SliceActionView";
+
+ private static final int[] STATE_CHECKED = {
+ android.R.attr.state_checked
+ };
+
+ private SliceActionImpl mSliceAction;
+ private EventInfo mEventInfo;
+ private SliceView.OnSliceActionListener mObserver;
+
+ private View mActionView;
+
+ private int mIconSize;
+ private int mImageSize;
+
+ public SliceActionView(Context context) {
+ super(context);
+ Resources res = getContext().getResources();
+ mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
+ mImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size);
+ }
+
+ /**
+ * Populates the view with the provided action.
+ */
+ public void setAction(@NonNull SliceActionImpl action, EventInfo info,
+ SliceView.OnSliceActionListener listener, int color) {
+ mSliceAction = action;
+ mEventInfo = info;
+ mObserver = listener;
+ mActionView = null;
+
+ if (action.isDefaultToggle()) {
+ Switch switchView = new Switch(getContext());
+ addView(switchView);
+ switchView.setChecked(action.isChecked());
+ switchView.setOnCheckedChangeListener(this);
+ switchView.setMinimumHeight(mImageSize);
+ switchView.setMinimumWidth(mImageSize);
+ if (color != -1) {
+ // TODO - find nice way to tint toggles
+ }
+ mActionView = switchView;
+
+ } else if (action.getIcon() != null) {
+ if (action.isToggle()) {
+ ImageToggle imageToggle = new ImageToggle(getContext());
+ imageToggle.setChecked(action.isChecked());
+ mActionView = imageToggle;
+ } else {
+ mActionView = new ImageView(getContext());
+ }
+ addView(mActionView);
+
+ Drawable d = mSliceAction.getIcon().loadDrawable(getContext());
+ ((ImageView) mActionView).setImageDrawable(d);
+ if (color != -1 && mSliceAction.getImageMode() == ICON_IMAGE) {
+ // TODO - Consider allowing option for untinted custom toggles
+ DrawableCompat.setTint(d, color);
+ }
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mActionView.getLayoutParams();
+ lp.width = mImageSize;
+ lp.height = mImageSize;
+ mActionView.setLayoutParams(lp);
+ int p = action.getImageMode() == ICON_IMAGE ? mIconSize / 2 : 0;
+ mActionView.setPadding(p, p, p, p);
+ mActionView.setBackground(SliceViewUtil.getDrawable(getContext(),
+ android.R.attr.selectableItemBackground));
+ mActionView.setOnClickListener(this);
+ }
+
+ if (mActionView != null) {
+ CharSequence contentDescription = action.getContentDescription() != null
+ ? action.getContentDescription()
+ : action.getTitle();
+ mActionView.setContentDescription(contentDescription);
+ }
+ }
+
+ /**
+ * Toggles this action if it is toggleable.
+ */
+ public void toggle() {
+ if (mActionView != null && mSliceAction != null && mSliceAction.isToggle()) {
+ ((Checkable) mActionView).toggle();
+ }
+ }
+
+ /**
+ * @return the action represented in this view.
+ */
+ @Nullable
+ public SliceActionImpl getAction() {
+ return mSliceAction;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mSliceAction == null || mActionView == null) {
+ return;
+ }
+ sendAction();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (mSliceAction == null || mActionView == null) {
+ return;
+ }
+ sendAction();
+ }
+
+ private void sendAction() {
+ // TODO - Show loading indicator here?
+ try {
+ PendingIntent pi = mSliceAction.getAction();
+ if (mSliceAction.isToggle()) {
+ // Update the intent extra state
+ boolean isChecked = ((Checkable) mActionView).isChecked();
+ Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
+ mSliceAction.getActionItem().fireAction(getContext(), i);
+
+ // Update event info state
+ if (mEventInfo != null) {
+ mEventInfo.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
+ }
+ } else {
+ mSliceAction.getActionItem().fireAction(null, null);
+ }
+ if (mObserver != null && mEventInfo != null) {
+ mObserver.onSliceAction(mEventInfo, mSliceAction.getSliceItem());
+ }
+ } catch (PendingIntent.CanceledException e) {
+ if (mActionView instanceof Checkable) {
+ mActionView.setSelected(!((Checkable) mActionView).isChecked());
+ }
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ }
+ }
+
+ /**
+ * Simple class allowing a toggleable image button.
+ */
+ private static class ImageToggle extends ImageView implements Checkable, View.OnClickListener {
+ private boolean mIsChecked;
+ private View.OnClickListener mListener;
+
+ ImageToggle(Context context) {
+ super(context);
+ super.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ toggle();
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!isChecked());
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mIsChecked != checked) {
+ mIsChecked = checked;
+ refreshDrawableState();
+ if (mListener != null) {
+ mListener.onClick(this);
+ }
+ }
+ }
+
+ @Override
+ public void setOnClickListener(View.OnClickListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mIsChecked) {
+ mergeDrawableStates(drawableState, STATE_CHECKED);
+ }
+ return drawableState;
+ }
+ }
+}
diff --git a/androidx/slice/widget/SliceChildView.java b/androidx/slice/widget/SliceChildView.java
index 2e522465..2400e738 100644
--- a/androidx/slice/widget/SliceChildView.java
+++ b/androidx/slice/widget/SliceChildView.java
@@ -138,9 +138,9 @@ public abstract class SliceChildView extends FrameLayout {
/**
* Populates style information for this view.
*/
- public void setStyle(AttributeSet attrs) {
+ public void setStyle(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
- R.attr.sliceViewStyle, R.style.Widget_SliceView);
+ defStyleAttr, defStyleRes);
try {
int themeColor = a.getColor(R.styleable.SliceView_tintColor, -1);
mTintColor = themeColor != -1 ? themeColor : mTintColor;
diff --git a/androidx/slice/widget/SliceView.java b/androidx/slice/widget/SliceView.java
index bf1218b2..855bded5 100644
--- a/androidx/slice/widget/SliceView.java
+++ b/androidx/slice/widget/SliceView.java
@@ -16,10 +16,10 @@
package androidx.slice.widget;
-import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_INT;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
@@ -41,10 +41,13 @@ import androidx.lifecycle.Observer;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceMetadata;
+import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceHints;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -78,7 +81,7 @@ import java.util.List;
* </pre>
* @see SliceLiveData
*/
-public class SliceView extends ViewGroup implements Observer<Slice> {
+public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {
private static final String TAG = "SliceView";
@@ -103,6 +106,7 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
@IntDef({
MODE_SMALL, MODE_LARGE, MODE_SHORTCUT
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface SliceMode {}
/**
@@ -122,6 +126,7 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
private int mMode = MODE_LARGE;
private Slice mCurrentSlice;
+ private ListContent mListContent;
private SliceChildView mCurrentView;
private List<SliceItem> mActions;
private ActionRow mActionRow;
@@ -136,16 +141,20 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
private int mActionRowHeight;
private AttributeSet mAttrs;
+ private int mDefStyleAttr;
+ private int mDefStyleRes;
private int mThemeTintColor = -1;
private OnSliceActionListener mSliceObserver;
private int mTouchSlopSquared;
private View.OnLongClickListener mLongClickListener;
+ private View.OnClickListener mOnClickListener;
private int mDownX;
private int mDownY;
private boolean mPressing;
private boolean mInLongpress;
private Handler mHandler;
+ int[] mClickInfo;
public SliceView(Context context) {
this(context, null);
@@ -168,20 +177,16 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
mAttrs = attrs;
+ mDefStyleAttr = defStyleAttr;
+ mDefStyleRes = defStyleRes;
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
defStyleAttr, defStyleRes);
+
try {
mThemeTintColor = a.getColor(R.styleable.SliceView_tintColor, -1);
} finally {
a.recycle();
}
- // TODO: action row background should support light / dark / maybe presenter customization
- mActionRow = new ActionRow(getContext(), true);
- mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
- mCurrentView = new LargeTemplateView(getContext());
- mCurrentView.setMode(getMode());
- addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
- addView(mActionRow, getChildLp(mActionRow));
mShortcutSize = getContext().getResources()
.getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
mMinLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height);
@@ -189,13 +194,68 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
mActionRowHeight = getResources().getDimensionPixelSize(
R.dimen.abc_slice_action_row_height);
+ mCurrentView = new LargeTemplateView(getContext());
+ mCurrentView.setMode(getMode());
+ addView(mCurrentView, getChildLp(mCurrentView));
+
+ // TODO: action row background should support light / dark / maybe presenter customization
+ mActionRow = new ActionRow(getContext(), true);
+ mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
+ addView(mActionRow, getChildLp(mActionRow));
+
final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mTouchSlopSquared = slop * slop;
mHandler = new Handler();
+
+ super.setOnClickListener(this);
+ }
+
+ /**
+ * Indicates whether this view reacts to click events or not.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public boolean isSliceViewClickable() {
+ return mOnClickListener != null
+ || (mListContent != null && mListContent.getPrimaryAction() != null);
+ }
+
+ /**
+ * Sets the event info for logging a click.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public void setClickInfo(int[] info) {
+ mClickInfo = info;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mListContent != null && mListContent.getPrimaryAction() != null) {
+ try {
+ SliceActionImpl sa = new SliceActionImpl(mListContent.getPrimaryAction());
+ sa.getAction().send();
+ if (mSliceObserver != null && mClickInfo != null && mClickInfo.length > 1) {
+ EventInfo eventInfo = new EventInfo(getMode(),
+ EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]);
+ mSliceObserver.onSliceAction(eventInfo, mListContent.getPrimaryAction());
+ }
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ }
+ } else if (mOnClickListener != null) {
+ mOnClickListener.onClick(this);
+ }
+ }
+
+ @Override
+ public void setOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
}
@Override
public void setOnLongClickListener(View.OnLongClickListener listener) {
+ super.setOnLongClickListener(listener);
mLongClickListener = listener;
}
@@ -353,7 +413,18 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
}
/**
+ * @return the slice being used to populate this view.
+ */
+ @Nullable
+ public Slice getSlice() {
+ return mCurrentSlice;
+ }
+
+ /**
* Returns the slice actions presented in this view.
+ * <p>
+ * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
+ * set on the view have been adjusted using {@link #setSliceActions(List)}.
*/
@Nullable
public List<SliceItem> getSliceActions() {
@@ -466,53 +537,37 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
return mShowActions;
}
- private SliceChildView createView(int mode, boolean isGrid) {
- switch (mode) {
- case MODE_SHORTCUT:
- return new ShortcutView(getContext());
- case MODE_SMALL:
- return isGrid ? new GridRowView(getContext()) : new RowView(getContext());
- }
- return new LargeTemplateView(getContext());
- }
-
private void reinflate() {
if (mCurrentSlice == null) {
mCurrentView.resetView();
return;
}
- ListContent lc = new ListContent(getContext(), mCurrentSlice);
- if (!lc.isValid()) {
+ mListContent = new ListContent(getContext(), mCurrentSlice);
+ if (!mListContent.isValid()) {
mCurrentView.resetView();
- mCurrentView.setVisibility(View.GONE);
return;
}
+
// TODO: Smarter mapping here from one state to the next.
int mode = getMode();
- boolean reuseView = mode == mCurrentView.getMode();
- SliceItem header = lc.getHeaderItem();
- boolean isSmallGrid = header != null && SliceQuery.hasHints(header, HINT_HORIZONTAL);
- if (reuseView && mode == MODE_SMALL) {
- reuseView = (mCurrentView instanceof GridRowView) == isSmallGrid;
- }
- if (!reuseView) {
+ boolean isCurrentViewShortcut = mCurrentView instanceof ShortcutView;
+ if (mode == MODE_SHORTCUT && !isCurrentViewShortcut) {
removeAllViews();
- mCurrentView = createView(mode, isSmallGrid);
- if (mSliceObserver != null) {
- mCurrentView.setSliceActionListener(mSliceObserver);
- }
- addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
- addView(mActionRow, getChildLp(mActionRow));
- mCurrentView.setMode(mode);
+ mCurrentView = new ShortcutView(getContext());
+ addView(mCurrentView, getChildLp(mCurrentView));
+ } else if (mode != MODE_SHORTCUT && isCurrentViewShortcut) {
+ removeAllViews();
+ mCurrentView = new LargeTemplateView(getContext());
+ addView(mCurrentView, getChildLp(mCurrentView));
}
- // Scrolling
- if (mode == MODE_LARGE && (mCurrentView instanceof LargeTemplateView)) {
+ mCurrentView.setMode(mode);
+
+ mCurrentView.setSliceActionListener(mSliceObserver);
+ if (mCurrentView instanceof LargeTemplateView) {
((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
}
- // Styles
- mCurrentView.setStyle(mAttrs);
+ mCurrentView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
mCurrentView.setTint(getTintColor());
- mCurrentView.setVisibility(lc.isValid() ? View.VISIBLE : View.GONE);
// Check if the slice content is expired and show when it was last updated
SliceMetadata sliceMetadata = SliceMetadata.from(getContext(), mCurrentSlice);
diff --git a/androidx/webkit/WebViewClientCompat.java b/androidx/webkit/WebViewClientCompat.java
new file mode 100644
index 00000000..8c690f93
--- /dev/null
+++ b/androidx/webkit/WebViewClientCompat.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.os.Build;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import org.chromium.support_lib_boundary.WebViewClientBoundaryInterface;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationHandler;
+
+/**
+ * Compatibility version of {@link android.webkit.WebViewClient}.
+ */
+// Note: some methods are marked as RequiresApi 21, because only an up-to-date WebView APK would
+// ever invoke these methods (and WebView can only be updated on Lollipop and above). The app can
+// still construct a WebViewClientCompat on a pre-Lollipop devices, and explicitly invoke these
+// methods, so each of these methods must also handle this case.
+public class WebViewClientCompat extends WebViewClient implements WebViewClientBoundaryInterface {
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(value = {
+ WebViewClient.SAFE_BROWSING_THREAT_UNKNOWN,
+ WebViewClient.SAFE_BROWSING_THREAT_MALWARE,
+ WebViewClient.SAFE_BROWSING_THREAT_PHISHING,
+ WebViewClient.SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SafeBrowsingThreat {}
+
+ @Override
+ public void onPageCommitVisible(@NonNull WebView view, @NonNull String url) {
+ }
+
+ /**
+ * Invoked by chromium for the {@code onReceivedError} event. Applications are not meant to
+ * override this, and should instead override the non-final {@code onReceivedError} method.
+ * TODO(ntfschr): link to that method once it's implemented.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ public final void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
+ /* WebResourceError */ @NonNull InvocationHandler error) {
+ // TODO(ntfschr): implement this (b/73151460).
+ }
+
+ @Override
+ public void onReceivedHttpError(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @NonNull WebResourceResponse errorResponse) {
+ }
+
+ /**
+ * Invoked by chromium for the {@code onSafeBrowsingHit} event. Applications are not meant to
+ * override this, and should instead override the non-final {@code onSafeBrowsingHit} method.
+ * TODO(ntfschr): link to that method once it's implemented.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Override
+ public final void onSafeBrowsingHit(@NonNull WebView view, @NonNull WebResourceRequest request,
+ @SafeBrowsingThreat int threatType,
+ /* SafeBrowsingResponse */ @NonNull InvocationHandler callback) {
+ // TODO(ntfschr): implement this (b/73151460).
+ }
+
+ // Default behavior in WebViewClient is to invoke the other (deprecated)
+ // shouldOverrideUrlLoading method.
+ @Override
+ @SuppressWarnings("deprecation")
+ @RequiresApi(21)
+ public boolean shouldOverrideUrlLoading(@NonNull WebView view,
+ @NonNull WebResourceRequest request) {
+ if (Build.VERSION.SDK_INT < 21) return false;
+ return shouldOverrideUrlLoading(view, request.getUrl().toString());
+ }
+}
diff --git a/androidx/webkit/WebViewCompat.java b/androidx/webkit/WebViewCompat.java
index bdd53c7a..88de335b 100644
--- a/androidx/webkit/WebViewCompat.java
+++ b/androidx/webkit/WebViewCompat.java
@@ -318,7 +318,7 @@ public class WebViewCompat {
@SuppressWarnings("NewApi")
private static void checkThread(WebView webview) {
if (BuildCompat.isAtLeastP()) {
- if (webview.getLooper() != Looper.myLooper()) {
+ if (webview.getWebViewLooper() != Looper.myLooper()) {
throw new RuntimeException("A WebView method was called on thread '"
+ Thread.currentThread().getName() + "'. "
+ "All WebView methods must be called on the same thread. "
diff --git a/androidx/widget/BaseLayout.java b/androidx/widget/BaseLayout.java
new file mode 100644
index 00000000..127006e9
--- /dev/null
+++ b/androidx/widget/BaseLayout.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroup.MarginLayoutParams;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.StyleRes;
+
+import java.util.ArrayList;
+
+class BaseLayout extends ViewGroup {
+ private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+ BaseLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @RequiresApi(21)
+ BaseLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public boolean checkLayoutParams(LayoutParams p) {
+ return p instanceof MarginLayoutParams;
+ }
+
+ @Override
+ public LayoutParams generateDefaultLayoutParams() {
+ return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new MarginLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(LayoutParams lp) {
+ if (lp instanceof MarginLayoutParams) {
+ return lp;
+ }
+ return new MarginLayoutParams(lp);
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int count = getChildCount();
+
+ final boolean measureMatchParentChildren =
+ View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.EXACTLY
+ || View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.EXACTLY;
+ mMatchParentChildren.clear();
+
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ measureChildWithMargins(
+ child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = childState | child.getMeasuredState();
+ if (measureMatchParentChildren) {
+ if (lp.width == LayoutParams.MATCH_PARENT
+ || lp.height == LayoutParams.MATCH_PARENT) {
+ mMatchParentChildren.add(child);
+ }
+ }
+ }
+ }
+
+ // Account for padding too
+ maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
+ maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
+
+ // Check against our minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ // Check against our foreground's minimum height and width
+ final Drawable drawable = getForeground();
+ if (drawable != null) {
+ maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+ maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+ }
+ }
+
+ setMeasuredDimension(
+ resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << View.MEASURED_HEIGHT_STATE_SHIFT));
+
+ count = mMatchParentChildren.size();
+ if (count > 1) {
+ for (int i = 0; i < count; i++) {
+ final View child = mMatchParentChildren.get(i);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int childWidthMeasureSpec;
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ final int width = Math.max(0, getMeasuredWidth()
+ - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
+ - lp.leftMargin - lp.rightMargin);
+ childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+ width, View.MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ getPaddingLeftWithForeground() + getPaddingRightWithForeground()
+ + lp.leftMargin + lp.rightMargin, lp.width);
+ }
+
+ final int childHeightMeasureSpec;
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ final int height = Math.max(0, getMeasuredHeight()
+ - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
+ - lp.topMargin - lp.bottomMargin);
+ childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+ height, View.MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ getPaddingTopWithForeground() + getPaddingBottomWithForeground()
+ + lp.topMargin + lp.bottomMargin, lp.height);
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int count = getChildCount();
+
+ final int parentLeft = getPaddingLeftWithForeground();
+ final int parentRight = right - left - getPaddingRightWithForeground();
+
+ final int parentTop = getPaddingTopWithForeground();
+ final int parentBottom = bottom - top - getPaddingBottomWithForeground();
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != View.GONE) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+ final int width = child.getMeasuredWidth();
+ final int height = child.getMeasuredHeight();
+
+ int childLeft;
+ int childTop;
+
+ childLeft = parentLeft + (parentRight - parentLeft - width) / 2
+ + lp.leftMargin - lp.rightMargin;
+
+ childTop = parentTop + (parentBottom - parentTop - height) / 2
+ + lp.topMargin - lp.bottomMargin;
+
+ child.layout(childLeft, childTop, childLeft + width, childTop + height);
+ }
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ private int getPaddingLeftWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(getPaddingLeft(), 0) :
+ getPaddingLeft() + 0;
+ }
+
+ private int getPaddingRightWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(getPaddingRight(), 0) :
+ getPaddingRight() + 0;
+ }
+
+ private int getPaddingTopWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(getPaddingTop(), 0) :
+ getPaddingTop() + 0;
+ }
+
+ private int getPaddingBottomWithForeground() {
+ return isForegroundInsidePadding() ? Math.max(getPaddingBottom(), 0) :
+ getPaddingBottom() + 0;
+ }
+
+ // A stub method for View's isForegroundInsidePadding() which is hidden.
+ // Always returns true for now, since the default value is true.
+ // See View's isForegroundInsidePadding method.
+ private boolean isForegroundInsidePadding() {
+ return true;
+ }
+}
diff --git a/androidx/widget/MediaControlView2.java b/androidx/widget/MediaControlView2.java
new file mode 100644
index 00000000..51d427f9
--- /dev/null
+++ b/androidx/widget/MediaControlView2.java
@@ -0,0 +1,1901 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.media.R;
+import androidx.media.SessionToken2;
+// import androidx.mediarouter.app.MediaRouteButton;
+// import androidx.mediarouter.media.MediaRouter;
+// import androidx.mediarouter.media.MediaRouteSelector;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * @hide
+ * A View that contains the controls for MediaPlayer2.
+ * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
+ * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ *
+ * <p>
+ * <em> MediaControlView2 can be initialized in two different ways: </em>
+ * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
+ * adds it to the view.
+ * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ *
+ * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController,
+ * which is necessary to communicate with MediaSession2. In the second option, however, the
+ * developer needs to manually retrieve a MediaController instance and set it to MediaControlView2
+ * by calling setController(MediaController controller).
+ *
+ * <p>
+ * There is no separate method that handles the show/hide behavior for MediaControlView2. Instead,
+ * one can directly change the visibility of this view by calling View.setVisibility(int). The
+ * values supported are View.VISIBLE and View.GONE.
+ * In addition, the following customization is supported:
+ * Set focus to the play/pause button by calling requestPlayButtonFocus().
+ *
+ * <p>
+ * It is also possible to add custom buttons with custom icons and actions inside MediaControlView2.
+ * Those buttons will be shown when the overflow button is clicked.
+ * See VideoView2#setCustomActions for more details on how to add.
+ */
+@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
+@RestrictTo(LIBRARY_GROUP)
+public class MediaControlView2 extends BaseLayout {
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({
+ BUTTON_PLAY_PAUSE,
+ BUTTON_FFWD,
+ BUTTON_REW,
+ BUTTON_NEXT,
+ BUTTON_PREV,
+ BUTTON_SUBTITLE,
+ BUTTON_FULL_SCREEN,
+ BUTTON_OVERFLOW,
+ BUTTON_MUTE,
+ BUTTON_ASPECT_RATIO,
+ BUTTON_SETTINGS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Button {}
+
+ /**
+ * MediaControlView2 button value for playing and pausing media.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_PLAY_PAUSE = 1;
+ /**
+ * MediaControlView2 button value for jumping 30 seconds forward.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_FFWD = 2;
+ /**
+ * MediaControlView2 button value for jumping 10 seconds backward.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_REW = 3;
+ /**
+ * MediaControlView2 button value for jumping to next media.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_NEXT = 4;
+ /**
+ * MediaControlView2 button value for jumping to previous media.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_PREV = 5;
+ /**
+ * MediaControlView2 button value for showing/hiding subtitle track.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_SUBTITLE = 6;
+ /**
+ * MediaControlView2 button value for toggling full screen.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_FULL_SCREEN = 7;
+ /**
+ * MediaControlView2 button value for showing/hiding overflow buttons.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_OVERFLOW = 8;
+ /**
+ * MediaControlView2 button value for muting audio.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_MUTE = 9;
+ /**
+ * MediaControlView2 button value for adjusting aspect ratio of view.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_ASPECT_RATIO = 10;
+ /**
+ * MediaControlView2 button value for showing/hiding settings page.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public static final int BUTTON_SETTINGS = 11;
+
+ private static final String TAG = "MediaControlView2";
+
+ static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
+
+ // TODO: Make these constants public api to support custom video view.
+ // TODO: Combine these constants into one regarding TrackInfo.
+ static final String KEY_VIDEO_TRACK_COUNT = "VideoTrackCount";
+ static final String KEY_AUDIO_TRACK_COUNT = "AudioTrackCount";
+ static final String KEY_SUBTITLE_TRACK_COUNT = "SubtitleTrackCount";
+ static final String KEY_PLAYBACK_SPEED = "PlaybackSpeed";
+ static final String KEY_SELECTED_AUDIO_INDEX = "SelectedAudioIndex";
+ static final String KEY_SELECTED_SUBTITLE_INDEX = "SelectedSubtitleIndex";
+ static final String EVENT_UPDATE_TRACK_STATUS = "UpdateTrackStatus";
+
+ // TODO: Remove this once integrating with MediaSession2 & MediaMetadata2
+ static final String KEY_STATE_IS_ADVERTISEMENT = "MediaTypeAdvertisement";
+ static final String EVENT_UPDATE_MEDIA_TYPE_STATUS = "UpdateMediaTypeStatus";
+
+ // String for sending command to show subtitle to MediaSession.
+ static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
+ // String for sending command to hide subtitle to MediaSession.
+ static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
+ // TODO: remove once the implementation is revised
+ public static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
+ // String for sending command to select audio track to MediaSession.
+ static final String COMMAND_SELECT_AUDIO_TRACK = "SelectTrack";
+ // String for sending command to set playback speed to MediaSession.
+ static final String COMMAND_SET_PLAYBACK_SPEED = "SetPlaybackSpeed";
+ // String for sending command to mute audio to MediaSession.
+ static final String COMMAND_MUTE = "Mute";
+ // String for sending command to unmute audio to MediaSession.
+ static final String COMMAND_UNMUTE = "Unmute";
+
+ private static final int SETTINGS_MODE_AUDIO_TRACK = 0;
+ private static final int SETTINGS_MODE_PLAYBACK_SPEED = 1;
+ private static final int SETTINGS_MODE_HELP = 2;
+ private static final int SETTINGS_MODE_SUBTITLE_TRACK = 3;
+ private static final int SETTINGS_MODE_VIDEO_QUALITY = 4;
+ private static final int SETTINGS_MODE_MAIN = 5;
+ private static final int PLAYBACK_SPEED_1x_INDEX = 3;
+
+ private static final int MEDIA_TYPE_DEFAULT = 0;
+ private static final int MEDIA_TYPE_MUSIC = 1;
+ private static final int MEDIA_TYPE_ADVERTISEMENT = 2;
+
+ private static final int SIZE_TYPE_EMBEDDED = 0;
+ private static final int SIZE_TYPE_FULL = 1;
+ // TODO: add support for Minimal size type.
+ private static final int SIZE_TYPE_MINIMAL = 2;
+
+ private static final int MAX_PROGRESS = 1000;
+ private static final int DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
+ private static final int REWIND_TIME_MS = 10000;
+ private static final int FORWARD_TIME_MS = 30000;
+ private static final int AD_SKIP_WAIT_TIME_MS = 5000;
+ private static final int RESOURCE_NON_EXISTENT = -1;
+ private static final String RESOURCE_EMPTY = "";
+
+ private Resources mResources;
+ private MediaControllerCompat mController;
+ private MediaControllerCompat.TransportControls mControls;
+ private PlaybackStateCompat mPlaybackState;
+ private MediaMetadataCompat mMetadata;
+ private int mDuration;
+ private int mPrevState;
+ private int mPrevWidth;
+ private int mPrevHeight;
+ private int mOriginalLeftBarWidth;
+ private int mVideoTrackCount;
+ private int mAudioTrackCount;
+ private int mSubtitleTrackCount;
+ private int mSettingsMode;
+ private int mSelectedSubtitleTrackIndex;
+ private int mSelectedAudioTrackIndex;
+ private int mSelectedVideoQualityIndex;
+ private int mSelectedSpeedIndex;
+ private int mEmbeddedSettingsItemWidth;
+ private int mFullSettingsItemWidth;
+ private int mEmbeddedSettingsItemHeight;
+ private int mFullSettingsItemHeight;
+ private int mSettingsWindowMargin;
+ private int mMediaType;
+ private int mSizeType;
+ private int mOrientation;
+ private long mPlaybackActions;
+ private boolean mDragging;
+ private boolean mIsFullScreen;
+ private boolean mOverflowExpanded;
+ private boolean mIsStopped;
+ private boolean mSubtitleIsEnabled;
+ private boolean mSeekAvailable;
+ private boolean mIsAdvertisement;
+ private boolean mIsMute;
+ private boolean mNeedUXUpdate;
+
+ // Relating to Title Bar View
+ private ViewGroup mRoot;
+ private View mTitleBar;
+ private TextView mTitleView;
+ private View mAdExternalLink;
+ private ImageButton mBackButton;
+ // TODO (b/77158231) revive
+ // private MediaRouteButton mRouteButton;
+ // private MediaRouteSelector mRouteSelector;
+
+ // Relating to Center View
+ private ViewGroup mCenterView;
+ private View mTransportControls;
+ private ImageButton mPlayPauseButton;
+ private ImageButton mFfwdButton;
+ private ImageButton mRewButton;
+ private ImageButton mNextButton;
+ private ImageButton mPrevButton;
+
+ // Relating to Minimal Extra View
+ private LinearLayout mMinimalExtraView;
+
+ // Relating to Progress Bar View
+ private ProgressBar mProgress;
+ private View mProgressBuffer;
+
+ // Relating to Bottom Bar View
+ private ViewGroup mBottomBar;
+
+ // Relating to Bottom Bar Left View
+ private ViewGroup mBottomBarLeftView;
+ private ViewGroup mTimeView;
+ private TextView mEndTime;
+ private TextView mCurrentTime;
+ private TextView mAdSkipView;
+ private StringBuilder mFormatBuilder;
+ private Formatter mFormatter;
+
+ // Relating to Bottom Bar Right View
+ private ViewGroup mBottomBarRightView;
+ private ViewGroup mBasicControls;
+ private ViewGroup mExtraControls;
+ private ViewGroup mCustomButtons;
+ private ImageButton mSubtitleButton;
+ private ImageButton mFullScreenButton;
+ private ImageButton mOverflowButtonRight;
+ private ImageButton mOverflowButtonLeft;
+ private ImageButton mMuteButton;
+ private ImageButton mVideoQualityButton;
+ private ImageButton mSettingsButton;
+ private TextView mAdRemainingView;
+
+ // Relating to Settings List View
+ private ListView mSettingsListView;
+ private PopupWindow mSettingsWindow;
+ private SettingsAdapter mSettingsAdapter;
+ private SubSettingsAdapter mSubSettingsAdapter;
+ private List<String> mSettingsMainTextsList;
+ private List<String> mSettingsSubTextsList;
+ private List<Integer> mSettingsIconIdsList;
+ private List<String> mSubtitleDescriptionsList;
+ private List<String> mAudioTrackList;
+ private List<String> mVideoQualityList;
+ private List<String> mPlaybackSpeedTextList;
+ private List<Float> mPlaybackSpeedList;
+
+ public MediaControlView2(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+// super((instance, superProvider, privateProvider) ->
+// ApiLoader.getProvider().createMediaControlView2(
+// (MediaControlView2) instance, superProvider, privateProvider,
+// attrs, defStyleAttr, defStyleRes),
+// context, attrs, defStyleAttr, defStyleRes);
+// mProvider.initialize(attrs, defStyleAttr, defStyleRes);
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mResources = getContext().getResources();
+ // Inflate MediaControlView2 from XML
+ mRoot = makeControllerView();
+ addView(mRoot);
+ }
+
+ /**
+ * Sets MediaSession2 token to control corresponding MediaSession2.
+ */
+ public void setMediaSessionToken(SessionToken2 token) {
+ //mProvider.setMediaSessionToken_impl(token);
+ }
+
+ /**
+ * Registers a callback to be invoked when the fullscreen mode should be changed.
+ * @param l The callback that will be run
+ */
+ public void setOnFullScreenListener(OnFullScreenListener l) {
+ //mProvider.setOnFullScreenListener_impl(l);
+ }
+
+ /**
+ * @hide TODO: remove once the implementation is revised
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setController(MediaControllerCompat controller) {
+ mController = controller;
+ if (controller != null) {
+ mControls = controller.getTransportControls();
+ // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
+ // be called before the callback is called
+ mPlaybackState = mController.getPlaybackState();
+ mMetadata = mController.getMetadata();
+ updateDuration();
+ updateTitle();
+
+ mController.registerCallback(new MediaControllerCallback());
+ }
+ }
+
+ /**
+ * Changes the visibility state of an individual button. Default value is View.Visible.
+ *
+ * @param button the {@code Button} assigned to individual buttons
+ * <ul>
+ * <li>{@link #BUTTON_PLAY_PAUSE}
+ * <li>{@link #BUTTON_FFWD}
+ * <li>{@link #BUTTON_REW}
+ * <li>{@link #BUTTON_NEXT}
+ * <li>{@link #BUTTON_PREV}
+ * <li>{@link #BUTTON_SUBTITLE}
+ * <li>{@link #BUTTON_FULL_SCREEN}
+ * <li>{@link #BUTTON_MUTE}
+ * <li>{@link #BUTTON_OVERFLOW}
+ * <li>{@link #BUTTON_ASPECT_RATIO}
+ * <li>{@link #BUTTON_SETTINGS}
+ * </ul>
+ * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setButtonVisibility(@Button int button, /*@Visibility*/ int visibility) {
+ // TODO: add member variables for Fast-Forward/Prvious/Rewind buttons to save visibility in
+ // order to prevent being overriden inside updateLayout().
+ switch (button) {
+ case MediaControlView2.BUTTON_PLAY_PAUSE:
+ if (mPlayPauseButton != null && canPause()) {
+ mPlayPauseButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_FFWD:
+ if (mFfwdButton != null && canSeekForward()) {
+ mFfwdButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_REW:
+ if (mRewButton != null && canSeekBackward()) {
+ mRewButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_NEXT:
+ if (mNextButton != null) {
+ mNextButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_PREV:
+ if (mPrevButton != null) {
+ mPrevButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_SUBTITLE:
+ if (mSubtitleButton != null && mSubtitleTrackCount > 0) {
+ mSubtitleButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_FULL_SCREEN:
+ if (mFullScreenButton != null) {
+ mFullScreenButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_OVERFLOW:
+ if (mOverflowButtonRight != null) {
+ mOverflowButtonRight.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_MUTE:
+ if (mMuteButton != null) {
+ mMuteButton.setVisibility(visibility);
+ }
+ break;
+ case MediaControlView2.BUTTON_SETTINGS:
+ if (mSettingsButton != null) {
+ mSettingsButton.setVisibility(visibility);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Requests focus for the play/pause button.
+ */
+ public void requestPlayButtonFocus() {
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
+ * Application should handle the fullscreen mode accordingly.
+ */
+ public interface OnFullScreenListener {
+ /**
+ * Called to indicate a fullscreen mode change.
+ */
+ void onFullScreen(View view, boolean fullScreen);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return MediaControlView2.class.getName();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return false;
+ }
+
+ // TODO: Should this function be removed?
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ return false;
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ // Update layout when this view's width changes in order to avoid any UI overlap between
+ // transport controls.
+ if (mPrevWidth != getMeasuredWidth()
+ || mPrevHeight != getMeasuredHeight() || mNeedUXUpdate) {
+ // Dismiss SettingsWindow if it is showing.
+ mSettingsWindow.dismiss();
+
+ // These views may not have been initialized yet.
+ if (mTransportControls.getWidth() == 0 || mTimeView.getWidth() == 0) {
+ return;
+ }
+
+ int currWidth = getMeasuredWidth();
+ int currHeight = getMeasuredHeight();
+ WindowManager manager = (WindowManager) getContext().getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ Point screenSize = new Point();
+ manager.getDefaultDisplay().getSize(screenSize);
+ int screenWidth = screenSize.x;
+ int screenHeight = screenSize.y;
+ int fullIconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_full_icon_size);
+ int embeddedIconSize = mResources.getDimensionPixelSize(
+ R.dimen.mcv2_embedded_icon_size);
+ int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
+
+ // TODO: add support for Advertisement Mode.
+ if (mMediaType == MEDIA_TYPE_DEFAULT) {
+ // Max number of icons inside BottomBarRightView for Music mode is 4.
+ int maxIconCount = 4;
+ updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
+ currHeight, screenWidth, screenHeight);
+
+ } else if (mMediaType == MEDIA_TYPE_MUSIC) {
+ if (mNeedUXUpdate) {
+ // One-time operation for Music media type
+ mBasicControls.removeView(mMuteButton);
+ mExtraControls.addView(mMuteButton, 0);
+ mVideoQualityButton.setVisibility(View.GONE);
+ if (mFfwdButton != null) {
+ mFfwdButton.setVisibility(View.GONE);
+ }
+ if (mRewButton != null) {
+ mRewButton.setVisibility(View.GONE);
+ }
+ }
+ mNeedUXUpdate = false;
+
+ // Max number of icons inside BottomBarRightView for Music mode is 3.
+ int maxIconCount = 3;
+ updateLayout(maxIconCount, fullIconSize, embeddedIconSize, marginSize, currWidth,
+ currHeight, screenWidth, screenHeight);
+ }
+ mPrevWidth = currWidth;
+ mPrevHeight = currHeight;
+ }
+ // TODO: move this to a different location.
+ // Update title bar parameters in order to avoid overlap between title view and the right
+ // side of the title bar.
+ updateTitleBarLayout();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+
+ // TODO: Merge the below code with disableUnsupportedButtons().
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.setEnabled(enabled);
+ }
+ if (mFfwdButton != null) {
+ mFfwdButton.setEnabled(enabled);
+ }
+ if (mRewButton != null) {
+ mRewButton.setEnabled(enabled);
+ }
+ if (mNextButton != null) {
+ mNextButton.setEnabled(enabled);
+ }
+ if (mPrevButton != null) {
+ mPrevButton.setEnabled(enabled);
+ }
+ if (mProgress != null) {
+ mProgress.setEnabled(enabled);
+ }
+ disableUnsupportedButtons();
+ }
+
+ @Override
+ public void onVisibilityAggregated(boolean isVisible) {
+ super.onVisibilityAggregated(isVisible);
+
+ if (isVisible) {
+ disableUnsupportedButtons();
+ removeCallbacks(mUpdateProgress);
+ post(mUpdateProgress);
+ } else {
+ removeCallbacks(mUpdateProgress);
+ }
+ }
+
+ // TODO (b/77158231) revive once androidx.mediarouter.* packagaes are available.
+ /*
+ void setRouteSelector(MediaRouteSelector selector) {
+ mRouteSelector = selector;
+ if (mRouteSelector != null && !mRouteSelector.isEmpty()) {
+ mRouteButton.setRouteSelector(selector, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ mRouteButton.setVisibility(View.VISIBLE);
+ } else {
+ mRouteButton.setRouteSelector(MediaRouteSelector.EMPTY);
+ mRouteButton.setVisibility(View.GONE);
+ }
+ }
+ */
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private boolean isPlaying() {
+ if (mPlaybackState != null) {
+ return mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ }
+ return false;
+ }
+
+ private int getCurrentPosition() {
+ mPlaybackState = mController.getPlaybackState();
+ if (mPlaybackState != null) {
+ return (int) mPlaybackState.getPosition();
+ }
+ return 0;
+ }
+
+ private int getBufferPercentage() {
+ if (mDuration == 0) {
+ return 0;
+ }
+ mPlaybackState = mController.getPlaybackState();
+ if (mPlaybackState != null) {
+ long bufferedPos = mPlaybackState.getBufferedPosition();
+ return (bufferedPos == -1) ? -1 : (int) (bufferedPos * 100 / mDuration);
+ }
+ return 0;
+ }
+
+ private boolean canPause() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_PAUSE) != 0;
+ }
+ return true;
+ }
+
+ private boolean canSeekBackward() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_REWIND) != 0;
+ }
+ return true;
+ }
+
+ private boolean canSeekForward() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0;
+ }
+ return true;
+ }
+
+ /**
+ * Create the view that holds the widgets that control playback.
+ * Derived classes can override this to create their own.
+ *
+ * @return The controller view.
+ */
+ // TODO: This was "protected". Determine if it should be protected in MCV2.
+ private ViewGroup makeControllerView() {
+ ViewGroup root = (ViewGroup) inflateLayout(getContext(), R.layout.media_controller);
+ initControllerView(root);
+ return root;
+ }
+
+ // TODO(b/76444971) make sure this is compatible with ApiHelper's one in updatable.
+ private View inflateLayout(Context context, int resId) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ return inflater.inflate(resId, null);
+ }
+
+ private void initControllerView(ViewGroup v) {
+ // Relating to Title Bar View
+ mTitleBar = v.findViewById(R.id.title_bar);
+ mTitleView = v.findViewById(R.id.title_text);
+ mAdExternalLink = v.findViewById(R.id.ad_external_link);
+ mBackButton = v.findViewById(R.id.back);
+ if (mBackButton != null) {
+ mBackButton.setOnClickListener(mBackListener);
+ mBackButton.setVisibility(View.GONE);
+ }
+ // TODO (b/77158231) revive
+ // mRouteButton = v.findViewById(R.id.cast);
+
+ // Relating to Center View
+ mCenterView = v.findViewById(R.id.center_view);
+ mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
+ mCenterView.addView(mTransportControls);
+
+ // Relating to Minimal Extra View
+ mMinimalExtraView = (LinearLayout) v.findViewById(R.id.minimal_extra_view);
+ LinearLayout.LayoutParams params =
+ (LinearLayout.LayoutParams) mMinimalExtraView.getLayoutParams();
+ int iconSize = mResources.getDimensionPixelSize(R.dimen.mcv2_embedded_icon_size);
+ int marginSize = mResources.getDimensionPixelSize(R.dimen.mcv2_icon_margin);
+ params.setMargins(0, (iconSize + marginSize * 2) * (-1), 0, 0);
+ mMinimalExtraView.setLayoutParams(params);
+ mMinimalExtraView.setVisibility(View.GONE);
+
+ // Relating to Progress Bar View
+ mProgress = v.findViewById(R.id.progress);
+ if (mProgress != null) {
+ if (mProgress instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) mProgress;
+ seeker.setOnSeekBarChangeListener(mSeekListener);
+ seeker.setProgressDrawable(mResources.getDrawable(R.drawable.custom_progress));
+ seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
+ }
+ mProgress.setMax(MAX_PROGRESS);
+ }
+ mProgressBuffer = v.findViewById(R.id.progress_buffer);
+
+ // Relating to Bottom Bar View
+ mBottomBar = v.findViewById(R.id.bottom_bar);
+
+ // Relating to Bottom Bar Left View
+ mBottomBarLeftView = v.findViewById(R.id.bottom_bar_left);
+ mTimeView = v.findViewById(R.id.time);
+ mEndTime = v.findViewById(R.id.time_end);
+ mCurrentTime = v.findViewById(R.id.time_current);
+ mAdSkipView = v.findViewById(R.id.ad_skip_time);
+ mFormatBuilder = new StringBuilder();
+ mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+ // Relating to Bottom Bar Right View
+ mBottomBarRightView = v.findViewById(R.id.bottom_bar_right);
+ mBasicControls = v.findViewById(R.id.basic_controls);
+ mExtraControls = v.findViewById(R.id.extra_controls);
+ mCustomButtons = v.findViewById(R.id.custom_buttons);
+ mSubtitleButton = v.findViewById(R.id.subtitle);
+ if (mSubtitleButton != null) {
+ mSubtitleButton.setOnClickListener(mSubtitleListener);
+ }
+ mFullScreenButton = v.findViewById(R.id.fullscreen);
+ if (mFullScreenButton != null) {
+ mFullScreenButton.setOnClickListener(mFullScreenListener);
+ // TODO: Show Fullscreen button when only it is possible.
+ }
+ mOverflowButtonRight = v.findViewById(R.id.overflow_right);
+ if (mOverflowButtonRight != null) {
+ mOverflowButtonRight.setOnClickListener(mOverflowRightListener);
+ }
+ mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
+ if (mOverflowButtonLeft != null) {
+ mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
+ }
+ mMuteButton = v.findViewById(R.id.mute);
+ if (mMuteButton != null) {
+ mMuteButton.setOnClickListener(mMuteButtonListener);
+ }
+ mSettingsButton = v.findViewById(R.id.settings);
+ if (mSettingsButton != null) {
+ mSettingsButton.setOnClickListener(mSettingsButtonListener);
+ }
+ mVideoQualityButton = v.findViewById(R.id.video_quality);
+ if (mVideoQualityButton != null) {
+ mVideoQualityButton.setOnClickListener(mVideoQualityListener);
+ }
+ mAdRemainingView = v.findViewById(R.id.ad_remaining);
+
+ // Relating to Settings List View
+ initializeSettingsLists();
+ mSettingsListView = (ListView) inflateLayout(getContext(), R.layout.settings_list);
+ mSettingsAdapter = new SettingsAdapter(mSettingsMainTextsList, mSettingsSubTextsList,
+ mSettingsIconIdsList);
+ mSubSettingsAdapter = new SubSettingsAdapter(null, 0);
+ mSettingsListView.setAdapter(mSettingsAdapter);
+ mSettingsListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ mSettingsListView.setOnItemClickListener(mSettingsItemClickListener);
+
+ mEmbeddedSettingsItemWidth = mResources.getDimensionPixelSize(
+ R.dimen.mcv2_embedded_settings_width);
+ mFullSettingsItemWidth = mResources.getDimensionPixelSize(R.dimen.mcv2_full_settings_width);
+ mEmbeddedSettingsItemHeight = mResources.getDimensionPixelSize(
+ R.dimen.mcv2_embedded_settings_height);
+ mFullSettingsItemHeight = mResources.getDimensionPixelSize(
+ R.dimen.mcv2_full_settings_height);
+ mSettingsWindowMargin = (-1) * mResources.getDimensionPixelSize(
+ R.dimen.mcv2_settings_offset);
+ mSettingsWindow = new PopupWindow(mSettingsListView, mEmbeddedSettingsItemWidth,
+ ViewGroup.LayoutParams.WRAP_CONTENT, true);
+ }
+
+ /**
+ * Disable pause or seek buttons if the stream cannot be paused or seeked.
+ * This requires the control interface to be a MediaPlayerControlExt
+ */
+ private void disableUnsupportedButtons() {
+ try {
+ if (mPlayPauseButton != null && !canPause()) {
+ mPlayPauseButton.setEnabled(false);
+ }
+ if (mRewButton != null && !canSeekBackward()) {
+ mRewButton.setEnabled(false);
+ }
+ if (mFfwdButton != null && !canSeekForward()) {
+ mFfwdButton.setEnabled(false);
+ }
+ // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+ // this scheme can break the case when applications want to allow seek through the
+ // progress bar but disable forward/backward buttons.
+ //
+ // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+ // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+ // shouldn't arise in existing applications.
+ if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
+ mProgress.setEnabled(false);
+ }
+ } catch (IncompatibleClassChangeError ex) {
+ // We were given an old version of the interface, that doesn't have
+ // the canPause/canSeekXYZ methods. This is OK, it just means we
+ // assume the media can be paused and seeked, and so we don't disable
+ // the buttons.
+ }
+ }
+
+ private final Runnable mUpdateProgress = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ boolean isShowing = getVisibility() == View.VISIBLE;
+ if (!mDragging && isShowing && isPlaying()) {
+ postDelayed(mUpdateProgress,
+ DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS));
+ }
+ }
+ };
+
+ private String stringForTime(int timeMs) {
+ int totalSeconds = timeMs / 1000;
+
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+
+ mFormatBuilder.setLength(0);
+ if (hours > 0) {
+ return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ private int setProgress() {
+ if (mController == null || mDragging) {
+ return 0;
+ }
+ int positionOnProgressBar = 0;
+ int currentPosition = getCurrentPosition();
+ if (mDuration > 0) {
+ positionOnProgressBar = (int) (MAX_PROGRESS * (long) currentPosition / mDuration);
+ }
+ if (mProgress != null && currentPosition != mDuration) {
+ mProgress.setProgress(positionOnProgressBar);
+ // If the media is a local file, there is no need to set a buffer, so set secondary
+ // progress to maximum.
+ if (getBufferPercentage() < 0) {
+ mProgress.setSecondaryProgress(MAX_PROGRESS);
+ } else {
+ mProgress.setSecondaryProgress(getBufferPercentage() * 10);
+ }
+ }
+
+ if (mEndTime != null) {
+ mEndTime.setText(stringForTime(mDuration));
+
+ }
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(currentPosition));
+ }
+
+ if (mIsAdvertisement) {
+ // Update the remaining number of seconds until the first 5 seconds of the
+ // advertisement.
+ if (mAdSkipView != null) {
+ if (currentPosition <= AD_SKIP_WAIT_TIME_MS) {
+ if (mAdSkipView.getVisibility() == View.GONE) {
+ mAdSkipView.setVisibility(View.VISIBLE);
+ }
+ String skipTimeText = mResources.getString(
+ R.string.MediaControlView2_ad_skip_wait_time,
+ ((AD_SKIP_WAIT_TIME_MS - currentPosition) / 1000 + 1));
+ mAdSkipView.setText(skipTimeText);
+ } else {
+ if (mAdSkipView.getVisibility() == View.VISIBLE) {
+ mAdSkipView.setVisibility(View.GONE);
+ mNextButton.setEnabled(true);
+ mNextButton.clearColorFilter();
+ }
+ }
+ }
+ // Update the remaining number of seconds of the advertisement.
+ if (mAdRemainingView != null) {
+ int remainingTime =
+ (mDuration - currentPosition < 0) ? 0 : (mDuration - currentPosition);
+ String remainingTimeText = mResources.getString(
+ R.string.MediaControlView2_ad_remaining_time,
+ stringForTime(remainingTime));
+ mAdRemainingView.setText(remainingTimeText);
+ }
+ }
+ return currentPosition;
+ }
+
+ private void togglePausePlayState() {
+ if (isPlaying()) {
+ mControls.pause();
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_play_button_desc));
+ } else {
+ mControls.play();
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_pause_button_desc));
+ }
+ }
+
+ // There are two scenarios that can trigger the seekbar listener to trigger:
+ //
+ // The first is the user using the touchpad to adjust the posititon of the
+ // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+ // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+ // We're setting the field "mDragging" to true for the duration of the dragging
+ // session to avoid jumps in the position in case of ongoing playback.
+ //
+ // The second scenario involves the user operating the scroll ball, in this
+ // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+ // we will simply apply the updated position without suspending regular updates.
+ private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar bar) {
+ if (!mSeekAvailable) {
+ return;
+ }
+
+ mDragging = true;
+
+ // By removing these pending progress messages we make sure
+ // that a) we won't update the progress while the user adjusts
+ // the seekbar and b) once the user is done dragging the thumb
+ // we will post one of these messages to the queue again and
+ // this ensures that there will be exactly one message queued up.
+ removeCallbacks(mUpdateProgress);
+
+ // Check if playback is currently stopped. In this case, update the pause button to
+ // show the play image instead of the replay image.
+ if (mIsStopped) {
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_play_button_desc));
+ mIsStopped = false;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+ if (!mSeekAvailable) {
+ return;
+ }
+ if (!fromUser) {
+ // We're not interested in programmatically generated changes to
+ // the progress bar's position.
+ return;
+ }
+ if (mDuration > 0) {
+ int position = (int) (((long) mDuration * progress) / MAX_PROGRESS);
+ mControls.seekTo(position);
+
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(position));
+ }
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar bar) {
+ if (!mSeekAvailable) {
+ return;
+ }
+ mDragging = false;
+
+ setProgress();
+
+ // Ensure that progress is properly updated in the future,
+ // the call to show() does not guarantee this because it is a
+ // no-op if we are already showing.
+ post(mUpdateProgress);
+ }
+ };
+
+ private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ togglePausePlayState();
+ }
+ };
+
+ private final View.OnClickListener mRewListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = getCurrentPosition() - REWIND_TIME_MS;
+ mControls.seekTo(pos);
+ setProgress();
+ }
+ };
+
+ private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = getCurrentPosition() + FORWARD_TIME_MS;
+ mControls.seekTo(pos);
+ setProgress();
+ }
+ };
+
+ private final View.OnClickListener mNextListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mControls.skipToNext();
+ }
+ };
+
+ private final View.OnClickListener mPrevListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mControls.skipToPrevious();
+ }
+ };
+
+ private final View.OnClickListener mBackListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: implement
+ }
+ };
+
+ private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSettingsMode = SETTINGS_MODE_SUBTITLE_TRACK;
+ mSubSettingsAdapter.setTexts(mSubtitleDescriptionsList);
+ mSubSettingsAdapter.setCheckPosition(mSelectedSubtitleTrackIndex);
+ displaySettingsWindow(mSubSettingsAdapter);
+ }
+ };
+
+ private final View.OnClickListener mVideoQualityListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSettingsMode = SETTINGS_MODE_VIDEO_QUALITY;
+ mSubSettingsAdapter.setTexts(mVideoQualityList);
+ mSubSettingsAdapter.setCheckPosition(mSelectedVideoQualityIndex);
+ displaySettingsWindow(mSubSettingsAdapter);
+ }
+ };
+
+ private final View.OnClickListener mFullScreenListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final boolean isEnteringFullScreen = !mIsFullScreen;
+ // TODO: Re-arrange the button layouts according to the UX.
+ if (isEnteringFullScreen) {
+ mFullScreenButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
+ } else {
+ mFullScreenButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_fullscreen, null));
+ }
+ Bundle args = new Bundle();
+ args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
+ mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
+
+ mIsFullScreen = isEnteringFullScreen;
+ }
+ };
+
+ private final View.OnClickListener mOverflowRightListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mBasicControls.setVisibility(View.GONE);
+ mExtraControls.setVisibility(View.VISIBLE);
+ }
+ };
+
+ private final View.OnClickListener mOverflowLeftListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mBasicControls.setVisibility(View.VISIBLE);
+ mExtraControls.setVisibility(View.GONE);
+ }
+ };
+
+ private final View.OnClickListener mMuteButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!mIsMute) {
+ mMuteButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_mute, null));
+ mMuteButton.setContentDescription(
+ mResources.getString(R.string.mcv2_muted_button_desc));
+ mIsMute = true;
+ mController.sendCommand(COMMAND_MUTE, null, null);
+ } else {
+ mMuteButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_unmute, null));
+ mMuteButton.setContentDescription(
+ mResources.getString(R.string.mcv2_unmuted_button_desc));
+ mIsMute = false;
+ mController.sendCommand(COMMAND_UNMUTE, null, null);
+ }
+ }
+ };
+
+ private final View.OnClickListener mSettingsButtonListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSettingsMode = SETTINGS_MODE_MAIN;
+ mSettingsAdapter.setSubTexts(mSettingsSubTextsList);
+ displaySettingsWindow(mSettingsAdapter);
+ }
+ };
+
+ private final AdapterView.OnItemClickListener mSettingsItemClickListener =
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ switch (mSettingsMode) {
+ case SETTINGS_MODE_MAIN:
+ if (position == SETTINGS_MODE_AUDIO_TRACK) {
+ mSubSettingsAdapter.setTexts(mAudioTrackList);
+ mSubSettingsAdapter.setCheckPosition(mSelectedAudioTrackIndex);
+ mSettingsMode = SETTINGS_MODE_AUDIO_TRACK;
+ } else if (position == SETTINGS_MODE_PLAYBACK_SPEED) {
+ mSubSettingsAdapter.setTexts(mPlaybackSpeedTextList);
+ mSubSettingsAdapter.setCheckPosition(mSelectedSpeedIndex);
+ mSettingsMode = SETTINGS_MODE_PLAYBACK_SPEED;
+ } else if (position == SETTINGS_MODE_HELP) {
+ // TODO: implement this.
+ mSettingsWindow.dismiss();
+ return;
+ }
+ displaySettingsWindow(mSubSettingsAdapter);
+ break;
+ case SETTINGS_MODE_AUDIO_TRACK:
+ if (position != mSelectedAudioTrackIndex) {
+ mSelectedAudioTrackIndex = position;
+ if (mAudioTrackCount > 0) {
+ Bundle extra = new Bundle();
+ extra.putInt(KEY_SELECTED_AUDIO_INDEX, position);
+ mController.sendCommand(COMMAND_SELECT_AUDIO_TRACK, extra, null);
+ }
+ mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK,
+ mSubSettingsAdapter.getMainText(position));
+ }
+ mSettingsWindow.dismiss();
+ break;
+ case SETTINGS_MODE_PLAYBACK_SPEED:
+ if (position != mSelectedSpeedIndex) {
+ mSelectedSpeedIndex = position;
+ Bundle extra = new Bundle();
+ extra.putFloat(KEY_PLAYBACK_SPEED, mPlaybackSpeedList.get(position));
+ mController.sendCommand(COMMAND_SET_PLAYBACK_SPEED, extra, null);
+ mSettingsSubTextsList.set(SETTINGS_MODE_PLAYBACK_SPEED,
+ mSubSettingsAdapter.getMainText(position));
+ }
+ mSettingsWindow.dismiss();
+ break;
+ case SETTINGS_MODE_HELP:
+ // TODO: implement this.
+ break;
+ case SETTINGS_MODE_SUBTITLE_TRACK:
+ if (position != mSelectedSubtitleTrackIndex) {
+ mSelectedSubtitleTrackIndex = position;
+ if (position > 0) {
+ Bundle extra = new Bundle();
+ extra.putInt(KEY_SELECTED_SUBTITLE_INDEX, position - 1);
+ mController.sendCommand(COMMAND_SHOW_SUBTITLE, extra, null);
+ mSubtitleButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_subtitle_on, null));
+ mSubtitleButton.setContentDescription(
+ mResources.getString(R.string.mcv2_cc_is_on));
+ mSubtitleIsEnabled = true;
+ } else {
+ mController.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
+ mSubtitleButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_subtitle_off, null));
+ mSubtitleButton.setContentDescription(
+ mResources.getString(R.string.mcv2_cc_is_off));
+ mSubtitleIsEnabled = false;
+ }
+ }
+ mSettingsWindow.dismiss();
+ break;
+ case SETTINGS_MODE_VIDEO_QUALITY:
+ // TODO: add support for video quality
+ mSelectedVideoQualityIndex = position;
+ mSettingsWindow.dismiss();
+ break;
+ }
+ }
+ };
+
+ private void updateDuration() {
+ if (mMetadata != null) {
+ if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) {
+ mDuration = (int) mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
+ // update progress bar
+ setProgress();
+ }
+ }
+ }
+
+ private void updateTitle() {
+ if (mMetadata != null) {
+ if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) {
+ mTitleView.setText(mMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE));
+ }
+ }
+ }
+
+ // The title bar is made up of two separate LinearLayouts. If the sum of the two bars are
+ // greater than the length of the title bar, reduce the size of the left bar (which makes the
+ // TextView that contains the title of the media file shrink).
+ private void updateTitleBarLayout() {
+ if (mTitleBar != null) {
+ int titleBarWidth = mTitleBar.getWidth();
+
+ View leftBar = mTitleBar.findViewById(R.id.title_bar_left);
+ View rightBar = mTitleBar.findViewById(R.id.title_bar_right);
+ int leftBarWidth = leftBar.getWidth();
+ int rightBarWidth = rightBar.getWidth();
+
+ RelativeLayout.LayoutParams params =
+ (RelativeLayout.LayoutParams) leftBar.getLayoutParams();
+ if (leftBarWidth + rightBarWidth > titleBarWidth) {
+ params.width = titleBarWidth - rightBarWidth;
+ mOriginalLeftBarWidth = leftBarWidth;
+ } else if (leftBarWidth + rightBarWidth < titleBarWidth && mOriginalLeftBarWidth != 0) {
+ params.width = mOriginalLeftBarWidth;
+ mOriginalLeftBarWidth = 0;
+ }
+ leftBar.setLayoutParams(params);
+ }
+ }
+
+ private void updateAudioMetadata() {
+ if (mMediaType != MEDIA_TYPE_MUSIC) {
+ return;
+ }
+
+ if (mMetadata != null) {
+ String titleText = "";
+ String artistText = "";
+ if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) {
+ titleText = mMetadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
+ } else {
+ titleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+ }
+
+ if (mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_ARTIST)) {
+ artistText = mMetadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
+ } else {
+ artistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+ }
+
+ // Update title for Embedded size type
+ mTitleView.setText(titleText + " - " + artistText);
+
+ // Set to true to update layout inside onMeasure()
+ mNeedUXUpdate = true;
+ }
+ }
+
+ private void updateLayout() {
+ if (mIsAdvertisement) {
+ mRewButton.setVisibility(View.GONE);
+ mFfwdButton.setVisibility(View.GONE);
+ mPrevButton.setVisibility(View.GONE);
+ mTimeView.setVisibility(View.GONE);
+
+ mAdSkipView.setVisibility(View.VISIBLE);
+ mAdRemainingView.setVisibility(View.VISIBLE);
+ mAdExternalLink.setVisibility(View.VISIBLE);
+
+ mProgress.setEnabled(false);
+ mNextButton.setEnabled(false);
+ mNextButton.setColorFilter(R.color.gray);
+ } else {
+ mRewButton.setVisibility(View.VISIBLE);
+ mFfwdButton.setVisibility(View.VISIBLE);
+ mPrevButton.setVisibility(View.VISIBLE);
+ mTimeView.setVisibility(View.VISIBLE);
+
+ mAdSkipView.setVisibility(View.GONE);
+ mAdRemainingView.setVisibility(View.GONE);
+ mAdExternalLink.setVisibility(View.GONE);
+
+ mProgress.setEnabled(true);
+ mNextButton.setEnabled(true);
+ mNextButton.clearColorFilter();
+ disableUnsupportedButtons();
+ }
+ }
+
+ private void updateLayout(int maxIconCount, int fullIconSize, int embeddedIconSize,
+ int marginSize, int currWidth, int currHeight, int screenWidth, int screenHeight) {
+ int fullBottomBarRightWidthMax = fullIconSize * maxIconCount
+ + marginSize * (maxIconCount * 2);
+ int embeddedBottomBarRightWidthMax = embeddedIconSize * maxIconCount
+ + marginSize * (maxIconCount * 2);
+ int fullWidth = mTransportControls.getWidth() + mTimeView.getWidth()
+ + fullBottomBarRightWidthMax;
+ int embeddedWidth = mTimeView.getWidth() + embeddedBottomBarRightWidthMax;
+ int screenMaxLength = Math.max(screenWidth, screenHeight);
+
+ if (fullWidth > screenMaxLength) {
+ // TODO: screen may be smaller than the length needed for Full size.
+ }
+
+ boolean isFullSize = (mMediaType == MEDIA_TYPE_DEFAULT) ? (currWidth == screenMaxLength) :
+ (currWidth == screenWidth && currHeight == screenHeight);
+
+ if (isFullSize) {
+ if (mSizeType != SIZE_TYPE_FULL) {
+ updateLayoutForSizeChange(SIZE_TYPE_FULL);
+ if (mMediaType == MEDIA_TYPE_MUSIC) {
+ mTitleView.setVisibility(View.GONE);
+ }
+ }
+ } else if (embeddedWidth <= currWidth) {
+ if (mSizeType != SIZE_TYPE_EMBEDDED) {
+ updateLayoutForSizeChange(SIZE_TYPE_EMBEDDED);
+ if (mMediaType == MEDIA_TYPE_MUSIC) {
+ mTitleView.setVisibility(View.VISIBLE);
+ }
+ }
+ } else {
+ if (mSizeType != SIZE_TYPE_MINIMAL) {
+ updateLayoutForSizeChange(SIZE_TYPE_MINIMAL);
+ if (mMediaType == MEDIA_TYPE_MUSIC) {
+ mTitleView.setVisibility(View.GONE);
+ }
+ }
+ }
+ }
+
+ private void updateLayoutForSizeChange(int sizeType) {
+ mSizeType = sizeType;
+ RelativeLayout.LayoutParams timeViewParams =
+ (RelativeLayout.LayoutParams) mTimeView.getLayoutParams();
+ SeekBar seeker = (SeekBar) mProgress;
+ switch (mSizeType) {
+ case SIZE_TYPE_EMBEDDED:
+ // Relating to Title Bar
+ mTitleBar.setVisibility(View.VISIBLE);
+ mBackButton.setVisibility(View.GONE);
+
+ // Relating to Full Screen Button
+ mMinimalExtraView.setVisibility(View.GONE);
+ mFullScreenButton = mBottomBarRightView.findViewById(R.id.fullscreen);
+ mFullScreenButton.setOnClickListener(mFullScreenListener);
+
+ // Relating to Center View
+ mCenterView.removeAllViews();
+ mBottomBarLeftView.removeView(mTransportControls);
+ mBottomBarLeftView.setVisibility(View.GONE);
+ mTransportControls = inflateTransportControls(R.layout.embedded_transport_controls);
+ mCenterView.addView(mTransportControls);
+
+ // Relating to Progress Bar
+ seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
+ mProgressBuffer.setVisibility(View.VISIBLE);
+
+ // Relating to Bottom Bar
+ mBottomBar.setVisibility(View.VISIBLE);
+ if (timeViewParams.getRules()[RelativeLayout.LEFT_OF] != 0) {
+ timeViewParams.removeRule(RelativeLayout.LEFT_OF);
+ timeViewParams.addRule(RelativeLayout.RIGHT_OF, R.id.bottom_bar_left);
+ }
+ break;
+ case SIZE_TYPE_FULL:
+ // Relating to Title Bar
+ mTitleBar.setVisibility(View.VISIBLE);
+ mBackButton.setVisibility(View.VISIBLE);
+
+ // Relating to Full Screen Button
+ mMinimalExtraView.setVisibility(View.GONE);
+ mFullScreenButton = mBottomBarRightView.findViewById(R.id.fullscreen);
+ mFullScreenButton.setOnClickListener(mFullScreenListener);
+
+ // Relating to Center View
+ mCenterView.removeAllViews();
+ mBottomBarLeftView.removeView(mTransportControls);
+ mTransportControls = inflateTransportControls(R.layout.full_transport_controls);
+ mBottomBarLeftView.addView(mTransportControls, 0);
+ mBottomBarLeftView.setVisibility(View.VISIBLE);
+
+ // Relating to Progress Bar
+ seeker.setThumb(mResources.getDrawable(R.drawable.custom_progress_thumb));
+ mProgressBuffer.setVisibility(View.VISIBLE);
+
+ // Relating to Bottom Bar
+ mBottomBar.setVisibility(View.VISIBLE);
+ if (timeViewParams.getRules()[RelativeLayout.RIGHT_OF] != 0) {
+ timeViewParams.removeRule(RelativeLayout.RIGHT_OF);
+ timeViewParams.addRule(RelativeLayout.LEFT_OF, R.id.bottom_bar_right);
+ }
+ break;
+ case SIZE_TYPE_MINIMAL:
+ // Relating to Title Bar
+ mTitleBar.setVisibility(View.GONE);
+ mBackButton.setVisibility(View.GONE);
+
+ // Relating to Full Screen Button
+ mMinimalExtraView.setVisibility(View.VISIBLE);
+ mFullScreenButton = mMinimalExtraView.findViewById(R.id.minimal_fullscreen);
+ mFullScreenButton.setOnClickListener(mFullScreenListener);
+
+ // Relating to Center View
+ mCenterView.removeAllViews();
+ mBottomBarLeftView.removeView(mTransportControls);
+ mTransportControls = inflateTransportControls(R.layout.minimal_transport_controls);
+ mCenterView.addView(mTransportControls);
+
+ // Relating to Progress Bar
+ seeker.setThumb(null);
+ mProgressBuffer.setVisibility(View.GONE);
+
+ // Relating to Bottom Bar
+ mBottomBar.setVisibility(View.GONE);
+ break;
+ }
+ mTimeView.setLayoutParams(timeViewParams);
+
+ if (isPlaying()) {
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_pause_button_desc));
+ } else {
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_play_button_desc));
+ }
+
+ if (mIsFullScreen) {
+ mFullScreenButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_fullscreen_exit, null));
+ } else {
+ mFullScreenButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_fullscreen, null));
+ }
+ }
+
+ private View inflateTransportControls(int layoutId) {
+ View v = inflateLayout(getContext(), layoutId);
+ mPlayPauseButton = v.findViewById(R.id.pause);
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ mPlayPauseButton.setOnClickListener(mPlayPauseListener);
+ }
+ mFfwdButton = v.findViewById(R.id.ffwd);
+ if (mFfwdButton != null) {
+ mFfwdButton.setOnClickListener(mFfwdListener);
+ if (mMediaType == MEDIA_TYPE_MUSIC) {
+ mFfwdButton.setVisibility(View.GONE);
+ }
+ }
+ mRewButton = v.findViewById(R.id.rew);
+ if (mRewButton != null) {
+ mRewButton.setOnClickListener(mRewListener);
+ if (mMediaType == MEDIA_TYPE_MUSIC) {
+ mRewButton.setVisibility(View.GONE);
+ }
+ }
+ // TODO: Add support for Next and Previous buttons
+ mNextButton = v.findViewById(R.id.next);
+ if (mNextButton != null) {
+ mNextButton.setOnClickListener(mNextListener);
+ mNextButton.setVisibility(View.GONE);
+ }
+ mPrevButton = v.findViewById(R.id.prev);
+ if (mPrevButton != null) {
+ mPrevButton.setOnClickListener(mPrevListener);
+ mPrevButton.setVisibility(View.GONE);
+ }
+ return v;
+ }
+
+ private void initializeSettingsLists() {
+ mSettingsMainTextsList = new ArrayList<String>();
+ mSettingsMainTextsList.add(
+ mResources.getString(R.string.MediaControlView2_audio_track_text));
+ mSettingsMainTextsList.add(
+ mResources.getString(R.string.MediaControlView2_playback_speed_text));
+ mSettingsMainTextsList.add(
+ mResources.getString(R.string.MediaControlView2_help_text));
+
+ mSettingsSubTextsList = new ArrayList<String>();
+ mSettingsSubTextsList.add(
+ mResources.getString(R.string.MediaControlView2_audio_track_none_text));
+ mSettingsSubTextsList.add(
+ mResources.getStringArray(
+ R.array.MediaControlView2_playback_speeds)[PLAYBACK_SPEED_1x_INDEX]);
+ mSettingsSubTextsList.add(RESOURCE_EMPTY);
+
+ mSettingsIconIdsList = new ArrayList<Integer>();
+ mSettingsIconIdsList.add(R.drawable.ic_audiotrack);
+ mSettingsIconIdsList.add(R.drawable.ic_play_circle_filled);
+ mSettingsIconIdsList.add(R.drawable.ic_help);
+
+ mAudioTrackList = new ArrayList<String>();
+ mAudioTrackList.add(
+ mResources.getString(R.string.MediaControlView2_audio_track_none_text));
+
+ mVideoQualityList = new ArrayList<String>();
+ mVideoQualityList.add(
+ mResources.getString(R.string.MediaControlView2_video_quality_auto_text));
+
+ mPlaybackSpeedTextList = new ArrayList<String>(Arrays.asList(
+ mResources.getStringArray(R.array.MediaControlView2_playback_speeds)));
+ // Select the "1x" speed as the default value.
+ mSelectedSpeedIndex = PLAYBACK_SPEED_1x_INDEX;
+
+ mPlaybackSpeedList = new ArrayList<Float>();
+ int[] speeds = mResources.getIntArray(R.array.speed_multiplied_by_100);
+ for (int i = 0; i < speeds.length; i++) {
+ float speed = (float) speeds[i] / 100.0f;
+ mPlaybackSpeedList.add(speed);
+ }
+ }
+
+ private void displaySettingsWindow(BaseAdapter adapter) {
+ // Set Adapter
+ mSettingsListView.setAdapter(adapter);
+
+ // Set width of window
+ int itemWidth = (mSizeType == SIZE_TYPE_EMBEDDED)
+ ? mEmbeddedSettingsItemWidth : mFullSettingsItemWidth;
+ mSettingsWindow.setWidth(itemWidth);
+
+ // Calculate height of window and show
+ int itemHeight = (mSizeType == SIZE_TYPE_EMBEDDED)
+ ? mEmbeddedSettingsItemHeight : mFullSettingsItemHeight;
+ int totalHeight = adapter.getCount() * itemHeight;
+ mSettingsWindow.dismiss();
+ mSettingsWindow.showAsDropDown(this, mSettingsWindowMargin,
+ mSettingsWindowMargin - totalHeight, Gravity.BOTTOM | Gravity.RIGHT);
+ }
+
+ @RequiresApi(26) // TODO correct minSdk API use incompatibilities and remove before release.
+ private class MediaControllerCallback extends MediaControllerCompat.Callback {
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ mPlaybackState = state;
+
+ // Update pause button depending on playback state for the following two reasons:
+ // 1) Need to handle case where app customizes playback state behavior when app
+ // activity is resumed.
+ // 2) Need to handle case where the media file reaches end of duration.
+ if (mPlaybackState.getState() != mPrevState) {
+ switch (mPlaybackState.getState()) {
+ case PlaybackStateCompat.STATE_PLAYING:
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_pause_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_pause_button_desc));
+ removeCallbacks(mUpdateProgress);
+ post(mUpdateProgress);
+ break;
+ case PlaybackStateCompat.STATE_PAUSED:
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_play_button_desc));
+ break;
+ case PlaybackStateCompat.STATE_STOPPED:
+ mPlayPauseButton.setImageDrawable(
+ mResources.getDrawable(R.drawable.ic_replay_circle_filled, null));
+ mPlayPauseButton.setContentDescription(
+ mResources.getString(R.string.mcv2_replay_button_desc));
+ mIsStopped = true;
+ break;
+ default:
+ break;
+ }
+ mPrevState = mPlaybackState.getState();
+ }
+
+ if (mPlaybackActions != mPlaybackState.getActions()) {
+ long newActions = mPlaybackState.getActions();
+ if ((newActions & PlaybackStateCompat.ACTION_PAUSE) != 0) {
+ mPlayPauseButton.setVisibility(View.VISIBLE);
+ }
+ if ((newActions & PlaybackStateCompat.ACTION_REWIND) != 0
+ && mMediaType != MEDIA_TYPE_MUSIC) {
+ if (mRewButton != null) {
+ mRewButton.setVisibility(View.VISIBLE);
+ }
+ }
+ if ((newActions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0
+ && mMediaType != MEDIA_TYPE_MUSIC) {
+ if (mFfwdButton != null) {
+ mFfwdButton.setVisibility(View.VISIBLE);
+ }
+ }
+ if ((newActions & PlaybackStateCompat.ACTION_SEEK_TO) != 0) {
+ mSeekAvailable = true;
+ } else {
+ mSeekAvailable = false;
+ }
+ mPlaybackActions = newActions;
+ }
+
+ // Add buttons if custom actions are present.
+ List<PlaybackStateCompat.CustomAction> customActions =
+ mPlaybackState.getCustomActions();
+ mCustomButtons.removeAllViews();
+ if (customActions.size() > 0) {
+ for (final PlaybackStateCompat.CustomAction action : customActions) {
+ ImageButton button = new ImageButton(getContext(),
+ null /* AttributeSet */, 0 /* Style */);
+ // TODO: Apply R.style.BottomBarButton to this button using library context.
+ // Refer Constructor with argument (int defStyleRes) of View.java
+ button.setImageResource(action.getIcon());
+ button.setTooltipText(action.getName());
+ final String actionString = action.getAction().toString();
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: Currently, we are just sending extras that came from session.
+ // Is it the right behavior?
+ mControls.sendCustomAction(actionString, action.getExtras());
+ setVisibility(View.VISIBLE);
+ }
+ });
+ mCustomButtons.addView(button);
+ }
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ mMetadata = metadata;
+ updateDuration();
+ updateTitle();
+ updateAudioMetadata();
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ switch (event) {
+ case EVENT_UPDATE_TRACK_STATUS:
+ mVideoTrackCount = extras.getInt(KEY_VIDEO_TRACK_COUNT);
+ // If there is one or more audio tracks, and this information has not been
+ // reflected into the Settings window yet, automatically check the first track.
+ // Otherwise, the Audio Track selection will be defaulted to "None".
+ mAudioTrackCount = extras.getInt(KEY_AUDIO_TRACK_COUNT);
+ mAudioTrackList = new ArrayList<String>();
+ if (mAudioTrackCount > 0) {
+ // TODO: add more text about track info.
+ for (int i = 0; i < mAudioTrackCount; i++) {
+ String track = mResources.getString(
+ R.string.MediaControlView2_audio_track_number_text, i + 1);
+ mAudioTrackList.add(track);
+ }
+ // Change sub text inside the Settings window.
+ mSettingsSubTextsList.set(SETTINGS_MODE_AUDIO_TRACK,
+ mAudioTrackList.get(0));
+ } else {
+ mAudioTrackList.add(mResources.getString(
+ R.string.MediaControlView2_audio_track_none_text));
+ }
+ if (mVideoTrackCount == 0 && mAudioTrackCount > 0) {
+ mMediaType = MEDIA_TYPE_MUSIC;
+ }
+
+ mSubtitleTrackCount = extras.getInt(KEY_SUBTITLE_TRACK_COUNT);
+ mSubtitleDescriptionsList = new ArrayList<String>();
+ if (mSubtitleTrackCount > 0) {
+ mSubtitleButton.setVisibility(View.VISIBLE);
+ mSubtitleButton.setEnabled(true);
+ mSubtitleDescriptionsList.add(mResources.getString(
+ R.string.MediaControlView2_subtitle_off_text));
+ for (int i = 0; i < mSubtitleTrackCount; i++) {
+ String track = mResources.getString(
+ R.string.MediaControlView2_subtitle_track_number_text, i + 1);
+ mSubtitleDescriptionsList.add(track);
+ }
+ } else {
+ mSubtitleButton.setVisibility(View.GONE);
+ mSubtitleButton.setEnabled(false);
+ }
+ break;
+ case EVENT_UPDATE_MEDIA_TYPE_STATUS:
+ boolean newStatus = extras.getBoolean(KEY_STATE_IS_ADVERTISEMENT);
+ if (newStatus != mIsAdvertisement) {
+ mIsAdvertisement = newStatus;
+ updateLayout();
+ }
+ break;
+ }
+ }
+ }
+
+ private class SettingsAdapter extends BaseAdapter {
+ private List<Integer> mIconIds;
+ private List<String> mMainTexts;
+ private List<String> mSubTexts;
+
+ SettingsAdapter(List<String> mainTexts, @Nullable List<String> subTexts,
+ @Nullable List<Integer> iconIds) {
+ mMainTexts = mainTexts;
+ mSubTexts = subTexts;
+ mIconIds = iconIds;
+ }
+
+ public void updateSubTexts(List<String> subTexts) {
+ mSubTexts = subTexts;
+ notifyDataSetChanged();
+ }
+
+ public String getMainText(int position) {
+ if (mMainTexts != null) {
+ if (position < mMainTexts.size()) {
+ return mMainTexts.get(position);
+ }
+ }
+ return RESOURCE_EMPTY;
+ }
+
+ @Override
+ public int getCount() {
+ return (mMainTexts == null) ? 0 : mMainTexts.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ // Auto-generated method stub--does not have any purpose here
+ // TODO: implement this.
+ return 0;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ // Auto-generated method stub--does not have any purpose here
+ // TODO: implement this.
+ return null;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup container) {
+ View row;
+ if (mSizeType == SIZE_TYPE_FULL) {
+ row = inflateLayout(getContext(), R.layout.full_settings_list_item);
+ } else {
+ row = inflateLayout(getContext(), R.layout.embedded_settings_list_item);
+ }
+ TextView mainTextView = (TextView) row.findViewById(R.id.main_text);
+ TextView subTextView = (TextView) row.findViewById(R.id.sub_text);
+ ImageView iconView = (ImageView) row.findViewById(R.id.icon);
+
+ // Set main text
+ mainTextView.setText(mMainTexts.get(position));
+
+ // Remove sub text and center the main text if sub texts do not exist at all or the sub
+ // text at this particular position is empty.
+ if (mSubTexts == null || RESOURCE_EMPTY.equals(mSubTexts.get(position))) {
+ subTextView.setVisibility(View.GONE);
+ } else {
+ // Otherwise, set sub text.
+ subTextView.setText(mSubTexts.get(position));
+ }
+
+ // Remove main icon and set visibility to gone if icons are set to null or the icon at
+ // this particular position is set to RESOURCE_NON_EXISTENT.
+ if (mIconIds == null || mIconIds.get(position) == RESOURCE_NON_EXISTENT) {
+ iconView.setVisibility(View.GONE);
+ } else {
+ // Otherwise, set main icon.
+ iconView.setImageDrawable(mResources.getDrawable(mIconIds.get(position), null));
+ }
+ return row;
+ }
+
+ public void setSubTexts(List<String> subTexts) {
+ mSubTexts = subTexts;
+ }
+ }
+
+ // TODO: extend this class from SettingsAdapter
+ private class SubSettingsAdapter extends BaseAdapter {
+ private List<String> mTexts;
+ private int mCheckPosition;
+
+ SubSettingsAdapter(List<String> texts, int checkPosition) {
+ mTexts = texts;
+ mCheckPosition = checkPosition;
+ }
+
+ public String getMainText(int position) {
+ if (mTexts != null) {
+ if (position < mTexts.size()) {
+ return mTexts.get(position);
+ }
+ }
+ return RESOURCE_EMPTY;
+ }
+
+ @Override
+ public int getCount() {
+ return (mTexts == null) ? 0 : mTexts.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ // Auto-generated method stub--does not have any purpose here
+ // TODO: implement this.
+ return 0;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ // Auto-generated method stub--does not have any purpose here
+ // TODO: implement this.
+ return null;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup container) {
+ View row;
+ if (mSizeType == SIZE_TYPE_FULL) {
+ row = inflateLayout(getContext(), R.layout.full_sub_settings_list_item);
+ } else {
+ row = inflateLayout(getContext(), R.layout.embedded_sub_settings_list_item);
+ }
+ TextView textView = (TextView) row.findViewById(R.id.text);
+ ImageView checkView = (ImageView) row.findViewById(R.id.check);
+
+ textView.setText(mTexts.get(position));
+ if (position != mCheckPosition) {
+ checkView.setVisibility(View.INVISIBLE);
+ }
+ return row;
+ }
+
+ public void setTexts(List<String> texts) {
+ mTexts = texts;
+ }
+
+ public void setCheckPosition(int checkPosition) {
+ mCheckPosition = checkPosition;
+ }
+ }
+}
diff --git a/androidx/widget/MediaUtils2.java b/androidx/widget/MediaUtils2.java
new file mode 100644
index 00000000..29eb9f6a
--- /dev/null
+++ b/androidx/widget/MediaUtils2.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.widget;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.util.Log;
+
+import java.io.IOException;
+
+// Borrowed from com.android.compatibility.common.util.MediaUtils
+class MediaUtils2 {
+ private static final String TAG = "MediaUtils2";
+ private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+ /**
+ * Returns true iff all audio and video tracks are supported
+ */
+ static boolean hasCodecsForResource(Context context, int resourceId) {
+ try {
+ AssetFileDescriptor afd = null;
+ MediaExtractor ex = null;
+ try {
+ afd = context.getResources().openRawResourceFd(resourceId);
+ ex = new MediaExtractor();
+ ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+ return hasCodecsForMedia(ex);
+ } finally {
+ if (ex != null) {
+ ex.release();
+ }
+ if (afd != null) {
+ afd.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.i(TAG, "could not open resource");
+ }
+ return false;
+ }
+
+ /**
+ * Returns true iff all audio and video tracks are supported
+ */
+ static boolean hasCodecsForMedia(MediaExtractor ex) {
+ for (int i = 0; i < ex.getTrackCount(); ++i) {
+ MediaFormat format = ex.getTrackFormat(i);
+ // only check for audio and video codecs
+ String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
+ if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
+ continue;
+ }
+ if (!canDecode(format)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static boolean canDecode(MediaFormat format) {
+ if (sMCL.findDecoderForFormat(format) == null) {
+ Log.i(TAG, "no decoder for " + format);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/androidx/widget/VideoSurfaceView.java b/androidx/widget/VideoSurfaceView.java
new file mode 100644
index 00000000..eafa6f33
--- /dev/null
+++ b/androidx/widget/VideoSurfaceView.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import static androidx.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.MediaPlayer;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
+ private static final String TAG = "VideoSurfaceView";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+ private SurfaceHolder mSurfaceHolder = null;
+ private SurfaceListener mSurfaceListener = null;
+ private MediaPlayer mMediaPlayer;
+ // A flag to indicate taking over other view should be proceed.
+ private boolean mIsTakingOverOldView;
+ private VideoViewInterface mOldView;
+
+
+ VideoSurfaceView(Context context) {
+ this(context, null);
+ }
+
+ VideoSurfaceView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ getHolder().addCallback(this);
+ }
+
+ @RequiresApi(21)
+ VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ getHolder().addCallback(this);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements VideoViewInterface
+ ////////////////////////////////////////////////////
+
+ @Override
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceHolder: " + mSurfaceHolder);
+ if (mp == null || !hasAvailableSurface()) {
+ return false;
+ }
+ mp.setDisplay(mSurfaceHolder);
+ return true;
+ }
+
+ @Override
+ public void setSurfaceListener(SurfaceListener l) {
+ mSurfaceListener = l;
+ }
+
+ @Override
+ public int getViewType() {
+ return VIEW_TYPE_SURFACEVIEW;
+ }
+
+ @Override
+ public void setMediaPlayer(MediaPlayer mp) {
+ mMediaPlayer = mp;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ }
+ }
+
+ @Override
+ public void takeOver(@NonNull VideoViewInterface oldView) {
+ if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+ ((View) oldView).setVisibility(GONE);
+ mIsTakingOverOldView = false;
+ mOldView = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceTakeOverDone(this);
+ }
+ } else {
+ mIsTakingOverOldView = true;
+ mOldView = oldView;
+ }
+ }
+
+ @Override
+ public boolean hasAvailableSurface() {
+ return (mSurfaceHolder != null && mSurfaceHolder.getSurface() != null);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements SurfaceHolder.Callback
+ ////////////////////////////////////////////////////
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.d(TAG, "surfaceCreated: mSurfaceHolder: " + mSurfaceHolder + ", new holder: " + holder);
+ mSurfaceHolder = holder;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ } else {
+ assignSurfaceToMediaPlayer(mMediaPlayer);
+ }
+
+ if (mSurfaceListener != null) {
+ Rect rect = mSurfaceHolder.getSurfaceFrame();
+ mSurfaceListener.onSurfaceCreated(this, rect.width(), rect.height());
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceChanged(this, width, height);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // After we return from this we can't use the surface any more
+ mSurfaceHolder = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceDestroyed(this);
+ }
+ }
+
+ // TODO: Investigate the way to move onMeasure() code into FrameLayout.
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+ int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + MeasureSpec.toString(heightMeasureSpec) + ")");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+ }
+
+ int width = getDefaultSize(videoWidth, widthMeasureSpec);
+ int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+ if (videoWidth > 0 && videoHeight > 0) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if (videoWidth * height < width * videoHeight) {
+ width = height * videoWidth / videoHeight;
+ if (DEBUG) {
+ Log.d(TAG, "image too wide, correcting. width: " + width);
+ }
+ } else if (videoWidth * height > width * videoHeight) {
+ height = width * videoHeight / videoWidth;
+ if (DEBUG) {
+ Log.d(TAG, "image too tall, correcting. height: " + height);
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ if (DEBUG) {
+ Log.i(TAG, "end of onMeasure()");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ViewType: SurfaceView / Visibility: " + getVisibility()
+ + " / surfaceHolder: " + mSurfaceHolder;
+ }
+}
diff --git a/androidx/widget/VideoTextureView.java b/androidx/widget/VideoTextureView.java
new file mode 100644
index 00000000..836fdc33
--- /dev/null
+++ b/androidx/widget/VideoTextureView.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import static androidx.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.media.MediaPlayer;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+@RequiresApi(21)
+class VideoTextureView extends TextureView
+ implements VideoViewInterface, TextureView.SurfaceTextureListener {
+ private static final String TAG = "VideoTextureView";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+
+ private Surface mSurface;
+ private SurfaceListener mSurfaceListener;
+ private MediaPlayer mMediaPlayer;
+ // A flag to indicate taking over other view should be proceed.
+ private boolean mIsTakingOverOldView;
+ private VideoViewInterface mOldView;
+
+ VideoTextureView(Context context) {
+ this(context, null);
+ }
+
+ VideoTextureView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ VideoTextureView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ setSurfaceTextureListener(this);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements VideoViewInterface
+ ////////////////////////////////////////////////////
+
+ @Override
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ if (mp == null || !hasAvailableSurface()) {
+ // Surface is not ready.
+ return false;
+ }
+ mp.setSurface(mSurface);
+ return true;
+ }
+
+ @Override
+ public void setSurfaceListener(SurfaceListener l) {
+ mSurfaceListener = l;
+ }
+
+ @Override
+ public int getViewType() {
+ return VIEW_TYPE_TEXTUREVIEW;
+ }
+
+ @Override
+ public void setMediaPlayer(MediaPlayer mp) {
+ mMediaPlayer = mp;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ }
+ }
+
+ @Override
+ public void takeOver(@NonNull VideoViewInterface oldView) {
+ if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+ ((View) oldView).setVisibility(GONE);
+ mIsTakingOverOldView = false;
+ mOldView = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceTakeOverDone(this);
+ }
+ } else {
+ mIsTakingOverOldView = true;
+ mOldView = oldView;
+ }
+ }
+
+ @Override
+ public boolean hasAvailableSurface() {
+ return mSurface != null && mSurface.isValid();
+ }
+
+ ////////////////////////////////////////////////////
+ // implements TextureView.SurfaceTextureListener
+ ////////////////////////////////////////////////////
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+ mSurface = new Surface(surfaceTexture);
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ } else {
+ assignSurfaceToMediaPlayer(mMediaPlayer);
+ }
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceCreated(this, width, height);
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceChanged(this, width, height);
+ }
+ // requestLayout(); // TODO: figure out if it should be called here?
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ // no-op
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceDestroyed(this);
+ }
+ mSurface = null;
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+ int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + MeasureSpec.toString(heightMeasureSpec) + ")");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+ }
+
+ int width = getDefaultSize(videoWidth, widthMeasureSpec);
+ int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+ if (videoWidth > 0 && videoHeight > 0) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if (videoWidth * height < width * videoHeight) {
+ width = height * videoWidth / videoHeight;
+ if (DEBUG) {
+ Log.d(TAG, "image too wide, correcting. width: " + width);
+ }
+ } else if (videoWidth * height > width * videoHeight) {
+ height = width * videoHeight / videoWidth;
+ if (DEBUG) {
+ Log.d(TAG, "image too tall, correcting. height: " + height);
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ if (DEBUG) {
+ Log.i(TAG, "end of onMeasure()");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ }
+ }
+}
diff --git a/androidx/widget/VideoView2.java b/androidx/widget/VideoView2.java
new file mode 100644
index 00000000..3cb17177
--- /dev/null
+++ b/androidx/widget/VideoView2.java
@@ -0,0 +1,1785 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaPlayer;
+import android.media.PlaybackParams;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.PlaybackInfo;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.VideoView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.media.DataSourceDesc;
+import androidx.media.MediaItem2;
+import androidx.media.MediaMetadata2;
+import androidx.media.R;
+import androidx.media.SessionToken2;
+import androidx.palette.graphics.Palette;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+// TODO: Replace MediaSession wtih MediaSession2 once MediaSession2 is submitted.
+/**
+ * @hide
+ * Displays a video file. VideoView2 class is a View class which is wrapping {@link MediaPlayer}
+ * so that developers can easily implement a video rendering application.
+ *
+ * <p>
+ * <em> Data sources that VideoView2 supports : </em>
+ * VideoView2 can play video files and audio-only files as
+ * well. It can load from various sources such as resources or content providers. The supported
+ * media file formats are the same as {@link MediaPlayer}.
+ *
+ * <p>
+ * <em> View type can be selected : </em>
+ * VideoView2 can render videos on top of TextureView as well as
+ * SurfaceView selectively. The default is SurfaceView and it can be changed using
+ * {@link #setViewType(int)} method. Using SurfaceView is recommended in most cases for saving
+ * battery. TextureView might be preferred for supporting various UIs such as animation and
+ * translucency.
+ *
+ * <p>
+ * <em> Differences between {@link VideoView} class : </em>
+ * VideoView2 covers and inherits the most of
+ * VideoView's functionalities. The main differences are
+ * <ul>
+ * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * selectively while VideoView inherits SurfaceView class.
+ * <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
+ * attached to VideoView2 by default. If a developer does not want to use the default
+ * MediaControlView2, needs to set enableControlView attribute to false. For instance,
+ * <pre>
+ * &lt;VideoView2
+ * android:id="@+id/video_view"
+ * xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
+ * widget:enableControlView="false" /&gt;
+ * </pre>
+ * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
+ * to false and assign the customed media control widget using {@link #setMediaControlView2}.
+ * <li> VideoView2 is integrated with MediaPlayer while VideoView is integrated with MediaPlayer.
+ * <li> VideoView2 is integrated with MediaSession and so it responses with media key events.
+ * A VideoView2 keeps a MediaSession instance internally and connects it to a corresponding
+ * MediaControlView2 instance.
+ * </p>
+ * </ul>
+ *
+ * <p>
+ * <em> Audio focus and audio attributes : </em>
+ * By default, VideoView2 requests audio focus with
+ * {@link AudioManager#AUDIOFOCUS_GAIN}. Use {@link #setAudioFocusRequest(int)} to change this
+ * behavior. The default {@link AudioAttributes} used during playback have a usage of
+ * {@link AudioAttributes#USAGE_MEDIA} and a content type of
+ * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
+ * modify them.
+ *
+ * <p>
+ * Note: VideoView2 does not retain its full state when going into the background. In particular, it
+ * does not restore the current play state, play position, selected tracks. Applications should save
+ * and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and
+ * {@link android.app.Activity#onRestoreInstanceState}.
+ */
+@RequiresApi(21) // TODO correct minSdk API use incompatibilities and remove before release.
+@RestrictTo(LIBRARY_GROUP)
+public class VideoView2 extends BaseLayout implements VideoViewInterface.SurfaceListener {
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({
+ VIEW_TYPE_TEXTUREVIEW,
+ VIEW_TYPE_SURFACEVIEW
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ViewType {}
+
+ /**
+ * Indicates video is rendering on SurfaceView.
+ *
+ * @see #setViewType
+ */
+ public static final int VIEW_TYPE_SURFACEVIEW = 0;
+
+ /**
+ * Indicates video is rendering on TextureView.
+ *
+ * @see #setViewType
+ */
+ public static final int VIEW_TYPE_TEXTUREVIEW = 1;
+
+ private static final String TAG = "VideoView2";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+ private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000;
+
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+ private static final int INVALID_TRACK_INDEX = -1;
+ private static final float INVALID_SPEED = 0f;
+
+ private static final int SIZE_TYPE_EMBEDDED = 0;
+ private static final int SIZE_TYPE_FULL = 1;
+ // TODO: add support for Minimal size type.
+ private static final int SIZE_TYPE_MINIMAL = 2;
+
+ private AccessibilityManager mAccessibilityManager;
+ private AudioManager mAudioManager;
+ private AudioAttributes mAudioAttributes;
+ private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+ private boolean mAudioFocused = false;
+
+ private Pair<Executor, VideoView2.OnCustomActionListener> mCustomActionListenerRecord;
+ private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener;
+ private VideoView2.OnFullScreenRequestListener mFullScreenRequestListener;
+
+ private VideoViewInterface mCurrentView;
+ private VideoTextureView mTextureView;
+ private VideoSurfaceView mSurfaceView;
+
+ private MediaPlayer mMediaPlayer;
+ private DataSourceDesc mDsd;
+ private MediaControlView2 mMediaControlView;
+ private MediaSessionCompat mMediaSession;
+ private MediaControllerCompat mMediaController;
+ private MediaMetadata2 mMediaMetadata;
+ private MediaMetadataRetriever mRetriever;
+ private boolean mNeedUpdateMediaType;
+ private Bundle mMediaTypeData;
+ private String mTitle;
+
+ // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface.
+ private WindowManager mManager;
+ private Resources mResources;
+ private View mMusicView;
+ private Drawable mMusicAlbumDrawable;
+ private String mMusicTitleText;
+ private String mMusicArtistText;
+ private boolean mIsMusicMediaType;
+ private int mPrevWidth;
+ private int mPrevHeight;
+ private int mDominantColor;
+ private int mSizeType;
+
+ private PlaybackStateCompat.Builder mStateBuilder;
+ private List<PlaybackStateCompat.CustomAction> mCustomActionList;
+
+ private int mTargetState = STATE_IDLE;
+ private int mCurrentState = STATE_IDLE;
+ private int mCurrentBufferPercentage;
+ private long mSeekWhenPrepared; // recording the seek position while preparing
+
+ private int mVideoWidth;
+ private int mVideoHeight;
+
+ private ArrayList<Integer> mVideoTrackIndices;
+ private ArrayList<Integer> mAudioTrackIndices;
+ // private ArrayList<Pair<Integer, SubtitleTrack>> mSubtitleTrackIndices;
+ // private SubtitleController mSubtitleController;
+
+ // selected video/audio/subtitle track index as MediaPlayer returns
+ private int mSelectedVideoTrackIndex;
+ private int mSelectedAudioTrackIndex;
+ private int mSelectedSubtitleTrackIndex;
+
+ // private SubtitleView mSubtitleView;
+ private boolean mSubtitleEnabled;
+
+ private float mSpeed;
+ // TODO: Remove mFallbackSpeed when integration with MediaPlayer's new setPlaybackParams().
+ // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
+ private float mFallbackSpeed; // keep the original speed before 'pause' is called.
+ private float mVolumeLevelFloat;
+ private int mVolumeLevel;
+
+ private long mShowControllerIntervalMs;
+
+ // private MediaRouter mMediaRouter;
+ // private MediaRouteSelector mRouteSelector;
+ // private MediaRouter.RouteInfo mRoute;
+ // private RoutePlayer mRoutePlayer;
+
+ // TODO (b/77158231)
+ /*
+ private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() {
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+ // Stop local playback (if necessary)
+ resetPlayer();
+ mRoute = route;
+ mRoutePlayer = new RoutePlayer(getContext(), route);
+ mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() {
+ @Override
+ public void onPlayerStateChanged(MediaItemStatus itemStatus) {
+ PlaybackStateCompat.Builder psBuilder = new PlaybackStateCompat.Builder();
+ psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS);
+ long position = itemStatus.getContentPosition();
+ switch (itemStatus.getPlaybackState()) {
+ case MediaItemStatus.PLAYBACK_STATE_PENDING:
+ psBuilder.setState(PlaybackStateCompat.STATE_NONE, position, 0);
+ mCurrentState = STATE_IDLE;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PLAYING:
+ psBuilder.setState(PlaybackStateCompat.STATE_PLAYING, position, 1);
+ mCurrentState = STATE_PLAYING;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_PAUSED:
+ psBuilder.setState(PlaybackStateCompat.STATE_PAUSED, position, 0);
+ mCurrentState = STATE_PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_BUFFERING:
+ psBuilder.setState(
+ PlaybackStateCompat.STATE_BUFFERING, position, 0);
+ mCurrentState = STATE_PAUSED;
+ break;
+ case MediaItemStatus.PLAYBACK_STATE_FINISHED:
+ psBuilder.setState(PlaybackStateCompat.STATE_STOPPED, position, 0);
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ break;
+ }
+
+ PlaybackStateCompat pbState = psBuilder.build();
+ mMediaSession.setPlaybackState(pbState);
+
+ MediaMetadataCompat.Builder mmBuilder = new MediaMetadataCompat.Builder();
+ mmBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION,
+ itemStatus.getContentDuration());
+ mMediaSession.setMetadata(mmBuilder.build());
+ }
+ });
+ // Start remote playback (if necessary)
+ mRoutePlayer.openVideo(mDsd);
+ }
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) {
+ if (mRoute != null && mRoutePlayer != null) {
+ mRoutePlayer.release();
+ mRoutePlayer = null;
+ }
+ if (mRoute == route) {
+ mRoute = null;
+ }
+ if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ // TODO: Resume local playback (if necessary)
+ openVideo(mDsd);
+ }
+ }
+ };
+ */
+
+ public VideoView2(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mSpeed = 1.0f;
+ mFallbackSpeed = mSpeed;
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+ // TODO: add attributes to get this value.
+ mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS;
+
+ mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ requestFocus();
+
+ // TODO: try to keep a single child at a time rather than always having both.
+ mTextureView = new VideoTextureView(getContext());
+ mSurfaceView = new VideoSurfaceView(getContext());
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mTextureView.setLayoutParams(params);
+ mSurfaceView.setLayoutParams(params);
+ mTextureView.setSurfaceListener(this);
+ mSurfaceView.setSurfaceListener(this);
+
+ addView(mTextureView);
+ addView(mSurfaceView);
+
+ // mSubtitleView = new SubtitleView(getContext());
+ // mSubtitleView.setLayoutParams(params);
+ // mSubtitleView.setBackgroundColor(0);
+ // addView(mSubtitleView);
+
+ boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/res/android",
+ "enableControlView", true);
+ if (enableControlView) {
+ mMediaControlView = new MediaControlView2(getContext());
+ }
+
+ mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/res/android",
+ "enableSubtitle", false);
+
+ // TODO: Choose TextureView when SurfaceView cannot be created.
+ // Choose surface view by default
+ int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW
+ : attrs.getAttributeIntValue(
+ "http://schemas.android.com/apk/res/android",
+ "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW);
+ if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "viewType attribute is surfaceView.");
+ mTextureView.setVisibility(View.GONE);
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mCurrentView = mSurfaceView;
+ } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "viewType attribute is textureView.");
+ mTextureView.setVisibility(View.VISIBLE);
+ mSurfaceView.setVisibility(View.GONE);
+ mCurrentView = mTextureView;
+ }
+
+ // TODO (b/77158231)
+ /*
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ mRouteSelector = builder.build();
+ */
+ }
+
+ /**
+ * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+ * instance if any.
+ *
+ * @param mediaControlView a media control view2 instance.
+ * @param intervalMs a time interval in milliseconds until VideoView2 hides MediaControlView2.
+ */
+ public void setMediaControlView2(MediaControlView2 mediaControlView, long intervalMs) {
+ mMediaControlView = mediaControlView;
+ mShowControllerIntervalMs = intervalMs;
+ // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe.
+ // TODO (b/77158231)
+ // mMediaControlView.setRouteSelector(mRouteSelector);
+
+ if (isAttachedToWindow()) {
+ attachMediaControlView();
+ }
+ }
+
+ /**
+ * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+ * {@link #setMediaControlView2} method.
+ */
+ public MediaControlView2 getMediaControlView2() {
+ return mMediaControlView;
+ }
+
+ /**
+ * Sets MediaMetadata2 instance. It will replace the previously assigned MediaMetadata2 instance
+ * if any.
+ *
+ * @param metadata a MediaMetadata2 instance.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setMediaMetadata(MediaMetadata2 metadata) {
+ //mProvider.setMediaMetadata_impl(metadata);
+ }
+
+ /**
+ * Returns MediaMetadata2 instance which is retrieved from MediaPlayer inside VideoView2 by
+ * default or by {@link #setMediaMetadata} method.
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public MediaMetadata2 getMediaMetadata() {
+ return mMediaMetadata;
+ }
+
+ /**
+ * Returns MediaController instance which is connected with MediaSession that VideoView2 is
+ * using. This method should be called when VideoView2 is attached to window, or it throws
+ * IllegalStateException, since internal MediaSession instance is not available until
+ * this view is attached to window. Please check {@link android.view.View#isAttachedToWindow}
+ * before calling this method.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ * @hide TODO: remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public MediaControllerCompat getMediaController() {
+ if (mMediaSession == null) {
+ throw new IllegalStateException("MediaSession instance is not available.");
+ }
+ return mMediaController;
+ }
+
+ /**
+ * Returns {@link androidx.media.SessionToken2} so that developers create their own
+ * {@link androidx.media.MediaController2} instance. This method should be called when
+ * VideoView2 is attached to window, or it throws IllegalStateException.
+ *
+ * @throws IllegalStateException if interal MediaSession is not created yet.
+ * @hide
+ */
+ public SessionToken2 getMediaSessionToken() {
+ //return mProvider.getMediaSessionToken_impl();
+ return null;
+ }
+
+ /**
+ * Shows or hides closed caption or subtitles if there is any.
+ * The first subtitle track will be chosen if there multiple subtitle tracks exist.
+ * Default behavior of VideoView2 is not showing subtitle.
+ * @param enable shows closed caption or subtitles if this value is true, or hides.
+ */
+ public void setSubtitleEnabled(boolean enable) {
+ if (enable != mSubtitleEnabled) {
+ selectOrDeselectSubtitle(enable);
+ }
+ mSubtitleEnabled = enable;
+ }
+
+ /**
+ * Returns true if showing subtitle feature is enabled or returns false.
+ * Although there is no subtitle track or closed caption, it can return true, if the feature
+ * has been enabled by {@link #setSubtitleEnabled}.
+ */
+ public boolean isSubtitleEnabled() {
+ return mSubtitleEnabled;
+ }
+
+ /**
+ * Sets playback speed.
+ *
+ * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+ * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+ * maximum speed that internal engine supports, system will determine best handling or it will
+ * be reset to the normal speed 1.0f.
+ * @param speed the playback speed. It should be positive.
+ */
+ // TODO: Support this via MediaController2.
+ public void setSpeed(float speed) {
+ if (speed <= 0.0f) {
+ Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+ return;
+ }
+ mSpeed = speed;
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ applySpeed();
+ }
+ updatePlaybackState();
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ public void setAudioFocusRequest(int focusGain) {
+ if (focusGain != AudioManager.AUDIOFOCUS_NONE
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+ throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+ }
+ mAudioFocusType = focusGain;
+ }
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes");
+ }
+ mAudioAttributes = attributes;
+ }
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ *
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setVideoPath(String path) {
+ setVideoUri(Uri.parse(path));
+ }
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ *
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setVideoUri(Uri uri) {
+ setVideoUri(uri, null);
+ }
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ *
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setVideoUri(Uri uri, Map<String, String> headers) {
+ mSeekWhenPrepared = 0;
+ openVideo(uri, headers);
+ }
+
+ /**
+ * Sets {@link MediaItem2} object to render using VideoView2. Alternative way to set media
+ * object to VideoView2 is {@link #setDataSource}.
+ * @param mediaItem the MediaItem2 to play
+ * @see #setDataSource
+ */
+ public void setMediaItem(@NonNull MediaItem2 mediaItem) {
+ //mProvider.setMediaItem_impl(mediaItem);
+ }
+
+ /**
+ * Sets {@link DataSourceDesc} object to render using VideoView2.
+ * @param dataSource the {@link DataSourceDesc} object to play.
+ * @see #setMediaItem
+ * @hide
+ */
+ public void setDataSource(@NonNull DataSourceDesc dataSource) {
+ //mProvider.setDataSource_impl(dataSource);
+ }
+
+ /**
+ * Selects which view will be used to render video between SurfacView and TextureView.
+ *
+ * @param viewType the view type to render video
+ * <ul>
+ * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ public void setViewType(@ViewType int viewType) {
+ if (viewType == mCurrentView.getViewType()) {
+ return;
+ }
+ VideoViewInterface targetView;
+ if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "switching to TextureView");
+ targetView = mTextureView;
+ } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "switching to SurfaceView");
+ targetView = mSurfaceView;
+ } else {
+ throw new IllegalArgumentException("Unknown view type: " + viewType);
+ }
+ ((View) targetView).setVisibility(View.VISIBLE);
+ targetView.takeOver(mCurrentView);
+ requestLayout();
+ }
+
+ /**
+ * Returns view type.
+ *
+ * @return view type. See {@see setViewType}.
+ */
+ @ViewType
+ public int getViewType() {
+ return mCurrentView.getViewType();
+ }
+
+ /**
+ * Sets custom actions which will be shown as custom buttons in {@link MediaControlView2}.
+ *
+ * @param actionList A list of {@link PlaybackStateCompat.CustomAction}. The return value of
+ * {@link PlaybackStateCompat.CustomAction#getIcon()} will be used to draw
+ * buttons in {@link MediaControlView2}.
+ * @param executor executor to run callbacks on.
+ * @param listener A listener to be called when a custom button is clicked.
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setCustomActions(List<PlaybackStateCompat.CustomAction> actionList,
+ Executor executor, OnCustomActionListener listener) {
+ mCustomActionList = actionList;
+ mCustomActionListenerRecord = new Pair<>(executor, listener);
+
+ // Create a new playback builder in order to clear existing the custom actions.
+ mStateBuilder = null;
+ updatePlaybackState();
+ }
+
+ /**
+ * Registers a callback to be invoked when a view type change is done.
+ * {@see #setViewType(int)}
+ * @param l The callback that will be run
+ * @hide
+ */
+ @VisibleForTesting
+ @RestrictTo(LIBRARY_GROUP)
+ public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) {
+ mViewTypeChangedListener = l;
+ }
+
+ /**
+ * Registers a callback to be invoked when the fullscreen mode should be changed.
+ * @param l The callback that will be run
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public void setFullScreenRequestListener(OnFullScreenRequestListener l) {
+ mFullScreenRequestListener = l;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ // Create MediaSession
+ mMediaSession = new MediaSessionCompat(getContext(), "VideoView2MediaSession");
+ mMediaSession.setCallback(new MediaSessionCallback());
+ mMediaSession.setActive(true);
+ mMediaController = mMediaSession.getController();
+ // TODO (b/77158231)
+ // mMediaRouter = MediaRouter.getInstance(getContext());
+ // mMediaRouter.setMediaSession(mMediaSession);
+ // mMediaRouter.addCallback(mRouteSelector, mRouterCallback);
+ attachMediaControlView();
+ // TODO: remove this after moving MediaSession creating code inside initializing VideoView2
+ if (mCurrentState == STATE_PREPARED) {
+ extractTracks();
+ extractMetadata();
+ extractAudioMetadata();
+ if (mNeedUpdateMediaType) {
+ mMediaSession.sendSessionEvent(
+ MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS,
+ mMediaTypeData);
+ mNeedUpdateMediaType = false;
+ }
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ mMediaSession.release();
+ mMediaSession = null;
+ mMediaController = null;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return VideoView2.class.getName();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (DEBUG) {
+ Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+ if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+ toggleMediaControlViewVisibility();
+ }
+ }
+
+ return super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) {
+ if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) {
+ toggleMediaControlViewVisibility();
+ }
+ }
+
+ return super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ // TODO: Test touch event handling logic thoroughly and simplify the logic.
+ return super.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mIsMusicMediaType) {
+ if (mPrevWidth != getMeasuredWidth()
+ || mPrevHeight != getMeasuredHeight()) {
+ int currWidth = getMeasuredWidth();
+ int currHeight = getMeasuredHeight();
+ Point screenSize = new Point();
+ mManager.getDefaultDisplay().getSize(screenSize);
+ int screenWidth = screenSize.x;
+ int screenHeight = screenSize.y;
+
+ if (currWidth == screenWidth && currHeight == screenHeight) {
+ int orientation = retrieveOrientation();
+ if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+ inflateMusicView(R.layout.full_landscape_music);
+ } else {
+ inflateMusicView(R.layout.full_portrait_music);
+ }
+
+ if (mSizeType != SIZE_TYPE_FULL) {
+ mSizeType = SIZE_TYPE_FULL;
+ // Remove existing mFadeOut callback
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mSizeType != SIZE_TYPE_EMBEDDED) {
+ mSizeType = SIZE_TYPE_EMBEDDED;
+ inflateMusicView(R.layout.embedded_music);
+ // Add new mFadeOut callback
+ mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+ }
+ }
+ mPrevWidth = currWidth;
+ mPrevHeight = currHeight;
+ }
+ }
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when the view type has been changed.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @RestrictTo(LIBRARY_GROUP)
+ public interface OnViewTypeChangedListener {
+ /**
+ * Called when the view type has been changed.
+ * @see #setViewType(int)
+ * @param view the View whose view type is changed
+ * @param viewType
+ * <ul>
+ * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ void onViewTypeChanged(View view, @ViewType int viewType);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
+ * Application should handle the fullscreen mode accordingly.
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public interface OnFullScreenRequestListener {
+ /**
+ * Called to indicate a fullscreen mode change.
+ */
+ void onFullScreenRequest(View view, boolean fullScreen);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to inform that a custom action is performed.
+ * @hide TODO remove
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public interface OnCustomActionListener {
+ /**
+ * Called to indicate that a custom action is performed.
+ *
+ * @param action The action that was originally sent in the
+ * {@link PlaybackStateCompat.CustomAction}.
+ * @param extras Optional extras.
+ */
+ void onCustomAction(String action, Bundle extras);
+ }
+
+ ///////////////////////////////////////////////////
+ // Implements VideoViewInterface.SurfaceListener
+ ///////////////////////////////////////////////////
+
+ @Override
+ public void onSurfaceCreated(View view, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+
+ @Override
+ public void onSurfaceDestroyed(View view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", " + view.toString());
+ }
+ }
+
+ @Override
+ public void onSurfaceChanged(View view, int width, int height) {
+ // TODO: Do we need to call requestLayout here?
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ }
+
+ @Override
+ public void onSurfaceTakeOverDone(VideoViewInterface view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+ }
+ mCurrentView = view;
+ if (mViewTypeChangedListener != null) {
+ mViewTypeChangedListener.onViewTypeChanged(this, view.getViewType());
+ }
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private void attachMediaControlView() {
+ // Get MediaController from MediaSession and set it inside MediaControlView
+ mMediaControlView.setController(mMediaSession.getController());
+
+ LayoutParams params =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ addView(mMediaControlView, params);
+ }
+
+ private boolean isInPlaybackState() {
+ // TODO (b/77158231)
+ // return (mMediaPlayer != null || mRoutePlayer != null)
+ return (mMediaPlayer != null)
+ && mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING;
+ }
+
+ private boolean needToStart() {
+ // TODO (b/77158231)
+ // return (mMediaPlayer != null || mRoutePlayer != null)
+ return (mMediaPlayer != null)
+ && isAudioGranted()
+ && isWaitingPlayback();
+ }
+
+ private boolean isWaitingPlayback() {
+ return mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING;
+ }
+
+ private boolean isAudioGranted() {
+ return mAudioFocused || mAudioFocusType == AudioManager.AUDIOFOCUS_NONE;
+ }
+
+ AudioManager.OnAudioFocusChangeListener mAudioFocusListener =
+ new AudioManager.OnAudioFocusChangeListener() {
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ mAudioFocused = true;
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ // There is no way to distinguish pause() by transient
+ // audio focus loss and by other explicit actions.
+ // TODO: If we can distinguish those cases, change the code to resume when it
+ // gains audio focus again for AUDIOFOCUS_LOSS_TRANSIENT and
+ // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
+ mAudioFocused = false;
+ if (isInPlaybackState() && mMediaPlayer.isPlaying()) {
+ mMediaController.getTransportControls().pause();
+ } else {
+ mTargetState = STATE_PAUSED;
+ }
+ }
+ }
+ };
+
+ private void requestAudioFocus(int focusType) {
+ int result;
+ if (android.os.Build.VERSION.SDK_INT >= 26) {
+ AudioFocusRequest focusRequest;
+ focusRequest = new AudioFocusRequest.Builder(focusType)
+ .setAudioAttributes(mAudioAttributes)
+ .setOnAudioFocusChangeListener(mAudioFocusListener)
+ .build();
+ result = mAudioManager.requestAudioFocus(focusRequest);
+ } else {
+ result = mAudioManager.requestAudioFocus(mAudioFocusListener,
+ AudioManager.STREAM_MUSIC,
+ focusType);
+ }
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
+ mAudioFocused = false;
+ } else if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mAudioFocused = true;
+ } else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
+ mAudioFocused = false;
+ }
+ }
+
+ // Creates a MediaPlayer instance and prepare playback.
+ private void openVideo(Uri uri, Map<String, String> headers) {
+ resetPlayer();
+ if (isRemotePlayback()) {
+ // TODO (b/77158231)
+ // mRoutePlayer.openVideo(dsd);
+ return;
+ }
+
+ try {
+ Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+ mMediaPlayer = new MediaPlayer();
+ mSurfaceView.setMediaPlayer(mMediaPlayer);
+ mTextureView.setMediaPlayer(mMediaPlayer);
+ mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+ final Context context = getContext();
+ // TODO: Add timely firing logic for more accurate sync between CC and video frame
+ // mSubtitleController = new SubtitleController(context);
+ // mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context));
+ // mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView);
+
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnInfoListener(mInfoListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+
+ mCurrentBufferPercentage = -1;
+ mMediaPlayer.setDataSource(getContext(), uri, headers);
+ mMediaPlayer.setAudioAttributes(mAudioAttributes);
+ // mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener);
+ // we don't set the target state here either, but preserve the
+ // target state that was there before.
+ mCurrentState = STATE_PREPARING;
+ mMediaPlayer.prepareAsync();
+
+ // Save file name as title since the file may not have a title Metadata.
+ mTitle = uri.getPath();
+ String scheme = uri.getScheme();
+ if (scheme != null && scheme.equals("file")) {
+ mTitle = uri.getLastPathSegment();
+ }
+ mRetriever = new MediaMetadataRetriever();
+ mRetriever.setDataSource(getContext(), uri);
+
+ if (DEBUG) {
+ Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ } catch (IOException | IllegalArgumentException ex) {
+ Log.w(TAG, "Unable to open content: " + uri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer,
+ MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
+ }
+ }
+
+ /*
+ * Reset the media player in any state
+ */
+ private void resetPlayer() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mTextureView.setMediaPlayer(null);
+ mSurfaceView.setMediaPlayer(null);
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
+
+ private void updatePlaybackState() {
+ if (mStateBuilder == null) {
+ /*
+ // Get the capabilities of the player for this stream
+ mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
+ MediaPlayer.BYPASS_METADATA_FILTER);
+
+ // Add Play action as default
+ long playbackActions = PlaybackStateCompat.ACTION_PLAY;
+ if (mMetadata != null) {
+ if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
+ playbackActions |= PlaybackStateCompat.ACTION_PAUSE;
+ }
+ if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
+ playbackActions |= PlaybackStateCompat.ACTION_REWIND;
+ }
+ if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
+ playbackActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
+ }
+ if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
+ playbackActions |= PlaybackStateCompat.ACTION_SEEK_TO;
+ }
+ } else {
+ playbackActions |= (PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_REWIND
+ | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SEEK_TO);
+ }
+ */
+ // TODO determine the actionable list based the metadata info.
+ long playbackActions = PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_PAUSE
+ | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD
+ | PlaybackStateCompat.ACTION_SEEK_TO;
+ mStateBuilder = new PlaybackStateCompat.Builder();
+ mStateBuilder.setActions(playbackActions);
+
+ if (mCustomActionList != null) {
+ for (PlaybackStateCompat.CustomAction action : mCustomActionList) {
+ mStateBuilder.addCustomAction(action);
+ }
+ }
+ }
+ mStateBuilder.setState(getCorrespondingPlaybackState(),
+ mMediaPlayer.getCurrentPosition(), mSpeed);
+ if (mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING) {
+ // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is
+ // implemented.
+ if (mCurrentBufferPercentage == -1) {
+ mStateBuilder.setBufferedPosition(-1);
+ } else {
+ mStateBuilder.setBufferedPosition(
+ (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration()));
+ }
+ }
+
+ // Set PlaybackState for MediaSession
+ if (mMediaSession != null) {
+ PlaybackStateCompat state = mStateBuilder.build();
+ mMediaSession.setPlaybackState(state);
+ }
+ }
+
+ private int getCorrespondingPlaybackState() {
+ switch (mCurrentState) {
+ case STATE_ERROR:
+ return PlaybackStateCompat.STATE_ERROR;
+ case STATE_IDLE:
+ return PlaybackStateCompat.STATE_NONE;
+ case STATE_PREPARING:
+ return PlaybackStateCompat.STATE_CONNECTING;
+ case STATE_PREPARED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case STATE_PLAYING:
+ return PlaybackStateCompat.STATE_PLAYING;
+ case STATE_PAUSED:
+ return PlaybackStateCompat.STATE_PAUSED;
+ case STATE_PLAYBACK_COMPLETED:
+ return PlaybackStateCompat.STATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ private final Runnable mFadeOut = new Runnable() {
+ @Override
+ public void run() {
+ if (mCurrentState == STATE_PLAYING) {
+ mMediaControlView.setVisibility(View.GONE);
+ }
+ }
+ };
+
+ private void showController() {
+ // TODO: Decide what to show when the state is not in playback state
+ if (mMediaControlView == null || !isInPlaybackState()
+ || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) {
+ return;
+ }
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.VISIBLE);
+ if (mShowControllerIntervalMs != 0
+ && !mAccessibilityManager.isTouchExplorationEnabled()) {
+ mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs);
+ }
+ }
+
+ private void toggleMediaControlViewVisibility() {
+ if (mMediaControlView.getVisibility() == View.VISIBLE) {
+ mMediaControlView.removeCallbacks(mFadeOut);
+ mMediaControlView.setVisibility(View.GONE);
+ } else {
+ showController();
+ }
+ }
+
+ private void applySpeed() {
+ if (android.os.Build.VERSION.SDK_INT < 23) {
+ // TODO: MediaPlayer2 will cover this, or implement with SoundPool.
+ return;
+ }
+ PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+ if (mSpeed != params.getSpeed()) {
+ try {
+ params.setSpeed(mSpeed);
+ mMediaPlayer.setPlaybackParams(params);
+ mFallbackSpeed = mSpeed;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+ // TODO: should revise this part after integrating with MP2.
+ // If mSpeed had an illegal value for speed rate, system will determine best
+ // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
+ // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
+ // use mFallbackSpeed instead.
+ float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+ if (fallbackSpeed > 0.0f) {
+ mFallbackSpeed = fallbackSpeed;
+ }
+ mSpeed = mFallbackSpeed;
+ }
+ }
+ }
+
+ private boolean isRemotePlayback() {
+ if (mMediaController == null) {
+ return false;
+ }
+ PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
+ return playbackInfo != null
+ && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
+ }
+
+ private void selectOrDeselectSubtitle(boolean select) {
+ if (!isInPlaybackState()) {
+ return;
+ }
+ /*
+ if (select) {
+ if (mSubtitleTrackIndices.size() > 0) {
+ // TODO: make this selection dynamic
+ mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first;
+ mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second);
+ mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex);
+ mSubtitleView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) {
+ mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex);
+ mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX;
+ mSubtitleView.setVisibility(View.GONE);
+ }
+ }
+ */
+ }
+
+ private void extractTracks() {
+ MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+ mVideoTrackIndices = new ArrayList<>();
+ mAudioTrackIndices = new ArrayList<>();
+ /*
+ mSubtitleTrackIndices = new ArrayList<>();
+ mSubtitleController.reset();
+ */
+ for (int i = 0; i < trackInfos.length; ++i) {
+ int trackType = trackInfos[i].getTrackType();
+ if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
+ mVideoTrackIndices.add(i);
+ } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) {
+ mAudioTrackIndices.add(i);
+ /*
+ } else if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
+ || trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
+ SubtitleTrack track = mSubtitleController.addTrack(trackInfos[i].getFormat());
+ if (track != null) {
+ mSubtitleTrackIndices.add(new Pair<>(i, track));
+ }
+ */
+ }
+ }
+ // Select first tracks as default
+ if (mVideoTrackIndices.size() > 0) {
+ mSelectedVideoTrackIndex = 0;
+ }
+ if (mAudioTrackIndices.size() > 0) {
+ mSelectedAudioTrackIndex = 0;
+ }
+ if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) {
+ mIsMusicMediaType = true;
+ }
+
+ Bundle data = new Bundle();
+ data.putInt(MediaControlView2.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size());
+ data.putInt(MediaControlView2.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size());
+ /*
+ data.putInt(MediaControlView2.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size());
+ if (mSubtitleTrackIndices.size() > 0) {
+ selectOrDeselectSubtitle(mSubtitleEnabled);
+ }
+ */
+ mMediaSession.sendSessionEvent(MediaControlView2.EVENT_UPDATE_TRACK_STATUS, data);
+ }
+
+ private void extractMetadata() {
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (title != null) {
+ mTitle = title;
+ }
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(
+ MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+ }
+ }
+
+ private void extractAudioMetadata() {
+ if (!mIsMusicMediaType) {
+ return;
+ }
+
+ mResources = getResources();
+ mManager = (WindowManager) getContext().getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+
+ byte[] album = mRetriever.getEmbeddedPicture();
+ if (album != null) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length);
+ mMusicAlbumDrawable = new BitmapDrawable(bitmap);
+
+ // TODO: replace with visualizer
+ Palette.Builder builder = Palette.from(bitmap);
+ builder.generate(new Palette.PaletteAsyncListener() {
+ @Override
+ public void onGenerated(Palette palette) {
+ // TODO: add dominant color for default album image.
+ mDominantColor = palette.getDominantColor(0);
+ if (mMusicView != null) {
+ mMusicView.setBackgroundColor(mDominantColor);
+ }
+ }
+ });
+ } else {
+ mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image);
+ }
+
+ String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (title != null) {
+ mMusicTitleText = title;
+ } else {
+ mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text);
+ }
+
+ String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
+ if (artist != null) {
+ mMusicArtistText = artist;
+ } else {
+ mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text);
+ }
+
+ // Send title and artist string to MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mMusicTitleText);
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mMusicArtistText);
+ mMediaSession.setMetadata(builder.build());
+
+ // Display Embedded mode as default
+ removeView(mSurfaceView);
+ removeView(mTextureView);
+ inflateMusicView(R.layout.embedded_music);
+ }
+
+ private int retrieveOrientation() {
+ DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
+ int width = dm.widthPixels;
+ int height = dm.heightPixels;
+
+ return (height > width)
+ ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ }
+
+ private void inflateMusicView(int layoutId) {
+ removeView(mMusicView);
+
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflater.inflate(layoutId, null);
+ v.setBackgroundColor(mDominantColor);
+
+ ImageView albumView = v.findViewById(R.id.album);
+ if (albumView != null) {
+ albumView.setImageDrawable(mMusicAlbumDrawable);
+ }
+
+ TextView titleView = v.findViewById(R.id.title);
+ if (titleView != null) {
+ titleView.setText(mMusicTitleText);
+ }
+
+ TextView artistView = v.findViewById(R.id.artist);
+ if (artistView != null) {
+ artistView.setText(mMusicArtistText);
+ }
+
+ mMusicView = v;
+ addView(mMusicView, 0);
+ }
+
+ /*
+ OnSubtitleDataListener mSubtitleListener =
+ new OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer mp, SubtitleData data) {
+ if (DEBUG) {
+ Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+ + ", getCurrentPosition: " + mp.getCurrentPosition()
+ + ", getStartTimeUs(): " + data.getStartTimeUs()
+ + ", diff: "
+ + (data.getStartTimeUs() / 1000 - mp.getCurrentPosition())
+ + "ms, getDurationUs(): " + data.getDurationUs());
+
+ }
+ final int index = data.getTrackIndex();
+ if (index != mSelectedSubtitleTrackIndex) {
+ Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex()
+ + ", selected track index: " + mSelectedSubtitleTrackIndex);
+ return;
+ }
+ for (Pair<Integer, SubtitleTrack> p : mSubtitleTrackIndices) {
+ if (p.first == index) {
+ SubtitleTrack track = p.second;
+ track.onData(data);
+ }
+ }
+ }
+ };
+ */
+
+ MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+ new MediaPlayer.OnVideoSizeChangedListener() {
+ @Override
+ public void onVideoSizeChanged(
+ MediaPlayer mp, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+ + mVideoHeight);
+ }
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ requestLayout();
+ }
+ }
+ };
+ MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ if (DEBUG) {
+ Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ mCurrentState = STATE_PREPARED;
+ // Create and set playback state for MediaControlView2
+ updatePlaybackState();
+
+ // TODO: change this to send TrackInfos to MediaControlView2
+ // TODO: create MediaSession when initializing VideoView2
+ if (mMediaSession != null) {
+ extractTracks();
+ }
+
+ if (mMediaControlView != null) {
+ mMediaControlView.setEnabled(true);
+ }
+ int videoWidth = mp.getVideoWidth();
+ int videoHeight = mp.getVideoHeight();
+
+ // mSeekWhenPrepared may be changed after seekTo() call
+ long seekToPosition = mSeekWhenPrepared;
+ if (seekToPosition != 0) {
+ mMediaController.getTransportControls().seekTo(seekToPosition);
+ }
+
+ if (videoWidth != 0 && videoHeight != 0) {
+ if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+ if (DEBUG) {
+ Log.i(TAG, "OnPreparedListener() : ");
+ Log.i(TAG, " video size: " + videoWidth + "/" + videoHeight);
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/"
+ + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ }
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+ requestLayout();
+ }
+
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ } else {
+ // We don't know the video size yet, but should start anyway.
+ // The video size might be reported to us later.
+ if (needToStart()) {
+ mMediaController.getTransportControls().play();
+ }
+ }
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+
+ // TODO: Get title via other public APIs.
+ /*
+ if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
+ mTitle = mMetadata.getString(Metadata.TITLE);
+ }
+ */
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mMediaPlayer.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+
+ // TODO: merge this code with the above code when integrating with
+ // MediaSession2.
+ if (mNeedUpdateMediaType) {
+ mMediaSession.sendSessionEvent(
+ MediaControlView2.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData);
+ mNeedUpdateMediaType = false;
+ }
+ }
+ }
+ };
+
+ MediaPlayer.OnSeekCompleteListener mSeekCompleteListener =
+ new MediaPlayer.OnSeekCompleteListener() {
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ updatePlaybackState();
+ }
+ };
+
+ MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ updatePlaybackState();
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ };
+
+ MediaPlayer.OnInfoListener mInfoListener = new MediaPlayer.OnInfoListener() {
+ @Override
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) {
+ extractTracks();
+ }
+ return true;
+ }
+ };
+
+ MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
+ if (DEBUG) {
+ Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+ }
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ updatePlaybackState();
+
+ if (mMediaControlView != null) {
+ mMediaControlView.setVisibility(View.GONE);
+ }
+ return true;
+ }
+ };
+
+ MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+ new MediaPlayer.OnBufferingUpdateListener() {
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mCurrentBufferPercentage = percent;
+ updatePlaybackState();
+ }
+ };
+
+ private class MediaSessionCallback extends MediaSessionCompat.Callback {
+ @Override
+ public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+ if (isRemotePlayback()) {
+ // TODO (b/77158231)
+ // mRoutePlayer.onCommand(command, args, receiver);
+ } else {
+ switch (command) {
+ case MediaControlView2.COMMAND_SHOW_SUBTITLE:
+ /*
+ int subtitleIndex = args.getInt(
+ MediaControlView2.KEY_SELECTED_SUBTITLE_INDEX,
+ INVALID_TRACK_INDEX);
+ if (subtitleIndex != INVALID_TRACK_INDEX) {
+ int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first;
+ if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) {
+ mSelectedSubtitleTrackIndex = subtitleTrackIndex;
+ setSubtitleEnabled(true);
+ }
+ }
+ */
+ break;
+ case MediaControlView2.COMMAND_HIDE_SUBTITLE:
+ setSubtitleEnabled(false);
+ break;
+ case MediaControlView2.COMMAND_SET_FULLSCREEN:
+ if (mFullScreenRequestListener != null) {
+ mFullScreenRequestListener.onFullScreenRequest(
+ VideoView2.this,
+ args.getBoolean(MediaControlView2.ARGUMENT_KEY_FULLSCREEN));
+ }
+ break;
+ case MediaControlView2.COMMAND_SELECT_AUDIO_TRACK:
+ int audioIndex = args.getInt(MediaControlView2.KEY_SELECTED_AUDIO_INDEX,
+ INVALID_TRACK_INDEX);
+ if (audioIndex != INVALID_TRACK_INDEX) {
+ int audioTrackIndex = mAudioTrackIndices.get(audioIndex);
+ if (audioTrackIndex != mSelectedAudioTrackIndex) {
+ mSelectedAudioTrackIndex = audioTrackIndex;
+ mMediaPlayer.selectTrack(mSelectedAudioTrackIndex);
+ }
+ }
+ break;
+ case MediaControlView2.COMMAND_SET_PLAYBACK_SPEED:
+ float speed = args.getFloat(
+ MediaControlView2.KEY_PLAYBACK_SPEED, INVALID_SPEED);
+ if (speed != INVALID_SPEED && speed != mSpeed) {
+ setSpeed(speed);
+ mSpeed = speed;
+ }
+ break;
+ case MediaControlView2.COMMAND_MUTE:
+ mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0);
+ break;
+ case MediaControlView2.COMMAND_UNMUTE:
+ mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0);
+ break;
+ }
+ }
+ showController();
+ }
+
+ @Override
+ public void onCustomAction(final String action, final Bundle extras) {
+ mCustomActionListenerRecord.first.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCustomActionListenerRecord.second.onCustomAction(action, extras);
+ }
+ });
+ showController();
+ }
+
+ @Override
+ public void onPlay() {
+ if (!isAudioGranted()) {
+ requestAudioFocus(mAudioFocusType);
+ }
+
+ if ((isInPlaybackState() && mCurrentView.hasAvailableSurface()) || mIsMusicMediaType) {
+ if (isRemotePlayback()) {
+ // TODO (b/77158231)
+ // mRoutePlayer.onPlay();
+ } else {
+ applySpeed();
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ updatePlaybackState();
+ }
+ mCurrentState = STATE_PLAYING;
+ }
+ mTargetState = STATE_PLAYING;
+ if (DEBUG) {
+ Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ showController();
+ }
+
+ @Override
+ public void onPause() {
+ if (isInPlaybackState()) {
+ if (isRemotePlayback()) {
+ // TODO (b/77158231)
+ // mRoutePlayer.onPause();
+ mCurrentState = STATE_PAUSED;
+ } else if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ updatePlaybackState();
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ if (DEBUG) {
+ Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ showController();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ if (isInPlaybackState()) {
+ if (isRemotePlayback()) {
+ // TODO (b/77158231)
+ // mRoutePlayer.onSeekTo(pos);
+ } else {
+ // TODO Refactor VideoView2 with FooImplBase and FooImplApiXX.
+ if (android.os.Build.VERSION.SDK_INT < 26) {
+ mMediaPlayer.seekTo((int) pos);
+ } else {
+ mMediaPlayer.seekTo(pos, MediaPlayer.SEEK_PREVIOUS_SYNC);
+ }
+ mSeekWhenPrepared = 0;
+ }
+ } else {
+ mSeekWhenPrepared = pos;
+ }
+ showController();
+ }
+
+ @Override
+ public void onStop() {
+ if (isRemotePlayback()) {
+ // TODO (b/77158231)
+ // mRoutePlayer.onStop();
+ } else {
+ resetPlayer();
+ }
+ showController();
+ }
+ }
+}
diff --git a/androidx/widget/VideoView2Test.java b/androidx/widget/VideoView2Test.java
new file mode 100644
index 00000000..7f3dcfc7
--- /dev/null
+++ b/androidx/widget/VideoView2Test.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import static android.content.Context.KEYGUARD_SERVICE;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.os.Build;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.media.AudioAttributesCompat;
+import androidx.media.test.R;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * Test {@link VideoView2}.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // TODO: KITKAT
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class VideoView2Test {
+ /** Debug TAG. **/
+ private static final String TAG = "VideoView2Test";
+ /** The maximum time to wait for an operation. */
+ private static final long TIME_OUT = 15000L;
+ /** The interval time to wait for completing an operation. */
+ private static final long OPERATION_INTERVAL = 1500L;
+ /** The duration of R.raw.testvideo. */
+ private static final int TEST_VIDEO_DURATION = 11047;
+ /** The full name of R.raw.testvideo. */
+ private static final String VIDEO_NAME = "testvideo.3gp";
+ /** delta for duration in case user uses different decoders on different
+ hardware that report a duration that's different by a few milliseconds */
+ private static final int DURATION_DELTA = 100;
+ /** AudioAttributes to be used by this player */
+ private static final AudioAttributesCompat AUDIO_ATTR = new AudioAttributesCompat.Builder()
+ .setUsage(AudioAttributes.USAGE_GAME)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build();
+ private Instrumentation mInstrumentation;
+ private Activity mActivity;
+ private KeyguardManager mKeyguardManager;
+ private VideoView2 mVideoView;
+ private MediaControllerCompat mController;
+ private String mVideoPath;
+
+ @Rule
+ public ActivityTestRule<VideoView2TestActivity> mActivityRule =
+ new ActivityTestRule<>(VideoView2TestActivity.class);
+
+ @Before
+ public void setup() throws Throwable {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mKeyguardManager = (KeyguardManager)
+ mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
+ mActivity = mActivityRule.getActivity();
+ mVideoView = (VideoView2) mActivity.findViewById(R.id.videoview);
+ mVideoPath = prepareSampleVideo();
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // Keep screen on while testing.
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mActivity.setTurnScreenOn(true);
+ mActivity.setShowWhenLocked(true);
+ mKeyguardManager.requestDismissKeyguard(mActivity, null);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
+ final View.OnAttachStateChangeListener mockAttachListener =
+ mock(View.OnAttachStateChangeListener.class);
+ if (!mVideoView.isAttachedToWindow()) {
+ mVideoView.addOnAttachStateChangeListener(mockAttachListener);
+ verify(mockAttachListener, timeout(TIME_OUT)).onViewAttachedToWindow(same(mVideoView));
+ }
+ mController = mVideoView.getMediaController();
+ }
+
+ @After
+ public void tearDown() throws Throwable {
+ /** call media controller's stop */
+ }
+
+ private boolean hasCodec() {
+ return MediaUtils2.hasCodecsForResource(mActivity, R.raw.testvideo);
+ }
+
+ private String prepareSampleVideo() throws IOException {
+ try (InputStream source = mActivity.getResources().openRawResource(R.raw.testvideo);
+ OutputStream target = mActivity.openFileOutput(VIDEO_NAME, Context.MODE_PRIVATE)) {
+ final byte[] buffer = new byte[1024];
+ for (int len = source.read(buffer); len > 0; len = source.read(buffer)) {
+ target.write(buffer, 0, len);
+ }
+ }
+
+ return mActivity.getFileStreamPath(VIDEO_NAME).getAbsolutePath();
+ }
+
+ @UiThreadTest
+ @Test
+ public void testConstructor() {
+ new VideoView2(mActivity);
+ new VideoView2(mActivity, null);
+ new VideoView2(mActivity, null, 0);
+ }
+
+ @Test
+ public void testPlayVideo() throws Throwable {
+ // Don't run the test if the codec isn't supported.
+ if (!hasCodec()) {
+ Log.i(TAG, "SKIPPING testPlayVideo(): codec is not supported");
+ return;
+ }
+
+ final MediaControllerCompat.Callback mockControllerCallback =
+ mock(MediaControllerCompat.Callback.class);
+ final MediaControllerCompat.Callback callbackHelper = new MediaControllerCompat.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ mockControllerCallback.onPlaybackStateChanged(state);
+ }
+ };
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mController.registerCallback(callbackHelper);
+ mVideoView.setVideoPath(mVideoPath);
+ mController.getTransportControls().play();
+ }
+ });
+ ArgumentCaptor<PlaybackStateCompat> someState =
+ ArgumentCaptor.forClass(PlaybackStateCompat.class);
+ verify(mockControllerCallback, timeout(TIME_OUT).atLeast(3)).onPlaybackStateChanged(
+ someState.capture());
+ List<PlaybackStateCompat> states = someState.getAllValues();
+ assertEquals(PlaybackStateCompat.STATE_PAUSED, states.get(0).getState());
+ assertEquals(PlaybackStateCompat.STATE_PLAYING, states.get(1).getState());
+ assertEquals(PlaybackStateCompat.STATE_STOPPED, states.get(2).getState());
+ }
+
+ @Test
+ public void testPlayVideoOnTextureView() throws Throwable {
+ // Don't run the test if the codec isn't supported.
+ if (!hasCodec()) {
+ Log.i(TAG, "SKIPPING testPlayVideoOnTextureView(): codec is not supported");
+ return;
+ }
+ final VideoView2.OnViewTypeChangedListener mockViewTypeListener =
+ mock(VideoView2.OnViewTypeChangedListener.class);
+ final MediaControllerCompat.Callback mockControllerCallback =
+ mock(MediaControllerCompat.Callback.class);
+ final MediaControllerCompat.Callback callbackHelper = new MediaControllerCompat.Callback() {
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ mockControllerCallback.onPlaybackStateChanged(state);
+ }
+ };
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mVideoView.setOnViewTypeChangedListener(mockViewTypeListener);
+ mVideoView.setViewType(mVideoView.VIEW_TYPE_TEXTUREVIEW);
+ mController.registerCallback(callbackHelper);
+ mVideoView.setVideoPath(mVideoPath);
+ }
+ });
+ verify(mockViewTypeListener, timeout(TIME_OUT))
+ .onViewTypeChanged(mVideoView, VideoView2.VIEW_TYPE_TEXTUREVIEW);
+
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mController.getTransportControls().play();
+ }
+ });
+ ArgumentCaptor<PlaybackStateCompat> someState =
+ ArgumentCaptor.forClass(PlaybackStateCompat.class);
+ verify(mockControllerCallback, timeout(TIME_OUT).atLeast(3)).onPlaybackStateChanged(
+ someState.capture());
+ List<PlaybackStateCompat> states = someState.getAllValues();
+ assertEquals(PlaybackStateCompat.STATE_PAUSED, states.get(0).getState());
+ assertEquals(PlaybackStateCompat.STATE_PLAYING, states.get(1).getState());
+ assertEquals(PlaybackStateCompat.STATE_STOPPED, states.get(2).getState());
+ }
+}
diff --git a/android/security/keystore/LockScreenRequiredException.java b/androidx/widget/VideoView2TestActivity.java
index 09702845..26a9c92d 100644
--- a/android/security/keystore/LockScreenRequiredException.java
+++ b/androidx/widget/VideoView2TestActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,14 +14,23 @@
* limitations under the License.
*/
-package android.security.keystore;
+package androidx.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.media.test.R;
/**
- * @deprecated Use {@link android.security.keystore.recovery.LockScreenRequiredException}.
- * @hide
+ * A minimal application for {@link VideoView2} test.
*/
-public class LockScreenRequiredException extends RecoveryControllerException {
- public LockScreenRequiredException(String msg) {
- super(msg);
+public class VideoView2TestActivity extends Activity {
+ /**
+ * Called with the activity is first created.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.videoview2_layout);
}
}
diff --git a/androidx/widget/VideoViewInterface.java b/androidx/widget/VideoViewInterface.java
new file mode 100644
index 00000000..eca8c82e
--- /dev/null
+++ b/androidx/widget/VideoViewInterface.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.widget;
+
+import android.media.MediaPlayer;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+interface VideoViewInterface {
+ /**
+ * Assigns the view's surface to the given MediaPlayer instance.
+ *
+ * @param mp MediaPlayer
+ * @return true if the surface is successfully assigned, false if not. It will fail to assign
+ * if any of MediaPlayer or surface is unavailable.
+ */
+ boolean assignSurfaceToMediaPlayer(MediaPlayer mp);
+ void setSurfaceListener(SurfaceListener l);
+ int getViewType();
+ void setMediaPlayer(MediaPlayer mp);
+
+ /**
+ * Takes over oldView. It means that the MediaPlayer will start rendering on this view.
+ * The visibility of oldView will be set as {@link View.GONE}. If the view doesn't have a
+ * MediaPlayer instance or its surface is not available, the actual execution is deferred until
+ * a MediaPlayer instance is set by {@link #setMediaPlayer} or its surface becomes available.
+ * {@link SurfaceListener.onSurfaceTakeOverDone} will be called when the actual execution is
+ * done.
+ *
+ * @param oldView The view that MediaPlayer is currently rendering on.
+ */
+ void takeOver(@NonNull VideoViewInterface oldView);
+
+ /**
+ * Indicates if the view's surface is available.
+ *
+ * @return true if the surface is available.
+ */
+ boolean hasAvailableSurface();
+
+ /**
+ * An instance of VideoViewInterface calls these surface notification methods accordingly if
+ * a listener has been registered via {@link #setSurfaceListener(SurfaceListener)}.
+ */
+ interface SurfaceListener {
+ void onSurfaceCreated(View view, int width, int height);
+ void onSurfaceDestroyed(View view);
+ void onSurfaceChanged(View view, int width, int height);
+ void onSurfaceTakeOverDone(VideoViewInterface view);
+ }
+}
diff --git a/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index dbdf5e16..cdc3867a 100644
--- a/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -30,6 +30,7 @@ import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.Proxy;
import android.net.Uri;
+import android.net.dns.ResolvUtil;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
@@ -119,6 +120,8 @@ public class CaptivePortalLoginActivity extends Activity {
// Also initializes proxy system properties.
mCm.bindProcessToNetwork(mNetwork);
+ mCm.setProcessDefaultNetworkForHostResolution(
+ ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
// Proxy system properties must be initialized before setContentView is called because
// setContentView initializes the WebView logic which in turn reads the system properties.
diff --git a/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java b/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java
index 95ec83dd..7479d9aa 100644
--- a/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java
+++ b/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java
@@ -32,6 +32,7 @@ import android.net.NetworkRequest;
import android.net.Proxy;
import android.net.TrafficStats;
import android.net.Uri;
+import android.net.dns.ResolvUtil;
import android.net.http.SslError;
import android.os.Bundle;
import android.telephony.CarrierConfigManager;
@@ -115,6 +116,8 @@ public class CaptivePortalLoginActivity extends Activity {
requestNetworkForCaptivePortal();
} else {
mCm.bindProcessToNetwork(mNetwork);
+ mCm.setProcessDefaultNetworkForHostResolution(
+ ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
// Start initial page load so WebView finishes loading proxy settings.
// Actual load of mUrl is initiated by MyWebViewClient.
mWebView.loadData("", "text/html", null);
diff --git a/com/android/clockwork/bluetooth/BluetoothShardRunner.java b/com/android/clockwork/bluetooth/BluetoothShardRunner.java
index 4f0fecdd..43db87b5 100644
--- a/com/android/clockwork/bluetooth/BluetoothShardRunner.java
+++ b/com/android/clockwork/bluetooth/BluetoothShardRunner.java
@@ -69,12 +69,12 @@ public class BluetoothShardRunner {
@MainThread
void stopProxyShard() {
- mCompanionShardStops += 1;
if (mProxyShard != null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "BluetoothShardRunner Stopping CompanionProxyShard.");
}
Util.close(mProxyShard);
+ mCompanionShardStops += 1;
}
mProxyShard = null;
}
diff --git a/com/android/clockwork/bluetooth/CompanionProxyShard.java b/com/android/clockwork/bluetooth/CompanionProxyShard.java
index 3f04725e..05b0bf81 100644
--- a/com/android/clockwork/bluetooth/CompanionProxyShard.java
+++ b/com/android/clockwork/bluetooth/CompanionProxyShard.java
@@ -246,11 +246,6 @@ public class CompanionProxyShard implements Closeable, ProxyServiceManager.Proxy
Log.d(TAG, "CompanionProxyShard [ " + mInstance + " ] Request to start"
+ " companion sysproxy network");
}
- if (companionIsNotAvailable()) {
- Log.e(TAG, "CompanionProxyShard [ " + mInstance + " ] Unable to start"
- + " sysproxy bluetooth off or companion unpaired");
- return;
- }
if (mState.checkState(State.SYSPROXY_CONNECTED)) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "CompanionProxyShard [ " + mInstance + " ] companion proxy"
@@ -302,6 +297,7 @@ public class CompanionProxyShard implements Closeable, ProxyServiceManager.Proxy
}
if (!mIsClosed && mState.current() != State.SYSPROXY_DISCONNECTED) {
disconnectAndNotify(Reason.SYSPROXY_DISCONNECTED);
+ setUpRetry();
}
mState.advanceState(mInstance, State.SYSPROXY_DISCONNECTED);
break;
@@ -324,18 +320,22 @@ public class CompanionProxyShard implements Closeable, ProxyServiceManager.Proxy
mHandler.removeMessages(WHAT_START_SYSPROXY);
mHandler.removeMessages(WHAT_RESET_CONNECTION);
disconnectNativeInBackground();
- // Setup a reconnect sequence if shard has not been closed.
- if (!mIsClosed) {
- final int nextRetry = mReconnectBackoff.getNextBackoff();
- mHandler.sendEmptyMessageDelayed(WHAT_START_SYSPROXY, nextRetry * 1000);
- Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Proxy reset"
- + " Attempting reconnect in " + nextRetry + " seconds");
- }
+ setUpRetry();
break;
}
}
};
+ private void setUpRetry() {
+ // Setup a reconnect sequence if shard has not been closed.
+ if (!mIsClosed) {
+ final int nextRetry = mReconnectBackoff.getNextBackoff();
+ mHandler.sendEmptyMessageDelayed(WHAT_START_SYSPROXY, nextRetry * 1000);
+ Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Proxy reset"
+ + " Attempting reconnect in " + nextRetry + " seconds");
+ }
+ }
+
/** Use binder API to directly request rfcomm socket from bluetooth module */
@MainThread
private void getBluetoothSocket() {
@@ -571,29 +571,6 @@ public class CompanionProxyShard implements Closeable, ProxyServiceManager.Proxy
}
}
- /** Check if bluetooth is on and companion paired before connecting to sysproxy */
- private boolean companionIsNotAvailable() {
- return !isBluetoothOn() || companionHasBecomeUnpaired();
- }
-
- private boolean companionHasBecomeUnpaired() {
- final boolean unpaired = mCompanionDevice.getBondState() == BluetoothDevice.BOND_NONE;
- if (unpaired) {
- Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Companion has become unpaired");
- }
- return unpaired;
- }
-
- private boolean isBluetoothOn() {
- final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- if (adapter != null && adapter.isEnabled()) {
- return true;
- }
- Log.w(TAG, "CompanionProxyShard [ " + mInstance + " ] Bluetooth adapter is off or in"
- + " unknown state");
- return false;
- }
-
private abstract static class DefaultPriorityAsyncTask<Params, Progress, Result>
extends AsyncTask<Params, Progress, Result> {
diff --git a/com/android/clockwork/bluetooth/CompanionProxyShardTest.java b/com/android/clockwork/bluetooth/CompanionProxyShardTest.java
index 386e00bc..7a23747b 100644
--- a/com/android/clockwork/bluetooth/CompanionProxyShardTest.java
+++ b/com/android/clockwork/bluetooth/CompanionProxyShardTest.java
@@ -224,12 +224,14 @@ public class CompanionProxyShardTest {
CompanionProxyShard.State.SYSPROXY_DISCONNECTED);
when(mockBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+ ShadowLooper.pauseMainLooper();
mCompanionProxyShard.onStartNetwork();
+ ShadowLooper.runMainLooperOneTask();
assertEquals(0, mCompanionProxyShard.connectNativeCount);
verify(mockParcelFileDescriptor, never()).detachFd();
- assertEquals(CompanionProxyShard.State.SYSPROXY_DISCONNECTED,
+ assertEquals(CompanionProxyShard.State.BLUETOOTH_SOCKET_REQUESTING,
mCompanionProxyShard.mState.current());
ensureMessageQueueEmpty();
@@ -247,7 +249,7 @@ public class CompanionProxyShardTest {
ShadowLooper.runMainLooperOneTask();
- assertEquals(CompanionProxyShard.State.SYSPROXY_DISCONNECTED,
+ assertEquals(CompanionProxyShard.State.BLUETOOTH_SOCKET_REQUESTING,
mCompanionProxyShard.mState.current());
ensureMessageQueueEmpty();
// Restore bluetooth adapter to return a valid instance
@@ -261,11 +263,12 @@ public class CompanionProxyShardTest {
when(mockBluetoothAdapter.isEnabled()).thenReturn(false);
+ ShadowLooper.pauseMainLooper();
mCompanionProxyShard.onStartNetwork();
ShadowLooper.runMainLooperOneTask();
- assertEquals(CompanionProxyShard.State.SYSPROXY_DISCONNECTED,
+ assertEquals(CompanionProxyShard.State.BLUETOOTH_SOCKET_REQUESTING,
mCompanionProxyShard.mState.current());
ensureMessageQueueEmpty();
}
diff --git a/com/android/clockwork/bluetooth/WearBluetoothMediator.java b/com/android/clockwork/bluetooth/WearBluetoothMediator.java
index 1ed769cf..14034bfd 100644
--- a/com/android/clockwork/bluetooth/WearBluetoothMediator.java
+++ b/com/android/clockwork/bluetooth/WearBluetoothMediator.java
@@ -509,7 +509,6 @@ public class WearBluetoothMediator implements
@WorkerThread
@Override
public void handleMessage(Message msg) {
- Log.e(TAG, "CMM handleMessage" + msg);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "handleMessage: " + msg);
}
diff --git a/com/android/ex/photo/ActionBarWrapper.java b/com/android/ex/photo/ActionBarWrapper.java
index ae621979..6d4d4d20 100644
--- a/com/android/ex/photo/ActionBarWrapper.java
+++ b/com/android/ex/photo/ActionBarWrapper.java
@@ -1,8 +1,7 @@
package com.android.ex.photo;
-
+import android.app.ActionBar;
import android.graphics.drawable.Drawable;
-import android.support.v7.app.ActionBar;
/**
* Wrapper around {@link ActionBar}.
diff --git a/com/android/ex/photo/PhotoViewActivity.java b/com/android/ex/photo/PhotoViewActivity.java
index a5c4a438..7b53918f 100644
--- a/com/android/ex/photo/PhotoViewActivity.java
+++ b/com/android/ex/photo/PhotoViewActivity.java
@@ -21,14 +21,14 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
+import android.support.v4.app.FragmentActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* Activity to view the contents of an album.
*/
-public class PhotoViewActivity extends AppCompatActivity
+public class PhotoViewActivity extends FragmentActivity
implements PhotoViewController.ActivityInterface {
private PhotoViewController mController;
@@ -41,7 +41,7 @@ public class PhotoViewActivity extends AppCompatActivity
mController.onCreate(savedInstanceState);
}
- protected PhotoViewController createController() {
+ public PhotoViewController createController() {
return new PhotoViewController(this);
}
@@ -122,7 +122,7 @@ public class PhotoViewActivity extends AppCompatActivity
@Override
public ActionBarInterface getActionBarInterface() {
if (mActionBar == null) {
- mActionBar = new ActionBarWrapper(getSupportActionBar());
+ mActionBar = new ActionBarWrapper(getActionBar());
}
return mActionBar;
}
diff --git a/com/android/ims/MmTelFeatureConnection.java b/com/android/ims/MmTelFeatureConnection.java
index de8f9282..b2472c8a 100644
--- a/com/android/ims/MmTelFeatureConnection.java
+++ b/com/android/ims/MmTelFeatureConnection.java
@@ -451,7 +451,11 @@ public class MmTelFeatureConnection {
mRegistrationCallbackManager.close();
mCapabilityCallbackManager.close();
try {
- getServiceInterface(mBinder).setListener(null);
+ synchronized (mLock) {
+ if (isBinderAlive()) {
+ getServiceInterface(mBinder).setListener(null);
+ }
+ }
} catch (RemoteException e) {
Log.w(TAG, "closeConnection: couldn't remove listener!");
}
diff --git a/com/android/internal/app/ColorDisplayController.java b/com/android/internal/app/ColorDisplayController.java
index 278d31af..f1539eeb 100644
--- a/com/android/internal/app/ColorDisplayController.java
+++ b/com/android/internal/app/ColorDisplayController.java
@@ -365,6 +365,10 @@ public final class ColorDisplayController {
* Get the current color mode.
*/
public int getColorMode() {
+ if (getAccessibilityTransformActivated()) {
+ return COLOR_MODE_SATURATED;
+ }
+
final int colorMode = System.getIntForUser(mContext.getContentResolver(),
System.DISPLAY_COLOR_MODE, -1, mUserId);
if (colorMode < COLOR_MODE_NATURAL || colorMode > COLOR_MODE_SATURATED) {
@@ -416,6 +420,18 @@ public final class ColorDisplayController {
R.integer.config_nightDisplayColorTemperatureDefault);
}
+ /**
+ * Returns true if any Accessibility color transforms are enabled.
+ */
+ public boolean getAccessibilityTransformActivated() {
+ final ContentResolver cr = mContext.getContentResolver();
+ return
+ Secure.getIntForUser(cr, Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
+ 0, mUserId) == 1
+ || Secure.getIntForUser(cr, Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+ 0, mUserId) == 1;
+ }
+
private void onSettingChanged(@NonNull String setting) {
if (DEBUG) {
Slog.d(TAG, "onSettingChanged: " + setting);
@@ -441,6 +457,10 @@ public final class ColorDisplayController {
case System.DISPLAY_COLOR_MODE:
mCallback.onDisplayColorModeChanged(getColorMode());
break;
+ case Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED:
+ case Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED:
+ mCallback.onAccessibilityTransformChanged(getAccessibilityTransformActivated());
+ break;
}
}
}
@@ -471,6 +491,12 @@ public final class ColorDisplayController {
false /* notifyForDescendants */, mContentObserver, mUserId);
cr.registerContentObserver(System.getUriFor(System.DISPLAY_COLOR_MODE),
false /* notifyForDecendants */, mContentObserver, mUserId);
+ cr.registerContentObserver(
+ Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED),
+ false /* notifyForDecendants */, mContentObserver, mUserId);
+ cr.registerContentObserver(
+ Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED),
+ false /* notifyForDecendants */, mContentObserver, mUserId);
}
}
}
@@ -531,5 +557,12 @@ public final class ColorDisplayController {
* @param displayColorMode the color mode
*/
default void onDisplayColorModeChanged(int displayColorMode) {}
+
+ /**
+ * Callback invoked when Accessibility color transforms change.
+ *
+ * @param state the state Accessibility color transforms (true of active)
+ */
+ default void onAccessibilityTransformChanged(boolean state) {}
}
}
diff --git a/com/android/internal/app/SuspendedAppActivity.java b/com/android/internal/app/SuspendedAppActivity.java
new file mode 100644
index 00000000..25af3557
--- /dev/null
+++ b/com/android/internal/app/SuspendedAppActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.internal.app;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+
+public class SuspendedAppActivity extends AlertActivity
+ implements DialogInterface.OnClickListener {
+ private static final String TAG = "SuspendedAppActivity";
+
+ public static final String EXTRA_SUSPENDED_PACKAGE =
+ "SuspendedAppActivity.extra.SUSPENDED_PACKAGE";
+ public static final String EXTRA_SUSPENDING_PACKAGE =
+ "SuspendedAppActivity.extra.SUSPENDING_PACKAGE";
+ public static final String EXTRA_DIALOG_MESSAGE = "SuspendedAppActivity.extra.DIALOG_MESSAGE";
+ public static final String EXTRA_MORE_DETAILS_INTENT =
+ "SuspendedAppActivity.extra.MORE_DETAILS_INTENT";
+
+ private Intent mMoreDetailsIntent;
+ private int mUserId;
+
+ private CharSequence getAppLabel(String packageName) {
+ final PackageManager pm = getPackageManager();
+ try {
+ return pm.getApplicationInfoAsUser(packageName, 0, mUserId).loadLabel(pm);
+ } catch (PackageManager.NameNotFoundException ne) {
+ Slog.e(TAG, "Package " + packageName + " not found", ne);
+ }
+ return packageName;
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ super.onCreate(icicle);
+
+ final Intent intent = getIntent();
+ mMoreDetailsIntent = intent.getParcelableExtra(EXTRA_MORE_DETAILS_INTENT);
+ mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1);
+ if (mUserId < 0) {
+ Slog.wtf(TAG, "Invalid user: " + mUserId);
+ finish();
+ return;
+ }
+ final String suppliedMessage = intent.getStringExtra(EXTRA_DIALOG_MESSAGE);
+ final CharSequence suspendedAppLabel = getAppLabel(
+ intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE));
+ final CharSequence dialogMessage;
+ if (suppliedMessage == null) {
+ dialogMessage = getString(R.string.app_suspended_default_message,
+ suspendedAppLabel,
+ getAppLabel(intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE)));
+ } else {
+ dialogMessage = String.format(getResources().getConfiguration().getLocales().get(0),
+ suppliedMessage, suspendedAppLabel);
+ }
+
+ final AlertController.AlertParams ap = mAlertParams;
+ ap.mTitle = getString(R.string.app_suspended_title);
+ ap.mMessage = dialogMessage;
+ ap.mPositiveButtonText = getString(android.R.string.ok);
+ if (mMoreDetailsIntent != null) {
+ ap.mNeutralButtonText = getString(R.string.app_suspended_more_details);
+ }
+ ap.mPositiveButtonListener = ap.mNeutralButtonListener = this;
+ setupAlert();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case AlertDialog.BUTTON_NEUTRAL:
+ startActivityAsUser(mMoreDetailsIntent, UserHandle.of(mUserId));
+ Slog.i(TAG, "Started more details activity");
+ break;
+ }
+ finish();
+ }
+}
diff --git a/com/android/internal/os/BatteryStatsHelper.java b/com/android/internal/os/BatteryStatsHelper.java
index 1e5bd189..b49aaced 100644
--- a/com/android/internal/os/BatteryStatsHelper.java
+++ b/com/android/internal/os/BatteryStatsHelper.java
@@ -657,7 +657,7 @@ public class BatteryStatsHelper {
* {@link #removeHiddenBatterySippers(List)}.
*/
private void addAmbientDisplayUsage() {
- long ambientDisplayMs = mStats.getScreenDozeTime(mRawRealtimeUs, mStatsType);
+ long ambientDisplayMs = mStats.getScreenDozeTime(mRawRealtimeUs, mStatsType) / 1000;
double power = mPowerProfile.getAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY)
* ambientDisplayMs / (60 * 60 * 1000);
if (power > 0) {
diff --git a/com/android/internal/os/BatteryStatsImpl.java b/com/android/internal/os/BatteryStatsImpl.java
index 89f61568..5da3874d 100644
--- a/com/android/internal/os/BatteryStatsImpl.java
+++ b/com/android/internal/os/BatteryStatsImpl.java
@@ -21,10 +21,13 @@ import android.annotation.Nullable;
import android.app.ActivityManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.UidTraffic;
+import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.database.ContentObserver;
+import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.NetworkStats;
import android.net.Uri;
@@ -766,7 +769,10 @@ public class BatteryStatsImpl extends BatteryStats {
int mCameraOnNesting;
StopwatchTimer mCameraOnTimer;
- int mUsbDataState; // 0: unknown, 1: disconnected, 2: connected
+ private static final int USB_DATA_UNKNOWN = 0;
+ private static final int USB_DATA_DISCONNECTED = 1;
+ private static final int USB_DATA_CONNECTED = 2;
+ int mUsbDataState = USB_DATA_UNKNOWN;
int mGpsSignalQualityBin = -1;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -5241,8 +5247,30 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
- public void noteUsbConnectionStateLocked(boolean connected) {
- int newState = connected ? 2 : 1;
+ private void registerUsbStateReceiver(Context context) {
+ final IntentFilter usbStateFilter = new IntentFilter();
+ usbStateFilter.addAction(UsbManager.ACTION_USB_STATE);
+ context.registerReceiver(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final boolean state = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false);
+ synchronized (BatteryStatsImpl.this) {
+ noteUsbConnectionStateLocked(state);
+ }
+ }
+ }, usbStateFilter);
+ synchronized (this) {
+ if (mUsbDataState == USB_DATA_UNKNOWN) {
+ final Intent usbState = context.registerReceiver(null, usbStateFilter);
+ final boolean initState = usbState != null && usbState.getBooleanExtra(
+ UsbManager.USB_CONNECTED, false);
+ noteUsbConnectionStateLocked(initState);
+ }
+ }
+ }
+
+ private void noteUsbConnectionStateLocked(boolean connected) {
+ int newState = connected ? USB_DATA_CONNECTED : USB_DATA_DISCONNECTED;
if (mUsbDataState != newState) {
mUsbDataState = newState;
if (connected) {
@@ -13218,6 +13246,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void systemServicesReady(Context context) {
mConstants.startObserving(context.getContentResolver());
+ registerUsbStateReceiver(context);
}
@VisibleForTesting
diff --git a/com/android/internal/os/RuntimeInit.java b/com/android/internal/os/RuntimeInit.java
index bb5a0ad8..a9cd5c8e 100644
--- a/com/android/internal/os/RuntimeInit.java
+++ b/com/android/internal/os/RuntimeInit.java
@@ -34,6 +34,7 @@ import dalvik.system.VMRuntime;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.util.Objects;
import java.util.TimeZone;
import java.util.logging.LogManager;
import org.apache.harmony.luni.internal.util.TimezoneGetter;
@@ -67,8 +68,12 @@ public class RuntimeInit {
* but apps can override that behavior.
*/
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
+ public volatile boolean mTriggered = false;
+
@Override
public void uncaughtException(Thread t, Throwable e) {
+ mTriggered = true;
+
// Don't re-enter if KillApplicationHandler has already run
if (mCrashing) return;
@@ -96,12 +101,33 @@ public class RuntimeInit {
/**
* Handle application death from an uncaught exception. The framework
* catches these for the main threads, so this should only matter for
- * threads created by applications. Before this method runs,
- * {@link LoggingHandler} will already have logged details.
+ * threads created by applications. Before this method runs, the given
+ * instance of {@link LoggingHandler} should already have logged details
+ * (and if not it is run first).
*/
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
+ private final LoggingHandler mLoggingHandler;
+
+ /**
+ * Create a new KillApplicationHandler that follows the given LoggingHandler.
+ * If {@link #uncaughtException(Thread, Throwable) uncaughtException} is called
+ * on the created instance without {@code loggingHandler} having been triggered,
+ * {@link LoggingHandler#uncaughtException(Thread, Throwable)
+ * loggingHandler.uncaughtException} will be called first.
+ *
+ * @param loggingHandler the {@link LoggingHandler} expected to have run before
+ * this instance's {@link #uncaughtException(Thread, Throwable) uncaughtException}
+ * is being called.
+ */
+ public KillApplicationHandler(LoggingHandler loggingHandler) {
+ this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
+ }
+
+ @Override
public void uncaughtException(Thread t, Throwable e) {
try {
+ ensureLogging(t, e);
+
// Don't re-enter -- avoid infinite loops if crash-reporting crashes.
if (mCrashing) return;
mCrashing = true;
@@ -132,6 +158,33 @@ public class RuntimeInit {
System.exit(10);
}
}
+
+ /**
+ * Ensures that the logging handler has been triggered.
+ *
+ * See b/73380984. This reinstates the pre-O behavior of
+ *
+ * {@code thread.getUncaughtExceptionHandler().uncaughtException(thread, e);}
+ *
+ * logging the exception (in addition to killing the app). This behavior
+ * was never documented / guaranteed but helps in diagnostics of apps
+ * using the pattern.
+ *
+ * If this KillApplicationHandler is invoked the "regular" way (by
+ * {@link Thread#dispatchUncaughtException(Throwable)
+ * Thread.dispatchUncaughtException} in case of an uncaught exception)
+ * then the pre-handler (expected to be {@link #mLoggingHandler}) will already
+ * have run. Otherwise, we manually invoke it here.
+ */
+ private void ensureLogging(Thread t, Throwable e) {
+ if (!mLoggingHandler.mTriggered) {
+ try {
+ mLoggingHandler.uncaughtException(t, e);
+ } catch (Throwable loggingThrowable) {
+ // Ignored.
+ }
+ }
+ }
}
protected static final void commonInit() {
@@ -141,8 +194,9 @@ public class RuntimeInit {
* set handlers; these apply to all threads in the VM. Apps can replace
* the default handler, but not the pre handler.
*/
- Thread.setUncaughtExceptionPreHandler(new LoggingHandler());
- Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler());
+ LoggingHandler loggingHandler = new LoggingHandler();
+ Thread.setUncaughtExceptionPreHandler(loggingHandler);
+ Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
/*
* Install a TimezoneGetter subclass for ZoneInfo.db
diff --git a/com/android/internal/os/ZygoteConnection.java b/com/android/internal/os/ZygoteConnection.java
index 5d40a730..f537e3e2 100644
--- a/com/android/internal/os/ZygoteConnection.java
+++ b/com/android/internal/os/ZygoteConnection.java
@@ -166,6 +166,11 @@ class ZygoteConnection {
return null;
}
+ if (parsedArgs.hiddenApiAccessLogSampleRate != -1) {
+ handleHiddenApiAccessLogSampleRate(parsedArgs.hiddenApiAccessLogSampleRate);
+ return null;
+ }
+
if (parsedArgs.permittedCapabilities != 0 || parsedArgs.effectiveCapabilities != 0) {
throw new ZygoteSecurityException("Client may not specify capabilities: " +
"permitted=0x" + Long.toHexString(parsedArgs.permittedCapabilities) +
@@ -294,6 +299,15 @@ class ZygoteConnection {
}
}
+ private void handleHiddenApiAccessLogSampleRate(int percent) {
+ try {
+ ZygoteInit.setHiddenApiAccessLogSampleRate(percent);
+ mSocketOutStream.writeInt(0);
+ } catch (IOException ioe) {
+ throw new IllegalStateException("Error writing to command socket", ioe);
+ }
+ }
+
protected void preload() {
ZygoteInit.lazyPreload();
}
@@ -461,6 +475,12 @@ class ZygoteConnection {
String[] apiBlacklistExemptions;
/**
+ * Sampling rate for logging hidden API accesses to the event log. This is sent to the
+ * pre-forked zygote at boot time, or when it changes, via --hidden-api-log-sampling-rate.
+ */
+ int hiddenApiAccessLogSampleRate = -1;
+
+ /**
* Constructs instance and parses args
* @param args zygote command-line args
* @throws IllegalArgumentException
@@ -483,6 +503,7 @@ class ZygoteConnection {
boolean seenRuntimeArgs = false;
+ boolean expectRuntimeArgs = true;
for ( /* curArg */ ; curArg < args.length; curArg++) {
String arg = args[curArg];
@@ -612,6 +633,7 @@ class ZygoteConnection {
preloadPackageCacheKey = args[++curArg];
} else if (arg.equals("--preload-default")) {
preloadDefault = true;
+ expectRuntimeArgs = false;
} else if (arg.equals("--start-child-zygote")) {
startChildZygote = true;
} else if (arg.equals("--set-api-blacklist-exemptions")) {
@@ -619,6 +641,16 @@ class ZygoteConnection {
// with the regular fork command.
apiBlacklistExemptions = Arrays.copyOfRange(args, curArg + 1, args.length);
curArg = args.length;
+ expectRuntimeArgs = false;
+ } else if (arg.startsWith("--hidden-api-log-sampling-rate=")) {
+ String rateStr = arg.substring(arg.indexOf('=') + 1);
+ try {
+ hiddenApiAccessLogSampleRate = Integer.parseInt(rateStr);
+ } catch (NumberFormatException nfe) {
+ throw new IllegalArgumentException(
+ "Invalid log sampling rate: " + rateStr, nfe);
+ }
+ expectRuntimeArgs = false;
} else {
break;
}
@@ -633,7 +665,7 @@ class ZygoteConnection {
throw new IllegalArgumentException(
"Unexpected arguments after --preload-package.");
}
- } else if (!preloadDefault && apiBlacklistExemptions == null) {
+ } else if (expectRuntimeArgs) {
if (!seenRuntimeArgs) {
throw new IllegalArgumentException("Unexpected argument : " + args[curArg]);
}
diff --git a/com/android/internal/os/ZygoteInit.java b/com/android/internal/os/ZygoteInit.java
index c5d41db9..6f583653 100644
--- a/com/android/internal/os/ZygoteInit.java
+++ b/com/android/internal/os/ZygoteInit.java
@@ -26,8 +26,8 @@ import android.icu.text.DecimalFormatSymbols;
import android.icu.util.ULocale;
import android.opengl.EGL14;
import android.os.Build;
-import android.os.IInstalld;
import android.os.Environment;
+import android.os.IInstalld;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -44,16 +44,16 @@ import android.system.OsConstants;
import android.system.StructCapUserData;
import android.system.StructCapUserHeader;
import android.text.Hyphenator;
-import android.util.TimingsTraceLog;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
+import android.util.TimingsTraceLog;
import android.webkit.WebViewFactory;
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
-
import com.android.internal.util.Preconditions;
+
import dalvik.system.DexFile;
import dalvik.system.VMRuntime;
import dalvik.system.ZygoteHooks;
@@ -67,8 +67,8 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.security.Security;
import java.security.Provider;
+import java.security.Security;
/**
* Startup class for the zygote process.
@@ -518,6 +518,10 @@ public class ZygoteInit {
VMRuntime.getRuntime().setHiddenApiExemptions(exemptions);
}
+ public static void setHiddenApiAccessLogSampleRate(int percent) {
+ VMRuntime.getRuntime().setHiddenApiAccessLogSamplingRate(percent);
+ }
+
/**
* Creates a PathClassLoader for the given class path that is associated with a shared
* namespace, i.e., this classloader can access platform-private native libraries. The
diff --git a/com/android/internal/telephony/CarrierIdentifier.java b/com/android/internal/telephony/CarrierIdentifier.java
index 56110a29..233adf1c 100644
--- a/com/android/internal/telephony/CarrierIdentifier.java
+++ b/com/android/internal/telephony/CarrierIdentifier.java
@@ -568,7 +568,7 @@ public class CarrierIdentifier extends Handler {
/*
* Write Carrier Identification Matching event, logging with the
- * carrierId, gid1 and carrier list version to differentiate below cases of metrics:
+ * carrierId, mccmnc, gid1 and carrier list version to differentiate below cases of metrics:
* 1) unknown mccmnc - the Carrier Id provider contains no rule that matches the
* read mccmnc.
* 2) the Carrier Id provider contains some rule(s) that match the read mccmnc,
@@ -578,7 +578,8 @@ public class CarrierIdentifier extends Handler {
*/
String unknownGid1ToLog = ((maxScore & CarrierMatchingRule.SCORE_GID1) == 0
&& !TextUtils.isEmpty(subscriptionRule.mGid1)) ? subscriptionRule.mGid1 : null;
- String unknownMccmncToLog = (maxScore == CarrierMatchingRule.SCORE_INVALID
+ String unknownMccmncToLog = ((maxScore == CarrierMatchingRule.SCORE_INVALID
+ || (maxScore & CarrierMatchingRule.SCORE_GID1) == 0)
&& !TextUtils.isEmpty(subscriptionRule.mMccMnc)) ? subscriptionRule.mMccMnc : null;
TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId,
diff --git a/com/android/internal/telephony/InboundSmsTracker.java b/com/android/internal/telephony/InboundSmsTracker.java
index c63ccc8a..36c69961 100644
--- a/com/android/internal/telephony/InboundSmsTracker.java
+++ b/com/android/internal/telephony/InboundSmsTracker.java
@@ -195,7 +195,7 @@ public class InboundSmsTracker {
mAddress = cursor.getString(InboundSmsHandler.ADDRESS_COLUMN);
mDisplayAddress = cursor.getString(InboundSmsHandler.DISPLAY_ADDRESS_COLUMN);
- if (cursor.isNull(InboundSmsHandler.COUNT_COLUMN)) {
+ if (cursor.getInt(InboundSmsHandler.COUNT_COLUMN) == 1) {
// single-part message
long rowId = cursor.getLong(InboundSmsHandler.ID_COLUMN);
mReferenceNumber = -1;
@@ -250,8 +250,8 @@ public class InboundSmsTracker {
values.put("display_originating_addr", mDisplayAddress);
values.put("reference_number", mReferenceNumber);
values.put("sequence", mSequenceNumber);
- values.put("count", mMessageCount);
}
+ values.put("count", mMessageCount);
values.put("message_body", mMessageBody);
return values;
}
diff --git a/com/android/internal/telephony/PhoneSubInfoController.java b/com/android/internal/telephony/PhoneSubInfoController.java
index 4cace9d5..a371191b 100644
--- a/com/android/internal/telephony/PhoneSubInfoController.java
+++ b/com/android/internal/telephony/PhoneSubInfoController.java
@@ -491,10 +491,6 @@ public class PhoneSubInfoController extends IPhoneSubInfo.Stub {
return uiccApp.getIccRecords().getIccSimChallengeResponse(authType, data);
}
- public String getGroupIdLevel1(String callingPackage) {
- return getGroupIdLevel1ForSubscriber(getDefaultSubscription(), callingPackage);
- }
-
public String getGroupIdLevel1ForSubscriber(int subId, String callingPackage) {
Phone phone = getPhone(subId);
if (phone != null) {
diff --git a/com/android/internal/telephony/ServiceStateTracker.java b/com/android/internal/telephony/ServiceStateTracker.java
index 6c94cb25..bdf8bb5f 100644
--- a/com/android/internal/telephony/ServiceStateTracker.java
+++ b/com/android/internal/telephony/ServiceStateTracker.java
@@ -279,6 +279,7 @@ public class ServiceStateTracker extends Handler {
if (DBG) log("SubscriptionListener.onSubscriptionInfoChanged");
// Set the network type, in case the radio does not restore it.
int subId = mPhone.getSubId();
+ ServiceStateTracker.this.mPrevSubId = mPreviousSubId.get();
if (mPreviousSubId.getAndSet(subId) != subId) {
if (SubscriptionManager.isValidSubscriptionId(subId)) {
Context context = mPhone.getContext();
@@ -1064,7 +1065,9 @@ public class ServiceStateTracker extends Handler {
case EVENT_SIM_READY:
// Reset the mPreviousSubId so we treat a SIM power bounce
// as a first boot. See b/19194287
- mOnSubscriptionsChangedListener.mPreviousSubId.set(-1);
+ mOnSubscriptionsChangedListener.mPreviousSubId.set(
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+ mPrevSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
mIsSimReady = true;
pollState();
// Signal strength polling stops when radio is off
@@ -3471,17 +3474,19 @@ public class ServiceStateTracker extends Handler {
}
/**
- * Cancels all notifications posted to NotificationManager. These notifications for restricted
- * state and rejection cause for cs registration are no longer valid after the SIM has been
- * removed.
+ * Cancels all notifications posted to NotificationManager for this subId. These notifications
+ * for restricted state and rejection cause for cs registration are no longer valid after the
+ * SIM has been removed.
*/
private void cancelAllNotifications() {
- if (DBG) log("setNotification: cancelAllNotifications");
+ if (DBG) log("cancelAllNotifications: mPrevSubId=" + mPrevSubId);
NotificationManager notificationManager = (NotificationManager)
mPhone.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(PS_NOTIFICATION);
- notificationManager.cancel(CS_NOTIFICATION);
- notificationManager.cancel(CS_REJECT_CAUSE_NOTIFICATION);
+ if (SubscriptionManager.isValidSubscriptionId(mPrevSubId)) {
+ notificationManager.cancel(Integer.toString(mPrevSubId), PS_NOTIFICATION);
+ notificationManager.cancel(Integer.toString(mPrevSubId), CS_NOTIFICATION);
+ notificationManager.cancel(Integer.toString(mPrevSubId), CS_REJECT_CAUSE_NOTIFICATION);
+ }
}
/**
@@ -3596,7 +3601,7 @@ public class ServiceStateTracker extends Handler {
if (DBG) {
log("setNotification, create notification, notifyType: " + notifyType
- + ", title: " + title + ", details: " + details);
+ + ", title: " + title + ", details: " + details + ", subId: " + mSubId);
}
mNotification = new Notification.Builder(context)
@@ -3617,24 +3622,24 @@ public class ServiceStateTracker extends Handler {
if (notifyType == PS_DISABLED || notifyType == CS_DISABLED) {
// cancel previous post notification
- notificationManager.cancel(notificationId);
+ notificationManager.cancel(Integer.toString(mSubId), notificationId);
} else {
boolean show = false;
- if (mNewSS.isEmergencyOnly() && notifyType == CS_EMERGENCY_ENABLED) {
+ if (mSS.isEmergencyOnly() && notifyType == CS_EMERGENCY_ENABLED) {
// if reg state is emergency only, always show restricted emergency notification.
show = true;
} else if (notifyType == CS_REJECT_CAUSE_ENABLED) {
// always show notification due to CS reject irrespective of service state.
show = true;
- } else if (mNewSS.getState() == ServiceState.STATE_IN_SERVICE) {
+ } else if (mSS.getState() == ServiceState.STATE_IN_SERVICE) {
// for non in service states, we have system UI and signal bar to indicate limited
// service. No need to show notification again. This also helps to mitigate the
// issue if phone go to OOS and camp to other networks and received restricted ind.
show = true;
}
- // update restricted state notification
+ // update restricted state notification for this subId
if (show) {
- notificationManager.notify(notificationId, mNotification);
+ notificationManager.notify(Integer.toString(mSubId), notificationId, mNotification);
}
}
}
diff --git a/com/android/internal/telephony/SubscriptionController.java b/com/android/internal/telephony/SubscriptionController.java
index daf64362..500094b3 100644
--- a/com/android/internal/telephony/SubscriptionController.java
+++ b/com/android/internal/telephony/SubscriptionController.java
@@ -64,7 +64,6 @@ import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
@@ -94,7 +93,7 @@ public class SubscriptionController extends ISub.Stub {
private ScLocalLog mLocalLog = new ScLocalLog(MAX_LOCAL_LOG_LINES);
/* The Cache of Active SubInfoRecord(s) list of currently in use SubInfoRecord(s) */
- private AtomicReference<List<SubscriptionInfo>> mCacheActiveSubInfoList = new AtomicReference();
+ private final List<SubscriptionInfo> mCacheActiveSubInfoList = new ArrayList<>();
/**
* Copied from android.util.LocalLog with flush() adding flush and line number
@@ -368,7 +367,7 @@ public class SubscriptionController extends ISub.Stub {
subList = new ArrayList<SubscriptionInfo>();
}
subList.add(subInfo);
- }
+ }
}
} else {
if (DBG) logd("Query fail");
@@ -598,6 +597,11 @@ public class SubscriptionController extends ISub.Stub {
*/
@Override
public List<SubscriptionInfo> getActiveSubscriptionInfoList(String callingPackage) {
+ if (!isSubInfoReady()) {
+ if (DBG) logdl("[getActiveSubInfoList] Sub Controller not ready");
+ return null;
+ }
+
boolean canReadAllPhoneState;
try {
canReadAllPhoneState = TelephonyPermissions.checkReadPhoneState(mContext,
@@ -607,96 +611,57 @@ public class SubscriptionController extends ISub.Stub {
canReadAllPhoneState = false;
}
- // Perform the operation as ourselves. If the caller cannot read phone state, they may still
- // have carrier privileges for that subscription, so we always need to make the query and
- // then filter the results.
- List<SubscriptionInfo> subList;
- final long identity = Binder.clearCallingIdentity();
- try {
- if (!isSubInfoReady()) {
- if (DBG) logdl("[getActiveSubInfoList] Sub Controller not ready");
- return null;
- }
-
- // Get the active subscription info list from the cache if the cache is not null
- List<SubscriptionInfo> tmpCachedSubList = mCacheActiveSubInfoList.get();
- if (tmpCachedSubList != null) {
- if (DBG_CACHE) {
- for (SubscriptionInfo si : tmpCachedSubList) {
- logd("[getActiveSubscriptionInfoList] Getting Cached subInfo=" + si);
- }
- }
- subList = tmpCachedSubList;
- } else {
- if (DBG_CACHE) {
- logd("[getActiveSubscriptionInfoList] Cached subInfo is null");
- }
- return null;
+ synchronized (mCacheActiveSubInfoList) {
+ // If the caller can read all phone state, just return the full list.
+ if (canReadAllPhoneState) {
+ return new ArrayList<>(mCacheActiveSubInfoList);
}
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- // If the caller can read all phone state, just return the full list.
- if (canReadAllPhoneState) {
- return new ArrayList<>(subList);
+ // Filter the list to only include subscriptions which the caller can manage.
+ return mCacheActiveSubInfoList.stream()
+ .filter(subscriptionInfo -> {
+ try {
+ return TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext,
+ subscriptionInfo.getSubscriptionId(), callingPackage,
+ "getActiveSubscriptionInfoList");
+ } catch (SecurityException e) {
+ return false;
+ }
+ })
+ .collect(Collectors.toList());
}
-
- // Filter the list to only include subscriptions which the (restored) caller can manage.
- return subList.stream()
- .filter(subscriptionInfo -> {
- try {
- return TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext,
- subscriptionInfo.getSubscriptionId(), callingPackage,
- "getActiveSubscriptionInfoList");
- } catch (SecurityException e) {
- return false;
- }
- })
- .collect(Collectors.toList());
}
/**
* Refresh the cache of SubInfoRecord(s) of the currently inserted SIM(s)
*/
- @VisibleForTesting
- protected void refreshCachedActiveSubscriptionInfoList() {
-
- // Now that all security checks passes, perform the operation as ourselves.
- final long identity = Binder.clearCallingIdentity();
- try {
- if (!isSubInfoReady()) {
- if (DBG_CACHE) {
- logdl("[refreshCachedActiveSubscriptionInfoList] "
- + "Sub Controller not ready ");
- }
- return;
+ @VisibleForTesting // For mockito to mock this method
+ public void refreshCachedActiveSubscriptionInfoList() {
+ if (!isSubInfoReady()) {
+ if (DBG_CACHE) {
+ logdl("[refreshCachedActiveSubscriptionInfoList] "
+ + "Sub Controller not ready ");
}
+ return;
+ }
- List<SubscriptionInfo> subList = getSubInfo(
+ synchronized (mCacheActiveSubInfoList) {
+ mCacheActiveSubInfoList.clear();
+ List<SubscriptionInfo> activeSubscriptionInfoList = getSubInfo(
SubscriptionManager.SIM_SLOT_INDEX + ">=0", null);
-
- if (subList != null) {
- // FIXME: Unnecessary when an insertion sort is used!
- subList.sort(SUBSCRIPTION_INFO_COMPARATOR);
-
- if (DBG_CACHE) {
- logdl("[refreshCachedActiveSubscriptionInfoList]- " + subList.size()
- + " infos return");
- }
- } else {
- if (DBG_CACHE) logdl("[refreshCachedActiveSubscriptionInfoList]- no info return");
+ if (activeSubscriptionInfoList != null) {
+ mCacheActiveSubInfoList.addAll(activeSubscriptionInfoList);
}
-
if (DBG_CACHE) {
- for (SubscriptionInfo si : subList) {
- logd("[refreshCachedActiveSubscriptionInfoList] Setting Cached subInfo=" + si);
+ if (!mCacheActiveSubInfoList.isEmpty()) {
+ for (SubscriptionInfo si : mCacheActiveSubInfoList) {
+ logd("[refreshCachedActiveSubscriptionInfoList] Setting Cached info="
+ + si);
+ }
+ } else {
+ logdl("[refreshCachedActiveSubscriptionInfoList]- no info return");
}
}
- mCacheActiveSubInfoList.set(subList);
-
- } finally {
- Binder.restoreCallingIdentity(identity);
}
}
diff --git a/com/android/internal/telephony/dataconnection/DataServiceManager.java b/com/android/internal/telephony/dataconnection/DataServiceManager.java
index 8c3f751d..5e016863 100644
--- a/com/android/internal/telephony/dataconnection/DataServiceManager.java
+++ b/com/android/internal/telephony/dataconnection/DataServiceManager.java
@@ -16,10 +16,18 @@
package com.android.internal.telephony.dataconnection;
+import static android.telephony.AccessNetworkConstants.TransportType.WLAN;
+import static android.telephony.AccessNetworkConstants.TransportType.WWAN;
+
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.net.LinkProperties;
import android.os.AsyncResult;
import android.os.Handler;
@@ -28,7 +36,7 @@ import android.os.Message;
import android.os.PersistableBundle;
import android.os.RegistrantList;
import android.os.RemoteException;
-import android.telephony.AccessNetworkConstants;
+import android.os.ServiceManager;
import android.telephony.CarrierConfigManager;
import android.telephony.Rlog;
import android.telephony.data.DataCallResponse;
@@ -41,8 +49,10 @@ import android.text.TextUtils;
import com.android.internal.telephony.Phone;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -58,6 +68,8 @@ public class DataServiceManager {
private final Phone mPhone;
private final CarrierConfigManager mCarrierConfigManager;
+ private final AppOpsManager mAppOps;
+ private final IPackageManager mPackageManager;
private final int mTransportType;
@@ -73,14 +85,10 @@ public class DataServiceManager {
private final RegistrantList mDataCallListChangedRegistrants = new RegistrantList();
- private class DataServiceManagerDeathRecipient implements IBinder.DeathRecipient {
-
- private final ComponentName mComponentName;
-
- DataServiceManagerDeathRecipient(ComponentName name) {
- mComponentName = name;
- }
+ // not final because it is set by the onServiceConnected method
+ private ComponentName mComponentName;
+ private class DataServiceManagerDeathRecipient implements IBinder.DeathRecipient {
@Override
public void binderDied() {
// TODO: try to rebind the service.
@@ -89,12 +97,53 @@ public class DataServiceManager {
}
}
+ private void grantPermissionsToService(String packageName) {
+ final String[] pkgToGrant = {packageName};
+ try {
+ mPackageManager.grantDefaultPermissionsToEnabledTelephonyDataServices(
+ pkgToGrant, mPhone.getContext().getUserId());
+ mAppOps.setMode(AppOpsManager.OP_MANAGE_IPSEC_TUNNELS, mPhone.getContext().getUserId(),
+ pkgToGrant[0], AppOpsManager.MODE_ALLOWED);
+ } catch (RemoteException e) {
+ loge("Binder to package manager died, permission grant for DataService failed.");
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
+ * Loop through all DataServices installed on the system and revoke permissions from any that
+ * are not currently the WWAN or WLAN data service.
+ */
+ private void revokePermissionsFromUnusedDataServices() {
+ // Except the current data services from having their permissions removed.
+ Set<String> dataServices = getAllDataServicePackageNames();
+ for (int transportType : new int[] {WWAN, WLAN}) {
+ dataServices.remove(getDataServicePackageName(transportType));
+ }
+
+ try {
+ String[] dataServicesArray = new String[dataServices.size()];
+ dataServices.toArray(dataServicesArray);
+ mPackageManager.revokeDefaultPermissionsFromDisabledTelephonyDataServices(
+ dataServicesArray, mPhone.getContext().getUserId());
+ for (String pkg : dataServices) {
+ mAppOps.setMode(AppOpsManager.OP_MANAGE_IPSEC_TUNNELS,
+ mPhone.getContext().getUserId(),
+ pkg, AppOpsManager.MODE_ERRORED);
+ }
+ } catch (RemoteException e) {
+ loge("Binder to package manager died; failed to revoke DataService permissions.");
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
private final class CellularDataServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (DBG) log("onServiceConnected");
+ mComponentName = name;
mIDataService = IDataService.Stub.asInterface(service);
- mDeathRecipient = new DataServiceManagerDeathRecipient(name);
+ mDeathRecipient = new DataServiceManagerDeathRecipient();
mBound = true;
try {
@@ -186,16 +235,22 @@ public class DataServiceManager {
mBound = false;
mCarrierConfigManager = (CarrierConfigManager) phone.getContext().getSystemService(
Context.CARRIER_CONFIG_SERVICE);
-
+ mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
+ mAppOps = (AppOpsManager) phone.getContext().getSystemService(Context.APP_OPS_SERVICE);
bindDataService();
}
private void bindDataService() {
+ // Start by cleaning up all packages that *shouldn't* have permissions.
+ revokePermissionsFromUnusedDataServices();
+
String packageName = getDataServicePackageName();
if (TextUtils.isEmpty(packageName)) {
loge("Can't find the binding package");
return;
}
+ // Then pre-emptively grant the permissions to the package we will bind.
+ grantPermissionsToService(packageName);
try {
if (!mPhone.getContext().bindService(
@@ -209,18 +264,55 @@ public class DataServiceManager {
}
}
+ @NonNull
+ private Set<String> getAllDataServicePackageNames() {
+ // Cowardly using the public PackageManager interface here.
+ // Note: This matches only packages that were installed on the system image. If we ever
+ // expand the permissions model to allow CarrierPrivileged packages, then this will need
+ // to be updated.
+ List<ResolveInfo> dataPackages =
+ mPhone.getContext().getPackageManager().queryIntentServices(
+ new Intent(DataService.DATA_SERVICE_INTERFACE),
+ PackageManager.MATCH_SYSTEM_ONLY);
+ HashSet<String> packageNames = new HashSet<>();
+ for (ResolveInfo info : dataPackages) {
+ if (info.serviceInfo == null) continue;
+ packageNames.add(info.serviceInfo.packageName);
+ }
+ return packageNames;
+ }
+
+ /**
+ * Get the data service package name for our current transport type.
+ *
+ * @return package name of the data service package for the the current transportType.
+ */
private String getDataServicePackageName() {
+ return getDataServicePackageName(mTransportType);
+ }
+
+ /**
+ * Get the data service package by transport type.
+ *
+ * When we bind to a DataService package, we need to revoke permissions from stale
+ * packages; we need to exclude data packages for all transport types, so we need to
+ * to be able to query by transport type.
+ *
+ * @param transportType either WWAN or WLAN
+ * @return package name of the data service package for the specified transportType.
+ */
+ private String getDataServicePackageName(int transportType) {
String packageName;
int resourceId;
String carrierConfig;
- switch (mTransportType) {
- case AccessNetworkConstants.TransportType.WWAN:
+ switch (transportType) {
+ case WWAN:
resourceId = com.android.internal.R.string.config_wwan_data_service_package;
carrierConfig = CarrierConfigManager
.KEY_CARRIER_DATA_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING;
break;
- case AccessNetworkConstants.TransportType.WLAN:
+ case WLAN:
resourceId = com.android.internal.R.string.config_wlan_data_service_package;
carrierConfig = CarrierConfigManager
.KEY_CARRIER_DATA_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING;
diff --git a/com/android/internal/telephony/dataconnection/DcTracker.java b/com/android/internal/telephony/dataconnection/DcTracker.java
index 6c389670..bdd334f2 100644
--- a/com/android/internal/telephony/dataconnection/DcTracker.java
+++ b/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -1976,8 +1976,8 @@ public class DcTracker extends Handler {
// a dun-profiled connection so we can't share an existing one
// On GSM/LTE we can share existing apn connections provided they support
// this type.
- if (apnContext.getApnType() != PhoneConstants.APN_TYPE_DUN ||
- teardownForDun() == false) {
+ if (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DUN)
+ || ServiceState.isGsm(mPhone.getServiceState().getRilDataRadioTechnology())) {
dcac = checkForCompatibleConnectedApnContext(apnContext);
if (dcac != null) {
// Get the dcacApnSetting for the connection we want to share.
diff --git a/com/android/internal/telephony/ims/ImsResolver.java b/com/android/internal/telephony/ims/ImsResolver.java
index c6a7efa9..8059ec07 100644
--- a/com/android/internal/telephony/ims/ImsResolver.java
+++ b/com/android/internal/telephony/ims/ImsResolver.java
@@ -29,6 +29,7 @@ import android.content.pm.ServiceInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telephony.CarrierConfigManager;
@@ -88,8 +89,10 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
private static final int HANDLER_CONFIG_CHANGED = 2;
// A query has been started for an ImsService to relay the features they support.
private static final int HANDLER_START_DYNAMIC_FEATURE_QUERY = 3;
- // A query to request ImsService features has completed.
- private static final int HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE = 4;
+ // A query to request ImsService features has completed or the ImsService has updated features.
+ private static final int HANDLER_DYNAMIC_FEATURE_CHANGE = 4;
+ // Testing: Overrides the current configuration for ImsService binding
+ private static final int HANDLER_OVERRIDE_IMS_SERVICE_CONFIG = 5;
// Delay between dynamic ImsService queries.
private static final int DELAY_DYNAMIC_QUERY_MS = 5000;
@@ -328,6 +331,8 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
private final Object mBoundServicesLock = new Object();
private final int mNumSlots;
private final boolean mIsDynamicBinding;
+ // Package name of the default device service.
+ private String mDeviceService;
// Synchronize all messages on a handler to ensure that the cache includes the most recent
// version of the installed ImsServices.
@@ -345,7 +350,7 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
}
case HANDLER_CONFIG_CHANGED: {
int slotId = (Integer) msg.obj;
- maybeRebindService(slotId);
+ carrierConfigChanged(slotId);
break;
}
case HANDLER_START_DYNAMIC_FEATURE_QUERY: {
@@ -353,7 +358,7 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
startDynamicQuery(info);
break;
}
- case HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE: {
+ case HANDLER_DYNAMIC_FEATURE_CHANGE: {
SomeArgs args = (SomeArgs) msg.obj;
ComponentName name = (ComponentName) args.arg1;
Set<ImsFeatureConfiguration.FeatureSlotPair> features =
@@ -362,6 +367,25 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
dynamicQueryComplete(name, features);
break;
}
+ case HANDLER_OVERRIDE_IMS_SERVICE_CONFIG: {
+ int slotId = msg.arg1;
+ // arg2 will be equal to 1 if it is a carrier service.
+ boolean isCarrierImsService = (msg.arg2 == 1);
+ String packageName = (String) msg.obj;
+ if (isCarrierImsService) {
+ Log.i(TAG, "overriding carrier ImsService - slot=" + slotId + " packageName="
+ + packageName);
+ maybeRebindService(slotId, packageName);
+ } else {
+ Log.i(TAG, "overriding device ImsService - packageName=" + packageName);
+ if (packageName == null || packageName.isEmpty()) {
+ unbindImsService(getImsServiceInfoFromCache(mDeviceService));
+ }
+ mDeviceService = packageName;
+ bindImsService(getImsServiceInfoFromCache(packageName));
+ }
+ break;
+ }
default:
return false;
}
@@ -377,7 +401,7 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
Set<ImsFeatureConfiguration.FeatureSlotPair> features) {
Log.d(TAG, "onComplete called for name: " + name + "features:"
+ printFeatures(features));
- handleFeatureQueryComplete(name, features);
+ handleFeaturesChanged(name, features);
}
@Override
@@ -387,8 +411,6 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
}
};
- // Package name of the default device service.
- private String mDeviceService;
// Array index corresponds to slot Id associated with the service package name.
private String[] mCarrierServices;
// List index corresponds to Slot Id, Maps ImsFeature.FEATURE->bound ImsServiceController
@@ -594,6 +616,36 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
return null;
}
+ // Used for testing only.
+ public boolean overrideImsServiceConfiguration(int slotId, boolean isCarrierService,
+ String packageName) {
+ if (slotId < 0 || slotId >= mNumSlots) {
+ Log.w(TAG, "overrideImsServiceConfiguration: invalid slotId!");
+ return false;
+ }
+
+ if (packageName == null) {
+ Log.w(TAG, "overrideImsServiceConfiguration: null packageName!");
+ return false;
+ }
+
+ // encode boolean to int for Message.
+ int carrierService = isCarrierService ? 1 : 0;
+ Message.obtain(mHandler, HANDLER_OVERRIDE_IMS_SERVICE_CONFIG, slotId, carrierService,
+ packageName).sendToTarget();
+ return true;
+ }
+
+ // used for testing only.
+ public String getImsServiceConfiguration(int slotId, boolean isCarrierService) {
+ if (slotId < 0 || slotId >= mNumSlots) {
+ Log.w(TAG, "getImsServiceConfiguration: invalid slotId!");
+ return "";
+ }
+
+ return isCarrierService ? mCarrierServices[slotId] : mDeviceService;
+ }
+
private void putImsController(int slotId, int feature, ImsServiceController controller) {
if (slotId < 0 || slotId >= mNumSlots || feature <= ImsFeature.FEATURE_INVALID
|| feature >= ImsFeature.FEATURE_MAX) {
@@ -901,6 +953,21 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
}
/**
+ * Implementation of
+ * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeaturesChanged, which
+ * notify the ImsResolver of a change to the supported ImsFeatures of a connected ImsService.
+ */
+ public void imsServiceFeaturesChanged(ImsFeatureConfiguration config,
+ ImsServiceController controller) {
+ if (controller == null || config == null) {
+ return;
+ }
+ Log.i(TAG, "imsServiceFeaturesChanged: config=" + config.getServiceFeatures()
+ + ", ComponentName=" + controller.getComponentName());
+ handleFeaturesChanged(controller.getComponentName(), config.getServiceFeatures());
+ }
+
+ /**
* Determines if the features specified should cause a bind or keep a binding active to an
* ImsService.
* @return true if MMTEL or RCS features are present, false if they are not or only
@@ -917,22 +984,31 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
// Possibly rebind to another ImsService if currently installed ImsServices were changed or if
// the SIM card has changed.
// Called from the handler ONLY
- private void maybeRebindService(int slotId) {
+ private void maybeRebindService(int slotId, String newPackageName) {
if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// not specified, replace package on all slots.
for (int i = 0; i < mNumSlots; i++) {
- updateBoundCarrierServices(i);
+ updateBoundCarrierServices(i, newPackageName);
}
} else {
- updateBoundCarrierServices(slotId);
+ updateBoundCarrierServices(slotId, newPackageName);
}
}
- private void updateBoundCarrierServices(int slotId) {
+ private void carrierConfigChanged(int slotId) {
int subId = mSubscriptionManagerProxy.getSubId(slotId);
- String newPackageName = mCarrierConfigManager.getConfigForSubId(subId).getString(
- CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null);
+ PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId);
+ if (config != null) {
+ String newPackageName = config.getString(
+ CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null);
+ maybeRebindService(slotId, newPackageName);
+ } else {
+ Log.w(TAG, "carrierConfigChanged: CarrierConfig is null!");
+ }
+ }
+
+ private void updateBoundCarrierServices(int slotId, String newPackageName) {
if (slotId > SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mNumSlots) {
String oldPackageName = mCarrierServices[slotId];
mCarrierServices[slotId] = newPackageName;
@@ -997,12 +1073,12 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
/**
* Schedules the processing of a completed query.
*/
- private void handleFeatureQueryComplete(ComponentName name,
+ private void handleFeaturesChanged(ComponentName name,
Set<ImsFeatureConfiguration.FeatureSlotPair> features) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = name;
args.arg2 = features;
- mHandler.obtainMessage(HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE, args).sendToTarget();
+ mHandler.obtainMessage(HANDLER_DYNAMIC_FEATURE_CHANGE, args).sendToTarget();
}
// Starts a dynamic query. Called from handler ONLY.
@@ -1022,7 +1098,7 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
Set<ImsFeatureConfiguration.FeatureSlotPair> features) {
ImsServiceInfo service = getImsServiceInfoFromCache(name.getPackageName());
if (service == null) {
- Log.w(TAG, "handleFeatureQueryComplete: Couldn't find cached info for name: "
+ Log.w(TAG, "handleFeaturesChanged: Couldn't find cached info for name: "
+ name);
return;
}
@@ -1047,7 +1123,7 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
public boolean isResolvingBinding() {
return mHandler.hasMessages(HANDLER_START_DYNAMIC_FEATURE_QUERY)
// We haven't processed this message yet, so it is still resolving.
- || mHandler.hasMessages(HANDLER_DYNAMIC_FEATURE_QUERY_COMPLETE)
+ || mHandler.hasMessages(HANDLER_DYNAMIC_FEATURE_CHANGE)
|| mFeatureQueryManager.isQueryInProgress();
}
diff --git a/com/android/internal/telephony/ims/ImsServiceController.java b/com/android/internal/telephony/ims/ImsServiceController.java
index e13a8ef6..e2a0f433 100644
--- a/com/android/internal/telephony/ims/ImsServiceController.java
+++ b/com/android/internal/telephony/ims/ImsServiceController.java
@@ -149,6 +149,16 @@ public class ImsServiceController {
}
}
+ private ImsService.Listener mFeatureChangedListener = new ImsService.Listener() {
+ @Override
+ public void onUpdateSupportedImsFeatures(ImsFeatureConfiguration c) {
+ if (mCallbacks == null) {
+ return;
+ }
+ mCallbacks.imsServiceFeaturesChanged(c, ImsServiceController.this);
+ }
+ };
+
/**
* Defines callbacks that are used by the ImsServiceController to notify when an ImsService
* has created or removed a new feature as well as the associated ImsServiceController.
@@ -162,6 +172,13 @@ public class ImsServiceController {
* Called by ImsServiceController when a new MMTEL or RCS feature has been removed.
*/
void imsServiceFeatureRemoved(int slotId, int feature, ImsServiceController controller);
+
+ /**
+ * Called by the ImsServiceController when the ImsService has notified the framework that
+ * its features have changed.
+ */
+ void imsServiceFeaturesChanged(ImsFeatureConfiguration config,
+ ImsServiceController controller);
}
/**
@@ -552,6 +569,7 @@ public class ImsServiceController {
synchronized (mLock) {
if (isServiceControllerAvailable()) {
Log.d(LOG_TAG, "notifyImsServiceReady");
+ mIImsServiceController.setListener(mFeatureChangedListener);
mIImsServiceController.notifyImsServiceReadyForFeatureCreation();
}
}
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
index ec5a6a00..87e51ecb 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneMmiCode.java
@@ -1277,10 +1277,18 @@ public final class ImsPhoneMmiCode extends Handler implements MmiCode {
sb.append(mContext.getText(
com.android.internal.R.string.serviceEnabled));
}
+ // Record CLIR setting
+ if (mSc.equals(SC_CLIR)) {
+ mPhone.saveClirSetting(CommandsInterface.CLIR_INVOCATION);
+ }
} else if (isDeactivate()) {
mState = State.COMPLETE;
sb.append(mContext.getText(
com.android.internal.R.string.serviceDisabled));
+ // Record CLIR setting
+ if (mSc.equals(SC_CLIR)) {
+ mPhone.saveClirSetting(CommandsInterface.CLIR_SUPPRESSION);
+ }
} else if (isRegister()) {
mState = State.COMPLETE;
sb.append(mContext.getText(
diff --git a/com/android/internal/telephony/metrics/TelephonyMetrics.java b/com/android/internal/telephony/metrics/TelephonyMetrics.java
index 116fdaee..a436d25c 100644
--- a/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -1835,12 +1835,18 @@ public class TelephonyMetrics {
final CarrierIdMatchingResult carrierIdMatchingResult = new CarrierIdMatchingResult();
if (cid != TelephonyManager.UNKNOWN_CARRIER_ID) {
+ // Successful matching event if result only has carrierId
carrierIdMatchingResult.carrierId = cid;
+ // Unknown Gid1 event if result only has carrierId, gid1 and mccmnc
if (gid1 != null) {
+ carrierIdMatchingResult.mccmnc = mccmnc;
carrierIdMatchingResult.gid1 = gid1;
}
} else {
- carrierIdMatchingResult.mccmnc = mccmnc;
+ // Unknown mccmnc event if result only has mccmnc
+ if (mccmnc != null) {
+ carrierIdMatchingResult.mccmnc = mccmnc;
+ }
}
carrierIdMatching.cidTableVersion = version;
diff --git a/com/android/internal/telephony/uicc/IccIoResult.java b/com/android/internal/telephony/uicc/IccIoResult.java
index 4a35e146..b1b6e753 100644
--- a/com/android/internal/telephony/uicc/IccIoResult.java
+++ b/com/android/internal/telephony/uicc/IccIoResult.java
@@ -16,6 +16,7 @@
package com.android.internal.telephony.uicc;
+import android.os.Build;
/**
* {@hide}
@@ -154,6 +155,12 @@ IccIoResult {
+ "CHV blocked"
+ "UNBLOCK CHV blocked";
case 0x50: return "increase cannot be performed, Max value reached";
+ // The definition for these status codes can be found in TS 31.102 7.3.1
+ case 0x62: return "authentication error, application specific";
+ case 0x64: return "authentication error, security context not supported";
+ case 0x65: return "key freshness failure";
+ case 0x66: return "authentication error, no memory space available";
+ case 0x67: return "authentication error, no memory space available in EF_MUK";
}
break;
case 0x9E: return null; // success
@@ -181,7 +188,9 @@ IccIoResult {
@Override
public String toString() {
return "IccIoResult sw1:0x" + Integer.toHexString(sw1) + " sw2:0x"
- + Integer.toHexString(sw2) + ((!success()) ? " Error: " + getErrorString() : "");
+ + Integer.toHexString(sw2) + " Payload: "
+ + ((Build.IS_DEBUGGABLE && Build.IS_ENG) ? payload : "*******")
+ + ((!success()) ? " Error: " + getErrorString() : "");
}
/**
diff --git a/com/android/internal/telephony/uicc/UiccProfile.java b/com/android/internal/telephony/uicc/UiccProfile.java
index 536cc388..d14e58c4 100644
--- a/com/android/internal/telephony/uicc/UiccProfile.java
+++ b/com/android/internal/telephony/uicc/UiccProfile.java
@@ -1464,21 +1464,7 @@ public class UiccProfile extends IccCard {
return null;
}
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
- String brandName = sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
- if (brandName == null) {
- // Check if CarrierConfig sets carrier name
- CarrierConfigManager manager = (CarrierConfigManager)
- mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
- int subId = SubscriptionController.getInstance().getSubIdUsingPhoneId(mPhoneId);
- if (manager != null) {
- PersistableBundle bundle = manager.getConfigForSubId(subId);
- if (bundle != null && bundle.getBoolean(
- CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL)) {
- brandName = bundle.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING);
- }
- }
- }
- return brandName;
+ return sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
}
/**
diff --git a/com/android/internal/telephony/uicc/UiccSlot.java b/com/android/internal/telephony/uicc/UiccSlot.java
index 004d3b19..a0fefe33 100644
--- a/com/android/internal/telephony/uicc/UiccSlot.java
+++ b/com/android/internal/telephony/uicc/UiccSlot.java
@@ -76,9 +76,11 @@ public class UiccSlot extends Handler {
* Update slot. The main trigger for this is a change in the ICC Card status.
*/
public void update(CommandsInterface ci, IccCardStatus ics, int phoneId) {
+ if (DBG) log("cardStatus update: " + ics.toString());
synchronized (mLock) {
CardState oldState = mCardState;
mCardState = ics.mCardState;
+ mIccId = ics.iccid;
mPhoneId = phoneId;
parseAtr(ics.atr);
mCi = ci;
@@ -104,8 +106,12 @@ public class UiccSlot extends Handler {
mUiccCard.dispose();
mUiccCard = null;
}
- } else if ((oldState == null || oldState == CardState.CARDSTATE_ABSENT)
- && mCardState != CardState.CARDSTATE_ABSENT) {
+ // Because mUiccCard may be updated in both IccCardStatus and IccSlotStatus, we need to
+ // create a new UiccCard instance in two scenarios:
+ // 1. mCardState is changing from ABSENT to non ABSENT.
+ // 2. The latest mCardState is not ABSENT, but there is no UiccCard instance.
+ } else if ((oldState == null || oldState == CardState.CARDSTATE_ABSENT
+ || mUiccCard == null) && mCardState != CardState.CARDSTATE_ABSENT) {
// No notifications while radio is off or we just powering up
if (radioState == RadioState.RADIO_ON && mLastRadioState == RadioState.RADIO_ON) {
if (DBG) log("update: notify card added");
@@ -136,7 +142,7 @@ public class UiccSlot extends Handler {
* Update slot based on IccSlotStatus.
*/
public void update(CommandsInterface ci, IccSlotStatus iss) {
- log("slotStatus update");
+ if (DBG) log("slotStatus update: " + iss.toString());
synchronized (mLock) {
mCi = ci;
if (iss.slotState == IccSlotStatus.SlotState.SLOTSTATE_INACTIVE) {
diff --git a/com/android/server/StatLogger.java b/com/android/internal/util/StatLogger.java
index d85810d3..1dac136c 100644
--- a/com/android/server/StatLogger.java
+++ b/com/android/internal/util/StatLogger.java
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package com.android.server;
+package com.android.internal.util;
import android.os.SystemClock;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.StatLoggerProto;
import com.android.server.StatLoggerProto.Event;
import java.io.PrintWriter;
@@ -30,8 +30,6 @@ import java.io.PrintWriter;
* Simple class to keep track of the number of times certain events happened and their durations for
* benchmarking.
*
- * TODO Update shortcut service to switch to it.
- *
* @hide
*/
public class StatLogger {
@@ -47,12 +45,35 @@ public class StatLogger {
@GuardedBy("mLock")
private final long[] mDurationStats;
+ @GuardedBy("mLock")
+ private final int[] mCallsPerSecond;
+
+ @GuardedBy("mLock")
+ private final long[] mDurationPerSecond;
+
+ @GuardedBy("mLock")
+ private final int[] mMaxCallsPerSecond;
+
+ @GuardedBy("mLock")
+ private final long[] mMaxDurationPerSecond;
+
+ @GuardedBy("mLock")
+ private final long[] mMaxDurationStats;
+
+ @GuardedBy("mLock")
+ private long mNextTickTime = SystemClock.elapsedRealtime() + 1000;
+
private final String[] mLabels;
public StatLogger(String[] eventLabels) {
SIZE = eventLabels.length;
mCountStats = new int[SIZE];
mDurationStats = new long[SIZE];
+ mCallsPerSecond = new int[SIZE];
+ mMaxCallsPerSecond = new int[SIZE];
+ mDurationPerSecond = new long[SIZE];
+ mMaxDurationPerSecond = new long[SIZE];
+ mMaxDurationStats = new long[SIZE];
mLabels = eventLabels;
}
@@ -67,19 +88,46 @@ public class StatLogger {
/**
* @see {@link #getTime()}
+ *
+ * @return the duration in microseconds.
*/
- public void logDurationStat(int eventId, long start) {
+ public long logDurationStat(int eventId, long start) {
synchronized (mLock) {
+ final long duration = getTime() - start;
if (eventId >= 0 && eventId < SIZE) {
mCountStats[eventId]++;
- mDurationStats[eventId] += (getTime() - start);
+ mDurationStats[eventId] += duration;
} else {
Slog.wtf(TAG, "Invalid event ID: " + eventId);
+ return duration;
+ }
+ if (mMaxDurationStats[eventId] < duration) {
+ mMaxDurationStats[eventId] = duration;
+ }
+
+ // Keep track of the per-second max.
+ final long nowRealtime = SystemClock.elapsedRealtime();
+ if (nowRealtime > mNextTickTime) {
+ if (mMaxCallsPerSecond[eventId] < mCallsPerSecond[eventId]) {
+ mMaxCallsPerSecond[eventId] = mCallsPerSecond[eventId];
+ }
+ if (mMaxDurationPerSecond[eventId] < mDurationPerSecond[eventId]) {
+ mMaxDurationPerSecond[eventId] = mDurationPerSecond[eventId];
+ }
+
+ mCallsPerSecond[eventId] = 0;
+ mDurationPerSecond[eventId] = 0;
+
+ mNextTickTime = nowRealtime + 1000;
}
+
+ mCallsPerSecond[eventId]++;
+ mDurationPerSecond[eventId] += duration;
+
+ return duration;
}
}
- @Deprecated
public void dump(PrintWriter pw, String prefix) {
dump(new IndentingPrintWriter(pw, " ").setIndent(prefix));
}
@@ -91,9 +139,14 @@ public class StatLogger {
for (int i = 0; i < SIZE; i++) {
final int count = mCountStats[i];
final double durationMs = mDurationStats[i] / 1000.0;
- pw.println(String.format("%s: count=%d, total=%.1fms, avg=%.3fms",
+
+ pw.println(String.format(
+ "%s: count=%d, total=%.1fms, avg=%.3fms, max calls/s=%d max dur/s=%.1fms"
+ + " max time=%.1fms",
mLabels[i], count, durationMs,
- (count == 0 ? 0 : ((double) durationMs) / count)));
+ (count == 0 ? 0 : durationMs / count),
+ mMaxCallsPerSecond[i], mMaxDurationPerSecond[i] / 1000.0,
+ mMaxDurationStats[i] / 1000.0));
}
pw.decreaseIndent();
}
diff --git a/com/android/internal/widget/FloatingToolbar.java b/com/android/internal/widget/FloatingToolbar.java
index 35aae15a..2ce5a0be 100644
--- a/com/android/internal/widget/FloatingToolbar.java
+++ b/com/android/internal/widget/FloatingToolbar.java
@@ -1706,6 +1706,7 @@ public final class FloatingToolbar {
contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
contentContainer.setTag(FLOATING_TOOLBAR_TAG);
+ contentContainer.setClipToOutline(true);
return contentContainer;
}
diff --git a/com/android/internal/widget/MessagingGroup.java b/com/android/internal/widget/MessagingGroup.java
index 239beaa2..07d78fe2 100644
--- a/com/android/internal/widget/MessagingGroup.java
+++ b/com/android/internal/widget/MessagingGroup.java
@@ -20,7 +20,7 @@ import android.annotation.AttrRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StyleRes;
-import android.app.Notification;
+import android.app.Person;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
@@ -63,8 +63,8 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
private boolean mFirstLayout;
private boolean mIsHidingAnimated;
private boolean mNeedsGeneratedAvatar;
- private Notification.Person mSender;
- private boolean mAvatarsAtEnd;
+ private Person mSender;
+ private boolean mImagesAtEnd;
private ViewGroup mImageContainer;
private MessagingImageMessage mIsolatedMessage;
private boolean mTransformingImages;
@@ -126,7 +126,7 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
return position;
}
- public void setSender(Notification.Person sender, CharSequence nameOverride) {
+ public void setSender(Person sender, CharSequence nameOverride) {
mSender = sender;
if (nameOverride == null) {
nameOverride = sender.getName();
@@ -342,7 +342,7 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
mAddedMessages.add(message);
}
boolean isImage = message instanceof MessagingImageMessage;
- if (mAvatarsAtEnd && isImage) {
+ if (mImagesAtEnd && isImage) {
isolatedMessage = (MessagingImageMessage) message;
} else {
if (removeFromParentIfDifferent(message, mMessageContainer)) {
@@ -466,7 +466,7 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
return mNeedsGeneratedAvatar;
}
- public Notification.Person getSender() {
+ public Person getSender() {
return mSender;
}
@@ -474,9 +474,9 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
mTransformingImages = transformingImages;
}
- public void setDisplayAvatarsAtEnd(boolean atEnd) {
- if (mAvatarsAtEnd != atEnd) {
- mAvatarsAtEnd = atEnd;
+ public void setDisplayImagesAtEnd(boolean atEnd) {
+ if (mImagesAtEnd != atEnd) {
+ mImagesAtEnd = atEnd;
mImageContainer.setVisibility(atEnd ? View.VISIBLE : View.GONE);
}
}
diff --git a/com/android/internal/widget/MessagingLayout.java b/com/android/internal/widget/MessagingLayout.java
index 5279636a..f8236c78 100644
--- a/com/android/internal/widget/MessagingLayout.java
+++ b/com/android/internal/widget/MessagingLayout.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StyleRes;
import android.app.Notification;
+import android.app.Person;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -79,9 +80,9 @@ public class MessagingLayout extends FrameLayout {
private Icon mLargeIcon;
private boolean mIsOneToOne;
private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
- private Notification.Person mUser;
+ private Person mUser;
private CharSequence mNameReplacement;
- private boolean mIsCollapsed;
+ private boolean mDisplayImagesAtEnd;
public MessagingLayout(@NonNull Context context) {
super(context);
@@ -128,8 +129,8 @@ public class MessagingLayout extends FrameLayout {
}
@RemotableViewMethod
- public void setIsCollapsed(boolean isCollapsed) {
- mIsCollapsed = isCollapsed;
+ public void setDisplayImagesAtEnd(boolean atEnd) {
+ mDisplayImagesAtEnd = atEnd;
}
@RemotableViewMethod
@@ -160,7 +161,7 @@ public class MessagingLayout extends FrameLayout {
for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
CharSequence message = remoteInputHistory[i];
newMessages.add(new Notification.MessagingStyle.Message(
- message, 0, (Notification.Person) null));
+ message, 0, (Person) null));
}
}
@@ -296,13 +297,13 @@ public class MessagingLayout extends FrameLayout {
mIsOneToOne = oneToOne;
}
- public void setUser(Notification.Person user) {
+ public void setUser(Person user) {
mUser = user;
if (mUser.getIcon() == null) {
Icon userIcon = Icon.createWithResource(getContext(),
com.android.internal.R.drawable.messaging_user);
userIcon.setTint(mLayoutColor);
- mUser.setIcon(userIcon);
+ mUser = mUser.toBuilder().setIcon(userIcon).build();
}
}
@@ -310,7 +311,7 @@ public class MessagingLayout extends FrameLayout {
List<MessagingMessage> messages) {
// Let's first find our groups!
List<List<MessagingMessage>> groups = new ArrayList<>();
- List<Notification.Person> senders = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
// Lets first find the groups
findGroups(historicMessages, messages, groups, senders);
@@ -320,7 +321,7 @@ public class MessagingLayout extends FrameLayout {
}
private void createGroupViews(List<List<MessagingMessage>> groups,
- List<Notification.Person> senders) {
+ List<Person> senders) {
mGroups.clear();
for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
List<MessagingMessage> group = groups.get(groupIndex);
@@ -337,9 +338,9 @@ public class MessagingLayout extends FrameLayout {
newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
mAddedGroups.add(newGroup);
}
- newGroup.setDisplayAvatarsAtEnd(mIsCollapsed);
+ newGroup.setDisplayImagesAtEnd(mDisplayImagesAtEnd);
newGroup.setLayoutColor(mLayoutColor);
- Notification.Person sender = senders.get(groupIndex);
+ Person sender = senders.get(groupIndex);
CharSequence nameOverride = null;
if (sender != mUser && mNameReplacement != null) {
nameOverride = mNameReplacement;
@@ -357,7 +358,7 @@ public class MessagingLayout extends FrameLayout {
private void findGroups(List<MessagingMessage> historicMessages,
List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
- List<Notification.Person> senders) {
+ List<Person> senders) {
CharSequence currentSenderKey = null;
List<MessagingMessage> currentGroup = null;
int histSize = historicMessages.size();
@@ -369,7 +370,7 @@ public class MessagingLayout extends FrameLayout {
message = messages.get(i - histSize);
}
boolean isNewGroup = currentGroup == null;
- Notification.Person sender = message.getMessage().getSenderPerson();
+ Person sender = message.getMessage().getSenderPerson();
CharSequence key = sender == null ? null
: sender.getKey() == null ? sender.getName() : sender.getKey();
isNewGroup |= !TextUtils.equals(key, currentSenderKey);
diff --git a/com/android/internal/widget/MessagingMessage.java b/com/android/internal/widget/MessagingMessage.java
index bf1c5ca7..a2cc7cfb 100644
--- a/com/android/internal/widget/MessagingMessage.java
+++ b/com/android/internal/widget/MessagingMessage.java
@@ -16,6 +16,7 @@
package com.android.internal.widget;
+import android.app.ActivityManager;
import android.app.Notification;
import android.view.View;
@@ -33,7 +34,7 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild {
static MessagingMessage createMessage(MessagingLayout layout,
Notification.MessagingStyle.Message m) {
- if (hasImage(m)) {
+ if (hasImage(m) && !ActivityManager.isLowRamDeviceStatic()) {
return MessagingImageMessage.createMessage(layout, m);
} else {
return MessagingTextMessage.createMessage(layout, m);
diff --git a/com/android/keyguard/KeyguardAbsKeyInputView.java b/com/android/keyguard/KeyguardAbsKeyInputView.java
index d63ad084..00cd5a7b 100644
--- a/com/android/keyguard/KeyguardAbsKeyInputView.java
+++ b/com/android/keyguard/KeyguardAbsKeyInputView.java
@@ -265,11 +265,11 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout
mPendingLockCheck.cancel(false);
mPendingLockCheck = null;
}
+ reset();
}
@Override
public void onResume(int reason) {
- reset();
}
@Override
diff --git a/com/android/keyguard/KeyguardPINView.java b/com/android/keyguard/KeyguardPINView.java
index c1cff9e8..adb24601 100644
--- a/com/android/keyguard/KeyguardPINView.java
+++ b/com/android/keyguard/KeyguardPINView.java
@@ -107,6 +107,13 @@ public class KeyguardPINView extends KeyguardPinBasedInputView {
new View[]{
null, mEcaView, null
}};
+
+ View cancelBtn = findViewById(R.id.cancel_button);
+ if (cancelBtn != null) {
+ cancelBtn.setOnClickListener(view -> {
+ mCallback.reset();
+ });
+ }
}
@Override
diff --git a/com/android/keyguard/KeyguardPasswordView.java b/com/android/keyguard/KeyguardPasswordView.java
index 75c52d8e..7cc37c47 100644
--- a/com/android/keyguard/KeyguardPasswordView.java
+++ b/com/android/keyguard/KeyguardPasswordView.java
@@ -205,6 +205,13 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView
}
});
+ View cancelBtn = findViewById(R.id.cancel_button);
+ if (cancelBtn != null) {
+ cancelBtn.setOnClickListener(view -> {
+ mCallback.reset();
+ });
+ }
+
// If there's more than one IME, enable the IME switcher button
updateSwitchImeButton();
diff --git a/com/android/keyguard/KeyguardPatternView.java b/com/android/keyguard/KeyguardPatternView.java
index 651831ee..174dcaba 100644
--- a/com/android/keyguard/KeyguardPatternView.java
+++ b/com/android/keyguard/KeyguardPatternView.java
@@ -157,6 +157,13 @@ public class KeyguardPatternView extends LinearLayout implements KeyguardSecurit
if (button != null) {
button.setCallback(this);
}
+
+ View cancelBtn = findViewById(R.id.cancel_button);
+ if (cancelBtn != null) {
+ cancelBtn.setOnClickListener(view -> {
+ mCallback.reset();
+ });
+ }
}
@Override
diff --git a/com/android/keyguard/KeyguardSimPinView.java b/com/android/keyguard/KeyguardSimPinView.java
index c71c433b..42c7a568 100644
--- a/com/android/keyguard/KeyguardSimPinView.java
+++ b/com/android/keyguard/KeyguardSimPinView.java
@@ -78,6 +78,11 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
}
break;
}
+ case READY: {
+ mRemainingAttempts = -1;
+ resetState();
+ break;
+ }
default:
resetState();
}
diff --git a/com/android/keyguard/KeyguardStatusView.java b/com/android/keyguard/KeyguardStatusView.java
index ce16efbb..ff3af17b 100644
--- a/com/android/keyguard/KeyguardStatusView.java
+++ b/com/android/keyguard/KeyguardStatusView.java
@@ -216,8 +216,7 @@ public class KeyguardStatusView extends GridLayout {
}
public void refreshTime() {
- mClockView.setFormat12Hour(Patterns.clockView12);
- mClockView.setFormat24Hour(Patterns.clockView24);
+ mClockView.refresh();
}
private void refresh() {
diff --git a/com/android/providers/settings/SettingsBackupAgent.java b/com/android/providers/settings/SettingsBackupAgent.java
index 313f73f8..f7286843 100644
--- a/com/android/providers/settings/SettingsBackupAgent.java
+++ b/com/android/providers/settings/SettingsBackupAgent.java
@@ -253,6 +253,7 @@ public class SettingsBackupAgent extends BackupAgentHelper {
&& !RESTORE_FROM_HIGHER_SDK_INT_SUPPORTED_KEYS.contains(key)) {
Log.w(TAG, "Not restoring unrecognized key '"
+ key + "' from future version " + appVersionCode);
+ data.skipEntityData();
continue;
}
diff --git a/com/android/providers/settings/SettingsHelper.java b/com/android/providers/settings/SettingsHelper.java
index ad422d80..4c98bb8c 100644
--- a/com/android/providers/settings/SettingsHelper.java
+++ b/com/android/providers/settings/SettingsHelper.java
@@ -144,10 +144,7 @@ public class SettingsHelper {
}
try {
- if (Settings.System.SCREEN_BRIGHTNESS.equals(name)) {
- setBrightness(Integer.parseInt(value));
- // fall through to the ordinary write to settings
- } else if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
+ if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
setSoundEffects(Integer.parseInt(value) == 1);
// fall through to the ordinary write to settings
} else if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
@@ -305,10 +302,6 @@ public class SettingsHelper {
}
}
- private void setBrightness(int brightness) {
- mContext.getSystemService(DisplayManager.class).setTemporaryBrightness(brightness);
- }
-
/* package */ byte[] getLocaleData() {
Configuration conf = mContext.getResources().getConfiguration();
return conf.getLocales().toLanguageTags().getBytes();
diff --git a/com/android/providers/settings/SettingsProtoDumpUtil.java b/com/android/providers/settings/SettingsProtoDumpUtil.java
index f43e719d..a2263b4c 100644
--- a/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -742,6 +742,9 @@ class SettingsProtoDumpUtil {
dumpSetting(s, p,
Settings.Global.LOCATION_GLOBAL_KILL_SWITCH,
GlobalSettingsProto.Location.GLOBAL_KILL_SWITCH);
+ dumpSetting(s, p,
+ Settings.Global.GNSS_SATELLITE_BLACKLIST,
+ GlobalSettingsProto.Location.GNSS_SATELLITE_BLACKLIST);
p.end(locationToken);
final long lpmToken = p.start(GlobalSettingsProto.LOW_POWER_MODE);
@@ -2019,6 +2022,10 @@ class SettingsProtoDumpUtil {
SecureSettingsProto.Rotation.NUM_ROTATION_SUGGESTIONS_ACCEPTED);
p.end(rotationToken);
+ dumpSetting(s, p,
+ Settings.Secure.RTT_CALLING_MODE,
+ SecureSettingsProto.RTT_CALLING_MODE);
+
final long screensaverToken = p.start(SecureSettingsProto.SCREENSAVER);
dumpSetting(s, p,
Settings.Secure.SCREENSAVER_ENABLED,
@@ -2224,6 +2231,12 @@ class SettingsProtoDumpUtil {
Settings.Secure.WAKE_GESTURE_ENABLED,
SecureSettingsProto.WAKE_GESTURE_ENABLED);
+ final long launcherToken = p.start(SecureSettingsProto.LAUNCHER);
+ dumpSetting(s, p,
+ Settings.Secure.SWIPE_UP_TO_SWITCH_APPS_ENABLED,
+ SecureSettingsProto.Launcher.SWIPE_UP_TO_SWITCH_APPS_ENABLED);
+ p.end(launcherToken);
+
// Please insert new settings using the same order as in SecureSettingsProto.
p.end(token);
@@ -2402,10 +2415,6 @@ class SettingsProtoDumpUtil {
SystemSettingsProto.Rotation.HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY);
p.end(rotationToken);
- dumpSetting(s, p,
- Settings.System.RTT_CALLING_MODE,
- SystemSettingsProto.RTT_CALLING_MODE);
-
final long screenToken = p.start(SystemSettingsProto.SCREEN);
dumpSetting(s, p,
Settings.System.SCREEN_OFF_TIMEOUT,
diff --git a/com/android/providers/settings/SettingsProvider.java b/com/android/providers/settings/SettingsProvider.java
index a6d6250d..022e3069 100644
--- a/com/android/providers/settings/SettingsProvider.java
+++ b/com/android/providers/settings/SettingsProvider.java
@@ -2935,7 +2935,7 @@ public class SettingsProvider extends ContentProvider {
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 162;
+ private static final int SETTINGS_VERSION = 163;
private final int mUserId;
@@ -3709,6 +3709,21 @@ public class SettingsProvider extends ContentProvider {
currentVersion = 162;
}
+ if (currentVersion == 162) {
+ // Version 162: Add a gesture for silencing phones
+ final SettingsState settings = getGlobalSettingsLocked();
+ final Setting currentSetting = settings.getSettingLocked(
+ Global.SHOW_ZEN_UPGRADE_NOTIFICATION);
+ if (!currentSetting.isNull()
+ && TextUtils.equals("0", currentSetting.getValue())) {
+ settings.insertSettingLocked(
+ Global.SHOW_ZEN_UPGRADE_NOTIFICATION, "1",
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+
+ currentVersion = 163;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
diff --git a/com/android/server/AlarmManagerService.java b/com/android/server/AlarmManagerService.java
index 8ce4e64b..6f4ae15b 100644
--- a/com/android/server/AlarmManagerService.java
+++ b/com/android/server/AlarmManagerService.java
@@ -88,6 +88,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.LocalLog;
+import com.android.internal.util.StatLogger;
import com.android.server.AppStateTracker.Listener;
import java.io.ByteArrayOutputStream;
diff --git a/com/android/server/AppStateTracker.java b/com/android/server/AppStateTracker.java
index cec4f1a0..23c57797 100644
--- a/com/android/server/AppStateTracker.java
+++ b/com/android/server/AppStateTracker.java
@@ -55,6 +55,7 @@ import com.android.internal.app.IAppOpsService;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
+import com.android.internal.util.StatLogger;
import com.android.server.ForceAppStandbyTrackerProto.ExemptedPackage;
import com.android.server.ForceAppStandbyTrackerProto.RunAnyInBackgroundRestrictedPackages;
diff --git a/com/android/server/ConnectivityService.java b/com/android/server/ConnectivityService.java
index a1ef1ed9..6463bed6 100644
--- a/com/android/server/ConnectivityService.java
+++ b/com/android/server/ConnectivityService.java
@@ -52,6 +52,8 @@ import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.PacketKeepalive;
import android.net.IConnectivityManager;
+import android.net.IIpConnectivityMetrics;
+import android.net.INetdEventCallback;
import android.net.INetworkManagementEventObserver;
import android.net.INetworkPolicyListener;
import android.net.INetworkPolicyManager;
@@ -140,6 +142,7 @@ import com.android.server.am.BatteryStatsService;
import com.android.server.connectivity.DataConnectionStats;
import com.android.server.connectivity.DnsManager;
import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
+import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.IpConnectivityMetrics;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
@@ -155,6 +158,7 @@ import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.Tethering;
import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.tethering.TetheringDependencies;
+import com.android.server.net.BaseNetdEventCallback;
import com.android.server.net.BaseNetworkObserver;
import com.android.server.net.LockdownVpnTracker;
import com.android.server.net.NetworkPolicyManagerInternal;
@@ -256,6 +260,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
private INetworkStatsService mStatsService;
private INetworkPolicyManager mPolicyManager;
private NetworkPolicyManagerInternal mPolicyManagerInternal;
+ private IIpConnectivityMetrics mIpConnectivityMetrics;
private String mCurrentTcpBufferSizes;
@@ -414,6 +419,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
// Handle changes in Private DNS settings.
private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37;
+ // Handle private DNS validation status updates.
+ private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
+
private static String eventName(int what) {
return sMagicDecoderRing.get(what, Integer.toString(what));
}
@@ -934,7 +942,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
// Used only for testing.
// TODO: Delete this and either:
- // 1. Give Fake SettingsProvider the ability to send settings change notifications (requires
+ // 1. Give FakeSettingsProvider the ability to send settings change notifications (requires
// changing ContentResolver to make registerContentObserver non-final).
// 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
// by subclassing SettingsObserver.
@@ -943,6 +951,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
}
+ // See FakeSettingsProvider comment above.
+ @VisibleForTesting
+ void updatePrivateDnsSettings() {
+ mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+ }
+
private void handleMobileDataAlwaysOn() {
final boolean enable = toBool(Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 1));
@@ -972,8 +986,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
private void registerPrivateDnsSettingsCallbacks() {
- for (Uri u : DnsManager.getPrivateDnsSettingsUris()) {
- mSettingsObserver.observe(u, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+ for (Uri uri : DnsManager.getPrivateDnsSettingsUris()) {
+ mSettingsObserver.observe(uri, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
}
}
@@ -1026,8 +1040,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
if (network == null) {
return null;
}
+ return getNetworkAgentInfoForNetId(network.netId);
+ }
+
+ private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) {
synchronized (mNetworkForNetId) {
- return mNetworkForNetId.get(network.netId);
+ return mNetworkForNetId.get(netId);
}
}
@@ -1167,9 +1185,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
NetworkAgentInfo nai;
if (vpnNetId != NETID_UNSET) {
- synchronized (mNetworkForNetId) {
- nai = mNetworkForNetId.get(vpnNetId);
- }
+ nai = getNetworkAgentInfoForNetId(vpnNetId);
if (nai != null) return nai.network;
}
nai = getDefaultNetwork();
@@ -1545,6 +1561,41 @@ public class ConnectivityService extends IConnectivityManager.Stub
return true;
}
+ @VisibleForTesting
+ protected final INetdEventCallback mNetdEventCallback = new BaseNetdEventCallback() {
+ @Override
+ public void onPrivateDnsValidationEvent(int netId, String ipAddress,
+ String hostname, boolean validated) {
+ try {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_PRIVATE_DNS_VALIDATION_UPDATE,
+ new PrivateDnsValidationUpdate(netId,
+ InetAddress.parseNumericAddress(ipAddress),
+ hostname, validated)));
+ } catch (IllegalArgumentException e) {
+ loge("Error parsing ip address in validation event");
+ }
+ }
+ };
+
+ @VisibleForTesting
+ protected void registerNetdEventCallback() {
+ mIpConnectivityMetrics =
+ (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface(
+ ServiceManager.getService(IpConnectivityLog.SERVICE_NAME));
+ if (mIpConnectivityMetrics == null) {
+ Slog.wtf(TAG, "Missing IIpConnectivityMetrics");
+ }
+
+ try {
+ mIpConnectivityMetrics.addNetdEventCallback(
+ INetdEventCallback.CALLBACK_CALLER_CONNECTIVITY_SERVICE,
+ mNetdEventCallback);
+ } catch (Exception e) {
+ loge("Error registering netd callback: " + e);
+ }
+ }
+
private final INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() {
@Override
public void onUidRulesChanged(int uid, int uidRules) {
@@ -1730,6 +1781,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
void systemReady() {
loadGlobalProxy();
+ registerNetdEventCallback();
synchronized (this) {
mSystemReady = true;
@@ -2155,41 +2207,21 @@ public class ConnectivityService extends IConnectivityManager.Stub
default:
return false;
case NetworkMonitor.EVENT_NETWORK_TESTED: {
- final NetworkAgentInfo nai;
- synchronized (mNetworkForNetId) {
- nai = mNetworkForNetId.get(msg.arg2);
- }
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
if (nai == null) break;
final boolean valid = (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
final boolean wasValidated = nai.lastValidated;
final boolean wasDefault = isDefaultNetwork(nai);
- final PrivateDnsConfig privateDnsCfg = (msg.obj instanceof PrivateDnsConfig)
- ? (PrivateDnsConfig) msg.obj : null;
final String redirectUrl = (msg.obj instanceof String) ? (String) msg.obj : "";
- final boolean reevaluationRequired;
- final String logMsg;
- if (valid) {
- reevaluationRequired = updatePrivateDns(nai, privateDnsCfg);
- logMsg = (DBG && (privateDnsCfg != null))
- ? " with " + privateDnsCfg.toString() : "";
- } else {
- reevaluationRequired = false;
- logMsg = (DBG && !TextUtils.isEmpty(redirectUrl))
- ? " with redirect to " + redirectUrl : "";
- }
if (DBG) {
+ final String logMsg = !TextUtils.isEmpty(redirectUrl)
+ ? " with redirect to " + redirectUrl
+ : "";
log(nai.name() + " validation " + (valid ? "passed" : "failed") + logMsg);
}
- // If there is a change in Private DNS configuration,
- // trigger reevaluation of the network to test it.
- if (reevaluationRequired) {
- nai.networkMonitor.sendMessage(
- NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
- break;
- }
if (valid != nai.lastValidated) {
if (wasDefault) {
metricsLogger().defaultNetworkMetrics().logDefaultNetworkValidity(
@@ -2218,10 +2250,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
case NetworkMonitor.EVENT_PROVISIONING_NOTIFICATION: {
final int netId = msg.arg2;
final boolean visible = toBool(msg.arg1);
- final NetworkAgentInfo nai;
- synchronized (mNetworkForNetId) {
- nai = mNetworkForNetId.get(netId);
- }
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
// If captive portal status has changed, update capabilities or disconnect.
if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
final int oldScore = nai.getCurrentScore();
@@ -2252,18 +2281,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
break;
}
case NetworkMonitor.EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
- final NetworkAgentInfo nai;
- synchronized (mNetworkForNetId) {
- nai = mNetworkForNetId.get(msg.arg2);
- }
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
if (nai == null) break;
- final PrivateDnsConfig cfg = (PrivateDnsConfig) msg.obj;
- final boolean reevaluationRequired = updatePrivateDns(nai, cfg);
- if (nai.lastValidated && reevaluationRequired) {
- nai.networkMonitor.sendMessage(
- NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
- }
+ updatePrivateDns(nai, (PrivateDnsConfig) msg.obj);
break;
}
}
@@ -2301,61 +2322,50 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
+ private boolean networkRequiresValidation(NetworkAgentInfo nai) {
+ return NetworkMonitor.isValidationRequired(
+ mDefaultRequest.networkCapabilities, nai.networkCapabilities);
+ }
+
private void handlePrivateDnsSettingsChanged() {
final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
- // Private DNS only ever applies to networks that might provide
- // Internet access and therefore also require validation.
- if (!NetworkMonitor.isValidationRequired(
- mDefaultRequest.networkCapabilities, nai.networkCapabilities)) {
- continue;
- }
-
- // Notify the NetworkMonitor thread in case it needs to cancel or
- // schedule DNS resolutions. If a DNS resolution is required the
- // result will be sent back to us.
- nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);
-
- if (!cfg.inStrictMode()) {
- // No strict mode hostname DNS resolution needed, so just update
- // DNS settings directly. In opportunistic and "off" modes this
- // just reprograms netd with the network-supplied DNS servers
- // (and of course the boolean of whether or not to attempt TLS).
- //
- // TODO: Consider code flow parity with strict mode, i.e. having
- // NetworkMonitor relay the PrivateDnsConfig back to us and then
- // performing this call at that time.
- updatePrivateDns(nai, cfg);
+ handlePerNetworkPrivateDnsConfig(nai, cfg);
+ if (networkRequiresValidation(nai)) {
+ handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
}
}
}
- private boolean updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
- final boolean reevaluationRequired = true;
- final boolean dontReevaluate = false;
+ private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
+ // Private DNS only ever applies to networks that might provide
+ // Internet access and therefore also require validation.
+ if (!networkRequiresValidation(nai)) return;
- final PrivateDnsConfig oldCfg = mDnsManager.updatePrivateDns(nai.network, newCfg);
- updateDnses(nai.linkProperties, null, nai.network.netId);
-
- if (newCfg == null) {
- if (oldCfg == null) return dontReevaluate;
- return oldCfg.useTls ? reevaluationRequired : dontReevaluate;
- }
+ // Notify the NetworkMonitor thread in case it needs to cancel or
+ // schedule DNS resolutions. If a DNS resolution is required the
+ // result will be sent back to us.
+ nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);
- if (oldCfg == null) {
- return newCfg.useTls ? reevaluationRequired : dontReevaluate;
- }
+ // With Private DNS bypass support, we can proceed to update the
+ // Private DNS config immediately, even if we're in strict mode
+ // and have not yet resolved the provider name into a set of IPs.
+ updatePrivateDns(nai, cfg);
+ }
- if (oldCfg.useTls != newCfg.useTls) {
- return reevaluationRequired;
- }
+ private void updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
+ mDnsManager.updatePrivateDns(nai.network, newCfg);
+ updateDnses(nai.linkProperties, null, nai.network.netId);
+ }
- if (newCfg.inStrictMode() && !Objects.equals(oldCfg.hostname, newCfg.hostname)) {
- return reevaluationRequired;
+ private void handlePrivateDnsValidationUpdate(PrivateDnsValidationUpdate update) {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetId(update.netId);
+ if (nai == null) {
+ return;
}
-
- return dontReevaluate;
+ mDnsManager.updatePrivateDnsValidation(update);
+ handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
}
private void updateLingerState(NetworkAgentInfo nai, long now) {
@@ -3048,6 +3058,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
case EVENT_PRIVATE_DNS_SETTINGS_CHANGED:
handlePrivateDnsSettingsChanged();
break;
+ case EVENT_PRIVATE_DNS_VALIDATION_UPDATE:
+ handlePrivateDnsValidationUpdate(
+ (PrivateDnsValidationUpdate) msg.obj);
+ break;
}
}
}
@@ -3300,7 +3314,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
if (isNetworkWithLinkPropertiesBlocked(lp, uid, false)) {
return;
}
- nai.networkMonitor.sendMessage(NetworkMonitor.CMD_FORCE_REEVALUATION, uid);
+ nai.networkMonitor.forceReevaluation(uid);
}
private ProxyInfo getDefaultProxy() {
@@ -4621,6 +4635,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
updateRoutes(newLp, oldLp, netId);
updateDnses(newLp, oldLp, netId);
+ // Make sure LinkProperties represents the latest private DNS status.
+ // This does not need to be done before updateDnses because the
+ // LinkProperties are not the source of the private DNS configuration.
+ // updateDnses will fetch the private DNS configuration from DnsManager.
+ mDnsManager.updatePrivateDnsStatus(netId, newLp);
// Start or stop clat accordingly to network state.
networkAgent.updateClat(mNetd);
@@ -4919,7 +4938,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
- if (mNetworkForNetId.get(nai.network.netId) != nai) {
+ if (getNetworkAgentInfoForNetId(nai.network.netId) != nai) {
// Ignore updates for disconnected networks
return;
}
@@ -5495,6 +5514,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
networkAgent.everConnected = true;
+ handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
updateLinkProperties(networkAgent, null);
notifyIfacesChangedForNetworkStats();
@@ -5911,4 +5931,4 @@ public class ConnectivityService extends IConnectivityManager.Stub
pw.println(" Get airplane mode.");
}
}
-} \ No newline at end of file
+}
diff --git a/com/android/server/InputMethodManagerService.java b/com/android/server/InputMethodManagerService.java
index 40f94760..f678eed8 100644
--- a/com/android/server/InputMethodManagerService.java
+++ b/com/android/server/InputMethodManagerService.java
@@ -263,17 +263,19 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
private static final class DebugFlag {
private static final Object LOCK = new Object();
private final String mKey;
+ private final boolean mDefaultValue;
@GuardedBy("LOCK")
private boolean mValue;
public DebugFlag(String key, boolean defaultValue) {
mKey = key;
+ mDefaultValue = defaultValue;
mValue = SystemProperties.getBoolean(key, defaultValue);
}
void refresh() {
synchronized (LOCK) {
- mValue = SystemProperties.getBoolean(mKey, true);
+ mValue = SystemProperties.getBoolean(mKey, mDefaultValue);
}
}
diff --git a/com/android/server/IpSecService.java b/com/android/server/IpSecService.java
index bde6bd8d..cd90e3f9 100644
--- a/com/android/server/IpSecService.java
+++ b/com/android/server/IpSecService.java
@@ -24,6 +24,8 @@ import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.SOCK_DGRAM;
import static com.android.internal.util.Preconditions.checkNotNull;
+import android.annotation.NonNull;
+import android.app.AppOpsManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IIpSecService;
@@ -42,6 +44,7 @@ import android.net.NetworkUtils;
import android.net.TrafficStats;
import android.net.util.NetdService;
import android.os.Binder;
+import android.os.DeadSystemException;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
@@ -974,6 +977,13 @@ public class IpSecService extends IIpSecService.Stub {
return service;
}
+ @NonNull
+ private AppOpsManager getAppOpsManager() {
+ AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+ if(appOps == null) throw new RuntimeException("System Server couldn't get AppOps");
+ return appOps;
+ }
+
/** @hide */
@VisibleForTesting
public IpSecService(Context context, IpSecServiceConfiguration config) {
@@ -1240,7 +1250,9 @@ public class IpSecService extends IIpSecService.Stub {
*/
@Override
public synchronized IpSecTunnelInterfaceResponse createTunnelInterface(
- String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder) {
+ String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder,
+ String callingPackage) {
+ enforceTunnelPermissions(callingPackage);
checkNotNull(binder, "Null Binder passed to createTunnelInterface");
checkNotNull(underlyingNetwork, "No underlying network was specified");
checkInetAddress(localAddr);
@@ -1320,8 +1332,8 @@ public class IpSecService extends IIpSecService.Stub {
*/
@Override
public synchronized void addAddressToTunnelInterface(
- int tunnelResourceId, LinkAddress localAddr) {
- enforceNetworkStackPermission();
+ int tunnelResourceId, LinkAddress localAddr, String callingPackage) {
+ enforceTunnelPermissions(callingPackage);
UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
// Get tunnelInterface record; if no such interface is found, will throw
@@ -1352,10 +1364,10 @@ public class IpSecService extends IIpSecService.Stub {
*/
@Override
public synchronized void removeAddressFromTunnelInterface(
- int tunnelResourceId, LinkAddress localAddr) {
- enforceNetworkStackPermission();
- UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ int tunnelResourceId, LinkAddress localAddr, String callingPackage) {
+ enforceTunnelPermissions(callingPackage);
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
// Get tunnelInterface record; if no such interface is found, will throw
// IllegalArgumentException
TunnelInterfaceRecord tunnelInterfaceInfo =
@@ -1383,7 +1395,9 @@ public class IpSecService extends IIpSecService.Stub {
* server
*/
@Override
- public synchronized void deleteTunnelInterface(int resourceId) throws RemoteException {
+ public synchronized void deleteTunnelInterface(
+ int resourceId, String callingPackage) throws RemoteException {
+ enforceTunnelPermissions(callingPackage);
UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
releaseResource(userRecord.mTunnelInterfaceRecords, resourceId);
}
@@ -1469,7 +1483,6 @@ public class IpSecService extends IIpSecService.Stub {
case IpSecTransform.MODE_TRANSPORT:
break;
case IpSecTransform.MODE_TUNNEL:
- enforceNetworkStackPermission();
break;
default:
throw new IllegalArgumentException(
@@ -1477,9 +1490,20 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private void enforceNetworkStackPermission() {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.NETWORK_STACK,
- "IpSecService");
+ private void enforceTunnelPermissions(String callingPackage) {
+ checkNotNull(callingPackage, "Null calling package cannot create IpSec tunnels");
+ switch (getAppOpsManager().noteOp(
+ AppOpsManager.OP_MANAGE_IPSEC_TUNNELS,
+ Binder.getCallingUid(), callingPackage)) {
+ case AppOpsManager.MODE_DEFAULT:
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService");
+ break;
+ case AppOpsManager.MODE_ALLOWED:
+ return;
+ default:
+ throw new SecurityException("Request to ignore AppOps for non-legacy API");
+ }
}
private void createOrUpdateTransform(
@@ -1535,8 +1559,12 @@ public class IpSecService extends IIpSecService.Stub {
* result in all of those sockets becoming unable to send or receive data.
*/
@Override
- public synchronized IpSecTransformResponse createTransform(IpSecConfig c, IBinder binder)
- throws RemoteException {
+ public synchronized IpSecTransformResponse createTransform(
+ IpSecConfig c, IBinder binder, String callingPackage) throws RemoteException {
+ checkNotNull(c);
+ if (c.getMode() == IpSecTransform.MODE_TUNNEL) {
+ enforceTunnelPermissions(callingPackage);
+ }
checkIpSecConfig(c);
checkNotNull(binder, "Null Binder passed to createTransform");
final int resourceId = mNextResourceId++;
@@ -1657,8 +1685,9 @@ public class IpSecService extends IIpSecService.Stub {
*/
@Override
public synchronized void applyTunnelModeTransform(
- int tunnelResourceId, int direction, int transformResourceId) throws RemoteException {
- enforceNetworkStackPermission();
+ int tunnelResourceId, int direction,
+ int transformResourceId, String callingPackage) throws RemoteException {
+ enforceTunnelPermissions(callingPackage);
checkDirection(direction);
UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
diff --git a/com/android/server/LocationManagerService.java b/com/android/server/LocationManagerService.java
index 26b83f5a..fb5fba0d 100644
--- a/com/android/server/LocationManagerService.java
+++ b/com/android/server/LocationManagerService.java
@@ -1344,15 +1344,28 @@ public class LocationManagerService extends ILocationManager.Stub {
* @param provider the name of the location provider
*/
private boolean isAllowedByCurrentUserSettingsLocked(String provider) {
+ return isAllowedByUserSettingsLockedForUser(provider, mCurrentUserId);
+ }
+
+ /**
+ * Returns "true" if access to the specified location provider is allowed by the specified
+ * user's settings. Access to all location providers is forbidden to non-location-provider
+ * processes belonging to background users.
+ *
+ * @param provider the name of the location provider
+ * @param userId the user id to query
+ */
+ private boolean isAllowedByUserSettingsLockedForUser(String provider, int userId) {
if (mEnabledProviders.contains(provider)) {
return true;
}
if (mDisabledProviders.contains(provider)) {
return false;
}
- return isLocationProviderEnabledForUser(provider, mCurrentUserId);
+ return isLocationProviderEnabledForUser(provider, userId);
}
+
/**
* Returns "true" if access to the specified location provider is allowed by the specified
* user's settings. Access to all location providers is forbidden to non-location-provider
@@ -1360,12 +1373,13 @@ public class LocationManagerService extends ILocationManager.Stub {
*
* @param provider the name of the location provider
* @param uid the requestor's UID
+ * @param userId the user id to query
*/
- private boolean isAllowedByUserSettingsLocked(String provider, int uid) {
+ private boolean isAllowedByUserSettingsLocked(String provider, int uid, int userId) {
if (!isCurrentProfile(UserHandle.getUserId(uid)) && !isUidALocationProvider(uid)) {
return false;
}
- return isAllowedByCurrentUserSettingsLocked(provider);
+ return isAllowedByUserSettingsLockedForUser(provider, userId);
}
/**
@@ -1572,7 +1586,8 @@ public class LocationManagerService extends ILocationManager.Stub {
continue;
}
if (allowedResolutionLevel >= getMinimumResolutionLevelForProviderUse(name)) {
- if (enabledOnly && !isAllowedByUserSettingsLocked(name, uid)) {
+ if (enabledOnly
+ && !isAllowedByUserSettingsLocked(name, uid, mCurrentUserId)) {
continue;
}
if (criteria != null && !LocationProvider.propertiesMeetCriteria(
@@ -2098,7 +2113,7 @@ public class LocationManagerService extends ILocationManager.Stub {
oldRecord.disposeLocked(false);
}
- boolean isProviderEnabled = isAllowedByUserSettingsLocked(name, uid);
+ boolean isProviderEnabled = isAllowedByUserSettingsLocked(name, uid, mCurrentUserId);
if (isProviderEnabled) {
applyRequirementsLocked(name);
} else {
@@ -2219,7 +2234,7 @@ public class LocationManagerService extends ILocationManager.Stub {
LocationProviderInterface provider = mProvidersByName.get(name);
if (provider == null) return null;
- if (!isAllowedByUserSettingsLocked(name, uid)) return null;
+ if (!isAllowedByUserSettingsLocked(name, uid, mCurrentUserId)) return null;
Location location;
if (allowedResolutionLevel < RESOLUTION_LEVEL_FINE) {
@@ -2540,6 +2555,173 @@ public class LocationManagerService extends ILocationManager.Stub {
}
/**
+ * Returns the current location enabled/disabled status for a user
+ *
+ * @param userId the id of the user
+ * @return true if location is enabled
+ */
+ @Override
+ public boolean isLocationEnabledForUser(int userId) {
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ final String allowedProviders = Settings.Secure.getStringForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+ userId);
+ if (allowedProviders == null) {
+ return false;
+ }
+ final List<String> providerList = Arrays.asList(allowedProviders.split(","));
+ for(String provider : mRealProviders.keySet()) {
+ if (provider.equals(LocationManager.PASSIVE_PROVIDER)
+ || provider.equals(LocationManager.FUSED_PROVIDER)) {
+ continue;
+ }
+ if (providerList.contains(provider)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Enable or disable location for a user
+ *
+ * @param enabled true to enable location, false to disable location
+ * @param userId the id of the user
+ */
+ @Override
+ public void setLocationEnabledForUser(boolean enabled, int userId) {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS,
+ "Requires WRITE_SECURE_SETTINGS permission");
+
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ final Set<String> allRealProviders = mRealProviders.keySet();
+ // Update all providers on device plus gps and network provider when disabling
+ // location
+ Set<String> allProvidersSet = new ArraySet<>(allRealProviders.size() + 2);
+ allProvidersSet.addAll(allRealProviders);
+ // When disabling location, disable gps and network provider that could have been
+ // enabled by location mode api.
+ if (enabled == false) {
+ allProvidersSet.add(LocationManager.GPS_PROVIDER);
+ allProvidersSet.add(LocationManager.NETWORK_PROVIDER);
+ }
+ if (allProvidersSet.isEmpty()) {
+ return;
+ }
+ // to ensure thread safety, we write the provider name with a '+' or '-'
+ // and let the SettingsProvider handle it rather than reading and modifying
+ // the list of enabled providers.
+ final String prefix = enabled ? "+" : "-";
+ StringBuilder locationProvidersAllowed = new StringBuilder();
+ for (String provider : allProvidersSet) {
+ if (provider.equals(LocationManager.PASSIVE_PROVIDER)
+ || provider.equals(LocationManager.FUSED_PROVIDER)) {
+ continue;
+ }
+ locationProvidersAllowed.append(prefix);
+ locationProvidersAllowed.append(provider);
+ locationProvidersAllowed.append(",");
+ }
+ // Remove the trailing comma
+ locationProvidersAllowed.setLength(locationProvidersAllowed.length() - 1);
+ Settings.Secure.putStringForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+ locationProvidersAllowed.toString(),
+ userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Returns the current enabled/disabled status of a location provider and user
+ *
+ * @param provider name of the provider
+ * @param userId the id of the user
+ * @return true if the provider exists and is enabled
+ */
+ @Override
+ public boolean isProviderEnabledForUser(String provider, int userId) {
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ // Fused provider is accessed indirectly via criteria rather than the provider-based APIs,
+ // so we discourage its use
+ if (LocationManager.FUSED_PROVIDER.equals(provider)) return false;
+
+ int uid = Binder.getCallingUid();
+ synchronized (mLock) {
+ LocationProviderInterface p = mProvidersByName.get(provider);
+ return p != null
+ && isAllowedByUserSettingsLocked(provider, uid, userId);
+ }
+ }
+
+ /**
+ * Enable or disable a single location provider.
+ *
+ * @param provider name of the provider
+ * @param enabled true to enable the provider. False to disable the provider
+ * @param userId the id of the user to set
+ * @return true if the value was set, false on errors
+ */
+ @Override
+ public boolean setProviderEnabledForUser(String provider, boolean enabled, int userId) {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS,
+ "Requires WRITE_SECURE_SETTINGS permission");
+
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ // Fused provider is accessed indirectly via criteria rather than the provider-based APIs,
+ // so we discourage its use
+ if (LocationManager.FUSED_PROVIDER.equals(provider)) return false;
+
+ long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ // No such provider exists
+ if (!mProvidersByName.containsKey(provider)) return false;
+
+ // If it is a test provider, do not write to Settings.Secure
+ if (mMockProviders.containsKey(provider)) {
+ setTestProviderEnabled(provider, enabled);
+ return true;
+ }
+
+ // to ensure thread safety, we write the provider name with a '+' or '-'
+ // and let the SettingsProvider handle it rather than reading and modifying
+ // the list of enabled providers.
+ String providerChange = (enabled ? "+" : "-") + provider;
+ return Settings.Secure.putStringForUser(
+ mContext.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+ providerChange, userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
* Read location provider status from Settings.Secure
*
* @param provider the location provider to query
@@ -2560,6 +2742,23 @@ public class LocationManagerService extends ILocationManager.Stub {
}
/**
+ * Method for checking INTERACT_ACROSS_USERS permission if specified user id is not the same as
+ * current user id
+ *
+ * @param userId the user id to get or set value
+ */
+ private void checkInteractAcrossUsersPermission(int userId) {
+ int uid = Binder.getCallingUid();
+ if (UserHandle.getUserId(uid) != userId) {
+ if (ActivityManager.checkComponentPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS, uid, -1, true)
+ != PERMISSION_GRANTED) {
+ throw new SecurityException("Requires INTERACT_ACROSS_USERS permission");
+ }
+ }
+ }
+
+ /**
* Returns "true" if the UID belongs to a bound location provider.
*
* @param uid the uid
@@ -3076,7 +3275,11 @@ public class LocationManagerService extends ILocationManager.Stub {
if (!canCallerAccessMockLocation(opPackageName)) {
return;
}
+ setTestProviderEnabled(provider, enabled);
+ }
+ /** Enable or disable a test location provider. */
+ private void setTestProviderEnabled(String provider, boolean enabled) {
synchronized (mLock) {
MockProvider mockProvider = mMockProviders.get(provider);
if (mockProvider == null) {
diff --git a/com/android/server/LockGuard.java b/com/android/server/LockGuard.java
index b7449173..5ce16c49 100644
--- a/com/android/server/LockGuard.java
+++ b/com/android/server/LockGuard.java
@@ -16,10 +16,14 @@
package com.android.server;
+import android.annotation.Nullable;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
+import com.android.internal.os.BackgroundThread;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -60,8 +64,6 @@ import java.io.PrintWriter;
public class LockGuard {
private static final String TAG = "LockGuard";
- public static final boolean ENABLED = false;
-
/**
* Well-known locks ordered by fixed index. Locks with a specific index
* should never be acquired while holding a lock of a lower index.
@@ -73,8 +75,9 @@ public class LockGuard {
public static final int INDEX_STORAGE = 4;
public static final int INDEX_WINDOW = 5;
public static final int INDEX_ACTIVITY = 6;
+ public static final int INDEX_DPMS = 7;
- private static Object[] sKnownFixed = new Object[INDEX_ACTIVITY + 1];
+ private static Object[] sKnownFixed = new Object[INDEX_DPMS + 1];
private static ArrayMap<Object, LockInfo> sKnown = new ArrayMap<>(0, true);
@@ -84,6 +87,9 @@ public class LockGuard {
/** Child locks that can be acquired while this lock is already held */
public ArraySet<Object> children = new ArraySet<>(0, true);
+
+ /** If true, do wtf instead of a warning log. */
+ public boolean doWtf;
}
private static LockInfo findOrCreateLockInfo(Object lock) {
@@ -114,9 +120,9 @@ public class LockGuard {
if (child == null) continue;
if (Thread.holdsLock(child)) {
- Slog.w(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding "
- + lockToString(child) + " while trying to acquire "
- + lockToString(lock), new Throwable());
+ doLog(lock, "Calling thread " + Thread.currentThread().getName()
+ + " is holding " + lockToString(child) + " while trying to acquire "
+ + lockToString(lock));
triggered = true;
}
}
@@ -145,13 +151,30 @@ public class LockGuard {
for (int i = 0; i < index; i++) {
final Object lock = sKnownFixed[i];
if (lock != null && Thread.holdsLock(lock)) {
- Slog.w(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding "
- + lockToString(i) + " while trying to acquire "
- + lockToString(index), new Throwable());
+
+ // Note in this case sKnownFixed may not contain a lock at the given index,
+ // which is okay and in that case we just don't do a WTF.
+ final Object targetMayBeNull = sKnownFixed[index];
+ doLog(targetMayBeNull, "Calling thread " + Thread.currentThread().getName()
+ + " is holding " + lockToString(i) + " while trying to acquire "
+ + lockToString(index));
}
}
}
+ private static void doLog(@Nullable Object lock, String message) {
+ if (lock != null && findOrCreateLockInfo(lock).doWtf) {
+
+ // Don't want to call into ActivityManager with any lock held, so let's just call it
+ // from a new thread. We don't want to use known threads (e.g. BackgroundThread) either
+ // because they may be stuck too.
+ final Throwable stackTrace = new RuntimeException(message);
+ new Thread(() -> Slog.wtf(TAG, stackTrace)).start();
+ return;
+ }
+ Slog.w(TAG, message, new Throwable());
+ }
+
/**
* Report the given lock with a well-known label.
*/
@@ -165,19 +188,33 @@ public class LockGuard {
* Report the given lock with a well-known index.
*/
public static Object installLock(Object lock, int index) {
+ return installLock(lock, index, /*doWtf=*/ false);
+ }
+
+ /**
+ * Report the given lock with a well-known index.
+ */
+ public static Object installLock(Object lock, int index, boolean doWtf) {
sKnownFixed[index] = lock;
+ final LockInfo info = findOrCreateLockInfo(lock);
+ info.doWtf = doWtf;
+ info.label = "Lock-" + lockToString(index);
return lock;
}
public static Object installNewLock(int index) {
+ return installNewLock(index, /*doWtf=*/ false);
+ }
+
+ public static Object installNewLock(int index, boolean doWtf) {
final Object lock = new Object();
- installLock(lock, index);
+ installLock(lock, index, doWtf);
return lock;
}
private static String lockToString(Object lock) {
final LockInfo info = sKnown.get(lock);
- if (info != null) {
+ if (info != null && !TextUtils.isEmpty(info.label)) {
return info.label;
} else {
return "0x" + Integer.toHexString(System.identityHashCode(lock));
@@ -193,6 +230,7 @@ public class LockGuard {
case INDEX_STORAGE: return "STORAGE";
case INDEX_WINDOW: return "WINDOW";
case INDEX_ACTIVITY: return "ACTIVITY";
+ case INDEX_DPMS: return "DPMS";
default: return Integer.toString(index);
}
}
diff --git a/com/android/server/NetworkScoreService.java b/com/android/server/NetworkScoreService.java
index 33f77697..80d7ac93 100644
--- a/com/android/server/NetworkScoreService.java
+++ b/com/android/server/NetworkScoreService.java
@@ -128,6 +128,30 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
}
};
+ public static final class Lifecycle extends SystemService {
+ private final NetworkScoreService mService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ mService = new NetworkScoreService(context);
+ }
+
+ @Override
+ public void onStart() {
+ Log.i(TAG, "Registering " + Context.NETWORK_SCORE_SERVICE);
+ publishBinderService(Context.NETWORK_SCORE_SERVICE, mService);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ mService.systemReady();
+ } else if (phase == PHASE_BOOT_COMPLETED) {
+ mService.systemRunning();
+ }
+ }
+ }
+
/**
* Clears scores when the active scorer package is no longer valid and
* manages the service connection.
diff --git a/com/android/server/SystemServer.java b/com/android/server/SystemServer.java
index 6f50ee27..5519d229 100644
--- a/com/android/server/SystemServer.java
+++ b/com/android/server/SystemServer.java
@@ -725,7 +725,6 @@ public final class SystemServer {
NetworkStatsService networkStats = null;
NetworkPolicyManagerService networkPolicy = null;
ConnectivityService connectivity = null;
- NetworkScoreService networkScore = null;
NsdService serviceDiscovery= null;
WindowManagerService wm = null;
SerialService serial = null;
@@ -1090,12 +1089,7 @@ public final class SystemServer {
}
traceBeginAndSlog("StartNetworkScoreService");
- try {
- networkScore = new NetworkScoreService(context);
- ServiceManager.addService(Context.NETWORK_SCORE_SERVICE, networkScore);
- } catch (Throwable e) {
- reportWtf("starting Network Score Service", e);
- }
+ mSystemServiceManager.startService(NetworkScoreService.Lifecycle.class);
traceEnd();
traceBeginAndSlog("StartNetworkStatsService");
@@ -1728,7 +1722,6 @@ public final class SystemServer {
final NetworkStatsService networkStatsF = networkStats;
final NetworkPolicyManagerService networkPolicyF = networkPolicy;
final ConnectivityService connectivityF = connectivity;
- final NetworkScoreService networkScoreF = networkScore;
final LocationManagerService locationF = location;
final CountryDetectorService countryDetectorF = countryDetector;
final NetworkTimeUpdateService networkTimeUpdaterF = networkTimeUpdater;
@@ -1789,13 +1782,6 @@ public final class SystemServer {
reportWtf("starting System UI", e);
}
traceEnd();
- traceBeginAndSlog("MakeNetworkScoreReady");
- try {
- if (networkScoreF != null) networkScoreF.systemReady();
- } catch (Throwable e) {
- reportWtf("making Network Score Service ready", e);
- }
- traceEnd();
traceBeginAndSlog("MakeNetworkManagementServiceReady");
try {
if (networkManagementF != null) networkManagementF.systemReady();
@@ -1917,13 +1903,6 @@ public final class SystemServer {
}
traceEnd();
- traceBeginAndSlog("MakeNetworkScoreServiceReady");
- try {
- if (networkScoreF != null) networkScoreF.systemRunning();
- } catch (Throwable e) {
- reportWtf("Notifying NetworkScoreService running", e);
- }
- traceEnd();
traceBeginAndSlog("IncidentDaemonReady");
try {
// TODO: Switch from checkService to getService once it's always
diff --git a/com/android/server/ThreadPriorityBooster.java b/com/android/server/ThreadPriorityBooster.java
index cc9ac0df..53e8ce43 100644
--- a/com/android/server/ThreadPriorityBooster.java
+++ b/com/android/server/ThreadPriorityBooster.java
@@ -25,6 +25,8 @@ import static android.os.Process.setThreadPriority;
*/
public class ThreadPriorityBooster {
+ private static final boolean ENABLE_LOCK_GUARD = false;
+
private volatile int mBoostToPriority;
private final int mLockGuardIndex;
@@ -50,7 +52,7 @@ public class ThreadPriorityBooster {
}
}
state.regionCounter++;
- if (LockGuard.ENABLED) {
+ if (ENABLE_LOCK_GUARD) {
LockGuard.guard(mLockGuardIndex);
}
}
diff --git a/com/android/server/VibratorService.java b/com/android/server/VibratorService.java
index 752c44a6..66c2b073 100644
--- a/com/android/server/VibratorService.java
+++ b/com/android/server/VibratorService.java
@@ -53,6 +53,7 @@ import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.util.DebugUtils;
import android.util.Slog;
+import android.util.SparseArray;
import android.view.InputDevice;
import android.media.AudioAttributes;
@@ -91,7 +92,7 @@ public class VibratorService extends IVibratorService.Stub
private final boolean mAllowPriorityVibrationsInLowPowerMode;
private final boolean mSupportsAmplitudeControl;
private final int mDefaultVibrationAmplitude;
- private final VibrationEffect[] mFallbackEffects;
+ private final SparseArray<VibrationEffect> mFallbackEffects;
private final WorkSource mTmpWorkSource = new WorkSource();
private final Handler mH = new Handler();
private final Object mLock = new Object();
@@ -177,6 +178,7 @@ public class VibratorService extends IVibratorService.Stub
switch (prebaked.getId()) {
case VibrationEffect.EFFECT_CLICK:
case VibrationEffect.EFFECT_DOUBLE_CLICK:
+ case VibrationEffect.EFFECT_HEAVY_CLICK:
case VibrationEffect.EFFECT_TICK:
return true;
default:
@@ -293,7 +295,11 @@ public class VibratorService extends IVibratorService.Stub
com.android.internal.R.array.config_clockTickVibePattern);
VibrationEffect tickEffect = createEffect(tickEffectTimings);
- mFallbackEffects = new VibrationEffect[] { clickEffect, doubleClickEffect, tickEffect };
+ mFallbackEffects = new SparseArray<VibrationEffect>();
+ mFallbackEffects.put(VibrationEffect.EFFECT_CLICK, clickEffect);
+ mFallbackEffects.put(VibrationEffect.EFFECT_DOUBLE_CLICK, doubleClickEffect);
+ mFallbackEffects.put(VibrationEffect.EFFECT_TICK, tickEffect);
+ mFallbackEffects.put(VibrationEffect.EFFECT_HEAVY_CLICK, clickEffect);
}
private static VibrationEffect createEffect(long[] timings) {
@@ -960,10 +966,7 @@ public class VibratorService extends IVibratorService.Stub
}
private VibrationEffect getFallbackEffect(int effectId) {
- if (effectId < 0 || effectId >= mFallbackEffects.length) {
- return null;
- }
- return mFallbackEffects[effectId];
+ return mFallbackEffects.get(effectId);
}
/**
diff --git a/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 5c5978ad..28aa9844 100644
--- a/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -53,7 +53,6 @@ import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
-
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.server.accessibility.AccessibilityManagerService.RemoteAccessibilityConnection;
@@ -76,7 +75,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
implements ServiceConnection, IBinder.DeathRecipient, KeyEventDispatcher.KeyEventFilter,
FingerprintGestureDispatcher.FingerprintGestureClient {
private static final boolean DEBUG = false;
- private static final String LOG_TAG = "AccessibilityClientConnection";
+ private static final String LOG_TAG = "AbstractAccessibilityServiceConnection";
protected final Context mContext;
protected final SystemSupport mSystemSupport;
diff --git a/com/android/server/accessibility/AccessibilityManagerService.java b/com/android/server/accessibility/AccessibilityManagerService.java
index de112f94..08aa0632 100644
--- a/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/com/android/server/accessibility/AccessibilityManagerService.java
@@ -2721,7 +2721,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
- private AccessibilityWindowInfo populateReportedWindow(WindowInfo window) {
+ private AccessibilityWindowInfo populateReportedWindowLocked(WindowInfo window) {
final int windowId = findWindowIdLocked(window.token);
if (windowId < 0) {
return null;
@@ -2949,7 +2949,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
| AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
- // In Z order
+ // In Z order top to bottom
public List<AccessibilityWindowInfo> mWindows;
public SparseArray<AccessibilityWindowInfo> mA11yWindowInfoById = new SparseArray<>();
public SparseArray<WindowInfo> mWindowInfoById = new SparseArray<>();
@@ -3140,11 +3140,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mAccessibilityFocusedWindowId != INVALID_WINDOW_ID;
if (windowCount > 0) {
for (int i = 0; i < windowCount; i++) {
- WindowInfo windowInfo = windows.get(i);
- AccessibilityWindowInfo window = (mWindowsForAccessibilityCallback != null)
- ? mWindowsForAccessibilityCallback.populateReportedWindow(windowInfo)
- : null;
+ final WindowInfo windowInfo = windows.get(i);
+ final AccessibilityWindowInfo window;
+ if (mWindowsForAccessibilityCallback != null) {
+ window = mWindowsForAccessibilityCallback
+ .populateReportedWindowLocked(windowInfo);
+ } else {
+ window = null;
+ }
if (window != null) {
+
+ // Flip layers in list to be consistent with AccessibilityService#getWindows
+ window.setLayer(windowCount - 1 - window.getLayer());
+
final int windowId = window.getId();
if (window.isFocused()) {
mFocusedWindowId = windowId;
@@ -3169,7 +3177,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
// active window once we decided which it is.
final int accessibilityWindowCount = mWindows.size();
for (int i = 0; i < accessibilityWindowCount; i++) {
- AccessibilityWindowInfo window = mWindows.get(i);
+ final AccessibilityWindowInfo window = mWindows.get(i);
if (window.getId() == mActiveWindowId) {
window.setActive(true);
}
@@ -3188,7 +3196,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
if (shouldClearAccessibilityFocus) {
- clearAccessibilityFocus(mAccessibilityFocusedWindowId);
+ mMainHandler.sendMessage(obtainMessage(
+ AccessibilityManagerService::clearAccessibilityFocus,
+ AccessibilityManagerService.this,
+ box(mAccessibilityFocusedWindowId)));
}
}
diff --git a/com/android/server/am/ActiveServices.java b/com/android/server/am/ActiveServices.java
index f57e9ac2..067566dc 100644
--- a/com/android/server/am/ActiveServices.java
+++ b/com/android/server/am/ActiveServices.java
@@ -2283,7 +2283,7 @@ public final class ActiveServices {
+ " app=" + app);
if (app != null && app.thread != null) {
try {
- app.addPackage(r.appInfo.packageName, r.appInfo.versionCode, mAm.mProcessStats);
+ app.addPackage(r.appInfo.packageName, r.appInfo.longVersionCode, mAm.mProcessStats);
realStartServiceLocked(r, app, execInFg);
return null;
} catch (TransactionTooLargeException e) {
@@ -2645,6 +2645,8 @@ public final class ActiveServices {
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG);
msg.obj = r.app;
+ msg.getData().putCharSequence(
+ ActivityManagerService.SERVICE_RECORD_KEY, r.toString());
mAm.mHandler.sendMessage(msg);
}
}
@@ -3009,7 +3011,7 @@ public final class ActiveServices {
mPendingServices.remove(i);
i--;
- proc.addPackage(sr.appInfo.packageName, sr.appInfo.versionCode,
+ proc.addPackage(sr.appInfo.packageName, sr.appInfo.longVersionCode,
mAm.mProcessStats);
realStartServiceLocked(sr, proc, sr.createdFromFg);
didSomething = true;
@@ -3563,13 +3565,15 @@ public final class ActiveServices {
if (app != null) {
mAm.mAppErrors.appNotResponding(app, null, null, false,
- "Context.startForegroundService() did not then call Service.startForeground()");
+ "Context.startForegroundService() did not then call Service.startForeground(): "
+ + r);
}
}
- void serviceForegroundCrash(ProcessRecord app) {
+ void serviceForegroundCrash(ProcessRecord app, CharSequence serviceRecord) {
mAm.crashApplication(app.uid, app.pid, app.info.packageName, app.userId,
- "Context.startForegroundService() did not then call Service.startForeground()");
+ "Context.startForegroundService() did not then call Service.startForeground(): "
+ + serviceRecord);
}
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
diff --git a/com/android/server/am/ActivityDisplay.java b/com/android/server/am/ActivityDisplay.java
index 4a8bc874..fac3f924 100644
--- a/com/android/server/am/ActivityDisplay.java
+++ b/com/android/server/am/ActivityDisplay.java
@@ -16,7 +16,6 @@
package com.android.server.am;
-import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -44,7 +43,6 @@ import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.WindowConfiguration;
import android.graphics.Point;
-import android.graphics.Rect;
import android.util.IntArray;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
@@ -702,57 +700,52 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack>
}
/**
- * @return the stack currently above the home stack. Can be null if there is no home stack, or
- * the home stack is already on top.
+ * @return the stack currently above the {@param stack}. Can be null if the {@param stack} is
+ * already top-most.
*/
- ActivityStack getStackAboveHome() {
- if (mHomeStack == null) {
- // Skip if there is no home stack
- return null;
- }
-
- final int stackIndex = mStacks.indexOf(mHomeStack) + 1;
+ ActivityStack getStackAbove(ActivityStack stack) {
+ final int stackIndex = mStacks.indexOf(stack) + 1;
return (stackIndex < mStacks.size()) ? mStacks.get(stackIndex) : null;
}
/**
- * Adjusts the home stack behind the last visible stack in the display if necessary. Generally
- * used in conjunction with {@link #moveHomeStackBehindStack}.
+ * Adjusts the {@param stack} behind the last visible stack in the display if necessary.
+ * Generally used in conjunction with {@link #moveStackBehindStack}.
*/
- void moveHomeStackBehindBottomMostVisibleStack() {
- if (mHomeStack == null || mHomeStack.shouldBeVisible(null)) {
- // Skip if there is no home stack, or if it is already visible
+ void moveStackBehindBottomMostVisibleStack(ActivityStack stack) {
+ if (stack.shouldBeVisible(null)) {
+ // Skip if the stack is already visible
return;
}
- // Move the home stack to the bottom to not affect the following visibility checks
- positionChildAtBottom(mHomeStack);
+ // Move the stack to the bottom to not affect the following visibility checks
+ positionChildAtBottom(stack);
- // Find the next position where the homes stack should be placed
+ // Find the next position where the stack should be placed
final int numStacks = mStacks.size();
for (int stackNdx = 0; stackNdx < numStacks; stackNdx++) {
- final ActivityStack stack = mStacks.get(stackNdx);
- if (stack == mHomeStack) {
+ final ActivityStack s = mStacks.get(stackNdx);
+ if (s == stack) {
continue;
}
- final int winMode = stack.getWindowingMode();
+ final int winMode = s.getWindowingMode();
final boolean isValidWindowingMode = winMode == WINDOWING_MODE_FULLSCREEN ||
winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
- if (stack.shouldBeVisible(null) && isValidWindowingMode) {
- // Move the home stack to behind this stack
- positionChildAt(mHomeStack, Math.max(0, stackNdx - 1));
+ if (s.shouldBeVisible(null) && isValidWindowingMode) {
+ // Move the provided stack to behind this stack
+ positionChildAt(stack, Math.max(0, stackNdx - 1));
break;
}
}
}
/**
- * Moves the home stack behind the given {@param stack} if possible. If {@param stack} is not
- * currently in the display, then then the home stack is moved to the back. Generally used in
- * conjunction with {@link #moveHomeStackBehindBottomMostVisibleStack}.
+ * Moves the {@param stack} behind the given {@param behindStack} if possible. If
+ * {@param behindStack} is not currently in the display, then then the stack is moved to the
+ * back. Generally used in conjunction with {@link #moveStackBehindBottomMostVisibleStack}.
*/
- void moveHomeStackBehindStack(ActivityStack behindStack) {
- if (behindStack == null || behindStack == mHomeStack) {
+ void moveStackBehindStack(ActivityStack stack, ActivityStack behindStack) {
+ if (behindStack == null || behindStack == stack) {
return;
}
@@ -760,11 +753,11 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack>
// list, so we need to adjust the insertion index to account for the removed index
// TODO: Remove this logic when WindowContainer.positionChildAt() is updated to adjust the
// position internally
- final int homeStackIndex = mStacks.indexOf(mHomeStack);
+ final int stackIndex = mStacks.indexOf(stack);
final int behindStackIndex = mStacks.indexOf(behindStack);
- final int insertIndex = homeStackIndex <= behindStackIndex
+ final int insertIndex = stackIndex <= behindStackIndex
? behindStackIndex - 1 : behindStackIndex;
- positionChildAt(mHomeStack, Math.max(0, insertIndex));
+ positionChildAt(stack, Math.max(0, insertIndex));
}
boolean isSleeping() {
diff --git a/com/android/server/am/ActivityManagerService.java b/com/android/server/am/ActivityManagerService.java
index f97c6d61..47284cba 100644
--- a/com/android/server/am/ActivityManagerService.java
+++ b/com/android/server/am/ActivityManagerService.java
@@ -27,6 +27,7 @@ import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS;
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.Manifest.permission.REMOVE_TASKS;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
+import static android.Manifest.permission.STOP_APP_SWITCHES;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.app.ActivityManager.RESIZE_MODE_PRESERVE_WINDOW;
import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
@@ -38,6 +39,7 @@ import static android.app.ActivityManagerInternal.ASSIST_KEY_STRUCTURE;
import static android.app.ActivityThread.PROC_START_SEQ_IDENT;
import static android.app.AppOpsManager.OP_ASSIST_STRUCTURE;
import static android.app.AppOpsManager.OP_NONE;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -50,6 +52,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
+import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT;
import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
import static android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY;
@@ -116,6 +119,7 @@ import static android.provider.Settings.Global.DEBUG_APP;
import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RTL;
+import static android.provider.Settings.Global.HIDE_ERROR_DIALOGS;
import static android.provider.Settings.Global.NETWORK_ACCESS_TIMEOUT_MS;
import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
import static android.provider.Settings.System.FONT_SCALE;
@@ -204,6 +208,8 @@ import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_TASK_IN_PLACE;
import static android.view.WindowManager.TRANSIT_TASK_OPEN;
import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
@@ -257,7 +263,6 @@ import android.app.WaitResult;
import android.app.WindowConfiguration.ActivityType;
import android.app.WindowConfiguration.WindowingMode;
import android.app.admin.DevicePolicyCache;
-import android.app.admin.DevicePolicyManager;
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.app.backup.IBackupManager;
@@ -762,7 +767,7 @@ public class ActivityManagerService extends IActivityManager.Stub
/**
* The controller for all operations related to locktask.
*/
- final LockTaskController mLockTaskController;
+ private final LockTaskController mLockTaskController;
final UserController mUserController;
@@ -1279,17 +1284,24 @@ public class ActivityManagerService extends IActivityManager.Stub
private final class FontScaleSettingObserver extends ContentObserver {
private final Uri mFontScaleUri = Settings.System.getUriFor(FONT_SCALE);
+ private final Uri mHideErrorDialogsUri = Settings.Global.getUriFor(HIDE_ERROR_DIALOGS);
public FontScaleSettingObserver() {
super(mHandler);
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(mFontScaleUri, false, this, UserHandle.USER_ALL);
+ resolver.registerContentObserver(mHideErrorDialogsUri, false, this,
+ UserHandle.USER_ALL);
}
@Override
public void onChange(boolean selfChange, Uri uri, @UserIdInt int userId) {
if (mFontScaleUri.equals(uri)) {
updateFontScaleIfNeeded(userId);
+ } else if (mHideErrorDialogsUri.equals(uri)) {
+ synchronized (ActivityManagerService.this) {
+ updateShouldShowDialogsLocked(getGlobalConfiguration());
+ }
}
}
}
@@ -1918,6 +1930,8 @@ public class ActivityManagerService extends IActivityManager.Stub
static final int FIRST_COMPAT_MODE_MSG = 300;
static final int FIRST_SUPERVISOR_STACK_MSG = 100;
+ static final String SERVICE_RECORD_KEY = "servicerecord";
+
static ServiceThread sKillThread = null;
static KillHandler sKillHandler = null;
@@ -1944,7 +1958,7 @@ public class ActivityManagerService extends IActivityManager.Stub
final ActivityManagerConstants mConstants;
// Encapsulates the global setting "hidden_api_blacklist_exemptions"
- final HiddenApiBlacklist mHiddenApiBlacklist;
+ final HiddenApiSettings mHiddenApiBlacklist;
PackageManagerInternal mPackageManagerInt;
@@ -2166,7 +2180,8 @@ public class ActivityManagerService extends IActivityManager.Stub
mServices.serviceForegroundTimeout((ServiceRecord)msg.obj);
} break;
case SERVICE_FOREGROUND_CRASH_MSG: {
- mServices.serviceForegroundCrash((ProcessRecord)msg.obj);
+ mServices.serviceForegroundCrash(
+ (ProcessRecord) msg.obj, msg.getData().getCharSequence(SERVICE_RECORD_KEY));
} break;
case DISPATCH_PENDING_INTENT_CANCEL_MSG: {
RemoteCallbackList<IResultReceiver> callbacks
@@ -2878,17 +2893,20 @@ public class ActivityManagerService extends IActivityManager.Stub
}
/**
- * Encapsulates the global setting "hidden_api_blacklist_exemptions", including tracking the
- * latest value via a content observer.
+ * Encapsulates global settings related to hidden API enforcement behaviour, including tracking
+ * the latest value via a content observer.
*/
- static class HiddenApiBlacklist extends ContentObserver {
+ static class HiddenApiSettings extends ContentObserver {
private final Context mContext;
private boolean mBlacklistDisabled;
private String mExemptionsStr;
private List<String> mExemptions = Collections.emptyList();
+ private int mLogSampleRate = -1;
+ @HiddenApiEnforcementPolicy private int mPolicyPreP = HIDDEN_API_ENFORCEMENT_DEFAULT;
+ @HiddenApiEnforcementPolicy private int mPolicyP = HIDDEN_API_ENFORCEMENT_DEFAULT;
- public HiddenApiBlacklist(Handler handler, Context context) {
+ public HiddenApiSettings(Handler handler, Context context) {
super(handler);
mContext = context;
}
@@ -2898,6 +2916,18 @@ public class ActivityManagerService extends IActivityManager.Stub
Settings.Global.getUriFor(Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS),
false,
this);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.HIDDEN_API_ACCESS_LOG_SAMPLING_RATE),
+ false,
+ this);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.HIDDEN_API_POLICY_PRE_P_APPS),
+ false,
+ this);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.HIDDEN_API_POLICY_P_APPS),
+ false,
+ this);
update();
}
@@ -2917,13 +2947,41 @@ public class ActivityManagerService extends IActivityManager.Stub
}
zygoteProcess.setApiBlacklistExemptions(mExemptions);
}
+ int logSampleRate = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.HIDDEN_API_ACCESS_LOG_SAMPLING_RATE, -1);
+ if (logSampleRate < 0 || logSampleRate > 0x10000) {
+ logSampleRate = -1;
+ }
+ if (logSampleRate != -1 && logSampleRate != mLogSampleRate) {
+ mLogSampleRate = logSampleRate;
+ zygoteProcess.setHiddenApiAccessLogSampleRate(mLogSampleRate);
+ }
+ mPolicyPreP = getValidEnforcementPolicy(Settings.Global.HIDDEN_API_POLICY_PRE_P_APPS);
+ mPolicyP = getValidEnforcementPolicy(Settings.Global.HIDDEN_API_POLICY_P_APPS);
+ }
+ private @HiddenApiEnforcementPolicy int getValidEnforcementPolicy(String settingsKey) {
+ int policy = Settings.Global.getInt(mContext.getContentResolver(), settingsKey,
+ ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT);
+ if (ApplicationInfo.isValidHiddenApiEnforcementPolicy(policy)) {
+ return policy;
+ } else {
+ return ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT;
+ }
}
boolean isDisabled() {
return mBlacklistDisabled;
}
+ @HiddenApiEnforcementPolicy int getPolicyForPrePApps() {
+ return mPolicyPreP;
+ }
+
+ @HiddenApiEnforcementPolicy int getPolicyForPApps() {
+ return mPolicyP;
+ }
+
public void onChange(boolean selfChange) {
update();
}
@@ -3096,7 +3154,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
};
- mHiddenApiBlacklist = new HiddenApiBlacklist(mHandler, mContext);
+ mHiddenApiBlacklist = new HiddenApiSettings(mHandler, mContext);
Watchdog.getInstance().addMonitor(this);
Watchdog.getInstance().addThread(mHandler);
@@ -4212,6 +4270,9 @@ public class ActivityManagerService extends IActivityManager.Stub
}
if (!disableHiddenApiChecks && !mHiddenApiBlacklist.isDisabled()) {
+ app.info.maybeUpdateHiddenApiEnforcementPolicy(
+ mHiddenApiBlacklist.getPolicyForPrePApps(),
+ mHiddenApiBlacklist.getPolicyForPApps());
@HiddenApiEnforcementPolicy int policy =
app.info.getHiddenApiEnforcementPolicy();
int policyBits = (policy << Zygote.API_ENFORCEMENT_POLICY_SHIFT);
@@ -5238,12 +5299,14 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void cancelRecentsAnimation() {
+ public void cancelRecentsAnimation(boolean restoreHomeStackPosition) {
enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "cancelRecentsAnimation()");
final long origId = Binder.clearCallingIdentity();
try {
synchronized (this) {
- mWindowManager.cancelRecentsAnimation();
+ mWindowManager.cancelRecentsAnimation(restoreHomeStackPosition
+ ? REORDER_MOVE_TO_ORIGINAL_POSITION
+ : REORDER_KEEP_IN_PLACE, "cancelRecentsAnimation");
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -7635,7 +7698,7 @@ public class ActivityManagerService extends IActivityManager.Stub
// target APIs higher than O MR1. Since access to the serial
// is now behind a permission we push down the value.
final String buildSerial = (appInfo.targetSandboxVersion < 2
- && appInfo.targetSdkVersion <= Build.VERSION_CODES.O_MR1)
+ && appInfo.targetSdkVersion < Build.VERSION_CODES.P)
? sTheRealBuildSerial : Build.UNKNOWN;
// Check if this is a secondary process that should be incorporated into some
@@ -12392,6 +12455,10 @@ public class ActivityManagerService extends IActivityManager.Stub
return mActivityStartController;
}
+ LockTaskController getLockTaskController() {
+ return mLockTaskController;
+ }
+
ClientLifecycleManager getLifecycleManager() {
return mLifecycleManager;
}
@@ -12951,9 +13018,13 @@ public class ActivityManagerService extends IActivityManager.Stub
} catch (RemoteException exc) {
// Ignore.
}
+ return isBackgroundRestrictedNoCheck(callingUid, packageName);
+ }
+
+ boolean isBackgroundRestrictedNoCheck(final int uid, final String packageName) {
final int mode = mAppOpsService.checkOperation(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
- callingUid, packageName);
- return (mode != AppOpsManager.MODE_ALLOWED);
+ uid, packageName);
+ return mode != AppOpsManager.MODE_ALLOWED;
}
@Override
@@ -13335,12 +13406,7 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public void stopAppSwitches() {
- if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
- != PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException("viewquires permission "
- + android.Manifest.permission.STOP_APP_SWITCHES);
- }
-
+ enforceCallerIsRecentsOrHasPermission(STOP_APP_SWITCHES, "stopAppSwitches");
synchronized(this) {
mAppSwitchesAllowedTime = SystemClock.uptimeMillis()
+ APP_SWITCH_DELAY_TIME;
@@ -13350,12 +13416,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
public void resumeAppSwitches() {
- if (checkCallingPermission(android.Manifest.permission.STOP_APP_SWITCHES)
- != PackageManager.PERMISSION_GRANTED) {
- throw new SecurityException("Requires permission "
- + android.Manifest.permission.STOP_APP_SWITCHES);
- }
-
+ enforceCallerIsRecentsOrHasPermission(STOP_APP_SWITCHES, "resumeAppSwitches");
synchronized(this) {
// Note that we don't execute any pending app switches... we will
// let those wait until either the timeout, or the next start
@@ -13382,9 +13443,11 @@ public class ActivityManagerService extends IActivityManager.Stub
return true;
}
- int perm = checkComponentPermission(
- android.Manifest.permission.STOP_APP_SWITCHES, sourcePid,
- sourceUid, -1, true);
+ if (mRecentTasks.isCallerRecents(sourceUid)) {
+ return true;
+ }
+
+ int perm = checkComponentPermission(STOP_APP_SWITCHES, sourcePid, sourceUid, -1, true);
if (perm == PackageManager.PERMISSION_GRANTED) {
return true;
}
@@ -13395,9 +13458,7 @@ public class ActivityManagerService extends IActivityManager.Stub
// If the actual IPC caller is different from the logical source, then
// also see if they are allowed to control app switches.
if (callingUid != -1 && callingUid != sourceUid) {
- perm = checkComponentPermission(
- android.Manifest.permission.STOP_APP_SWITCHES, callingPid,
- callingUid, -1, true);
+ perm = checkComponentPermission(STOP_APP_SWITCHES, callingPid, callingUid, -1, true);
if (perm == PackageManager.PERMISSION_GRANTED) {
return true;
}
@@ -16995,6 +17056,11 @@ public class ActivityManagerService extends IActivityManager.Stub
pw.println("ms");
}
mUidObservers.finishBroadcast();
+
+ pw.println();
+ pw.println(" ServiceManager statistics:");
+ ServiceManager.sStatLogger.dump(pw, " ");
+ pw.println();
}
}
pw.println(" mForceBackgroundCheck=" + mForceBackgroundCheck);
@@ -21058,6 +21124,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ final String action = intent.getAction();
BroadcastOptions brOptions = null;
if (bOptions != null) {
brOptions = new BroadcastOptions(bOptions);
@@ -21078,11 +21145,16 @@ public class ActivityManagerService extends IActivityManager.Stub
throw new SecurityException(msg);
}
}
+ if (brOptions.isDontSendToRestrictedApps()
+ && isBackgroundRestrictedNoCheck(callingUid, callerPackage)) {
+ Slog.i(TAG, "Not sending broadcast " + action + " - app " + callerPackage
+ + " has background restrictions");
+ return ActivityManager.START_CANCELED;
+ }
}
// Verify that protected broadcasts are only being sent by system code,
// and that system code is only sending protected broadcasts.
- final String action = intent.getAction();
final boolean isProtectedBroadcast;
try {
isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
@@ -21904,8 +21976,6 @@ public class ActivityManagerService extends IActivityManager.Stub
"com.android.frameworks.locationtests",
"com.android.frameworks.coretests.privacy",
"com.android.settings.ui",
- "com.android.perftests.core",
- "com.android.perftests.multiuser",
};
public boolean startInstrumentation(ComponentName className,
@@ -22443,7 +22513,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mUserController.getCurrentUserId());
// TODO: If our config changes, should we auto dismiss any currently showing dialogs?
- mShowDialogs = shouldShowDialogs(mTempConfig);
+ updateShouldShowDialogsLocked(mTempConfig);
AttributeCache ac = AttributeCache.instance();
if (ac != null) {
@@ -22698,7 +22768,7 @@ public class ActivityManagerService extends IActivityManager.Stub
* A thought: SystemUI might also want to get told about this, the Power
* dialog / global actions also might want different behaviors.
*/
- private static boolean shouldShowDialogs(Configuration config) {
+ private void updateShouldShowDialogsLocked(Configuration config) {
final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS
&& config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH
&& config.navigation == Configuration.NAVIGATION_NONAV);
@@ -22707,7 +22777,9 @@ public class ActivityManagerService extends IActivityManager.Stub
&& !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER)
&& modeType != Configuration.UI_MODE_TYPE_TELEVISION
&& modeType != Configuration.UI_MODE_TYPE_VR_HEADSET);
- return inputMethodExists && uiModeSupportsDialogs;
+ final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(),
+ HIDE_ERROR_DIALOGS, 0) != 0;
+ mShowDialogs = inputMethodExists && uiModeSupportsDialogs && !hideDialogsSet;
}
@Override
@@ -26614,7 +26686,7 @@ public class ActivityManagerService extends IActivityManager.Stub
record.waitingForNetwork = false;
final long totalTime = SystemClock.uptimeMillis() - startTime;
if (totalTime >= mWaitForNetworkTimeoutMs || DEBUG_NETWORK) {
- Slog.wtf(TAG_NETWORK, "Total time waited for network rules to get updated: "
+ Slog.w(TAG_NETWORK, "Total time waited for network rules to get updated: "
+ totalTime + ". Uid: " + callingUid + " procStateSeq: "
+ procStateSeq + " UidRec: " + record
+ " validateUidRec: " + mValidateUids.get(callingUid));
diff --git a/com/android/server/am/ActivityRecord.java b/com/android/server/am/ActivityRecord.java
index 1af41144..f32717a3 100644
--- a/com/android/server/am/ActivityRecord.java
+++ b/com/android/server/am/ActivityRecord.java
@@ -93,8 +93,6 @@ import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_SWITCH;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_VISIBILITY;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.am.ActivityStack.ActivityState.DESTROYED;
-import static com.android.server.am.ActivityStack.ActivityState.DESTROYING;
import static com.android.server.am.ActivityStack.ActivityState.INITIALIZING;
import static com.android.server.am.ActivityStack.ActivityState.PAUSED;
import static com.android.server.am.ActivityStack.ActivityState.PAUSING;
@@ -154,6 +152,7 @@ import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.GraphicBuffer;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
@@ -780,11 +779,13 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
* @param task The new parent {@link TaskRecord}.
*/
void setTask(TaskRecord task) {
- setTask(task, false /*reparenting*/);
+ setTask(task /* task */, false /* reparenting */);
}
/**
* This method should only be called by {@link TaskRecord#removeActivity(ActivityRecord)}.
+ * @param task The new parent task.
+ * @param reparenting Whether we're in the middle of reparenting.
*/
void setTask(TaskRecord task, boolean reparenting) {
// Do nothing if the {@link TaskRecord} is the same as the current {@link getTask}.
@@ -792,12 +793,19 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
return;
}
- final ActivityStack stack = getStack();
+ final ActivityStack oldStack = getStack();
+ final ActivityStack newStack = task != null ? task.getStack() : null;
+
+ // Inform old stack (if present) of activity removal and new stack (if set) of activity
+ // addition.
+ if (oldStack != newStack) {
+ if (!reparenting && oldStack != null) {
+ oldStack.onActivityRemovedFromStack(this);
+ }
- // If the new {@link TaskRecord} is from a different {@link ActivityStack}, remove this
- // {@link ActivityRecord} from its current {@link ActivityStack}.
- if (!reparenting && stack != null && (task == null || stack != task.getStack())) {
- stack.onActivityRemovedFromStack(this);
+ if (newStack != null) {
+ newStack.onActivityAddedToStack(this);
+ }
}
this.task = task;
@@ -1074,8 +1082,15 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
// Must reparent first in window manager
mWindowContainerController.reparent(newTask.getWindowContainerController(), position);
+ // Reparenting prevents informing the parent stack of activity removal in the case that
+ // the new stack has the same parent. we must manually signal here if this is not the case.
+ final ActivityStack prevStack = prevTask.getStack();
+
+ if (prevStack != newTask.getStack()) {
+ prevStack.onActivityRemovedFromStack(this);
+ }
// Remove the activity from the old task and add it to the new task.
- prevTask.removeActivity(this, true /*reparenting*/);
+ prevTask.removeActivity(this, true /* reparenting */);
newTask.addActivityAtIndex(position, this);
}
@@ -1199,10 +1214,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
}
boolean isFocusable() {
- if (inSplitScreenPrimaryWindowingMode() && mStackSupervisor.mIsDockMinimized) {
- return false;
- }
- return getWindowConfiguration().canReceiveKeys() || isAlwaysFocusable();
+ return mStackSupervisor.isFocusable(this, isAlwaysFocusable());
}
boolean isResizeable() {
@@ -1590,14 +1602,20 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
void pauseKeyDispatchingLocked() {
if (!keysPaused) {
keysPaused = true;
- mWindowContainerController.pauseKeyDispatching();
+
+ if (mWindowContainerController != null) {
+ mWindowContainerController.pauseKeyDispatching();
+ }
}
}
void resumeKeyDispatchingLocked() {
if (keysPaused) {
keysPaused = false;
- mWindowContainerController.resumeKeyDispatching();
+
+ if (mWindowContainerController != null) {
+ mWindowContainerController.resumeKeyDispatching();
+ }
}
}
@@ -2880,7 +2898,7 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
final ActivityManagerService service = stackSupervisor.mService;
final ActivityInfo aInfo = stackSupervisor.resolveActivity(intent, resolvedType, 0, null,
- userId);
+ userId, Binder.getCallingUid());
if (aInfo == null) {
throw new XmlPullParserException("restoreActivity resolver error. Intent=" + intent +
" resolvedType=" + resolvedType);
diff --git a/com/android/server/am/ActivityStack.java b/com/android/server/am/ActivityStack.java
index 00ebcbd9..eb482c1b 100644
--- a/com/android/server/am/ActivityStack.java
+++ b/com/android/server/am/ActivityStack.java
@@ -489,13 +489,13 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
*/
void onActivityStateChanged(ActivityRecord record, ActivityState state, String reason) {
if (record == mResumedActivity && state != RESUMED) {
- clearResumedActivity(reason + " - onActivityStateChanged");
+ setResumedActivity(null, reason + " - onActivityStateChanged");
}
if (state == RESUMED) {
if (DEBUG_STACK) Slog.v(TAG_STACK, "set resumed activity to:" + record + " reason:"
+ reason);
- mResumedActivity = record;
+ setResumedActivity(record, reason + " - onActivityStateChanged");
mService.setResumedActivityUncheckLocked(record, reason);
mStackSupervisor.mRecentTasks.add(record.getTask());
}
@@ -1077,13 +1077,8 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
}
boolean isFocusable() {
- if (getWindowConfiguration().canReceiveKeys()) {
- return true;
- }
- // The stack isn't focusable. See if its top activity is focusable to force focus on the
- // stack.
final ActivityRecord r = topRunningActivityLocked();
- return r != null && r.isFocusable();
+ return mStackSupervisor.isFocusable(this, r != null && r.isFocusable());
}
final boolean isAttached() {
@@ -2314,14 +2309,14 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
return mResumedActivity;
}
- /**
- * Clears reference to currently resumed activity.
- */
- private void clearResumedActivity(String reason) {
- if (DEBUG_STACK) Slog.d(TAG_STACK, "clearResumedActivity: " + mResumedActivity + " reason:"
- + reason);
+ private void setResumedActivity(ActivityRecord r, String reason) {
+ if (mResumedActivity == r) {
+ return;
+ }
- mResumedActivity = null;
+ if (DEBUG_STACK) Slog.d(TAG_STACK, "setResumedActivity stack:" + this + " + from: "
+ + mResumedActivity + " to:" + r + " reason:" + reason);
+ mResumedActivity = r;
}
@GuardedBy("mService")
@@ -3544,7 +3539,15 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
mService.updateOomAdjLocked();
}
- final TaskRecord finishTopRunningActivityLocked(ProcessRecord app, String reason) {
+ /**
+ * Finish the topmost activity that belongs to the crashed app. We may also finish the activity
+ * that requested launch of the crashed one to prevent launch-crash loop.
+ * @param app The app that crashed.
+ * @param reason Reason to perform this action.
+ * @return The task that was finished in this stack, {@code null} if top running activity does
+ * not belong to the crashed app.
+ */
+ final TaskRecord finishTopCrashedActivityLocked(ProcessRecord app, String reason) {
ActivityRecord r = topRunningActivityLocked();
TaskRecord finishedTask = null;
if (r == null || r.app != app) {
@@ -3735,7 +3738,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
}
if (endTask) {
- mService.mLockTaskController.clearLockedTask(task);
+ mService.getLockTaskController().clearLockedTask(task);
}
} else if (!r.isState(PAUSING)) {
// If the activity is PAUSING, we will complete the finish once
@@ -4019,14 +4022,20 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
* an activity moves away from the stack.
*/
void onActivityRemovedFromStack(ActivityRecord r) {
- if (mResumedActivity == r) {
- clearResumedActivity("onActivityRemovedFromStack");
+ removeTimeoutsForActivityLocked(r);
+
+ if (mResumedActivity != null && mResumedActivity == r) {
+ setResumedActivity(null, "onActivityRemovedFromStack");
}
- if (mPausingActivity == r) {
+ if (mPausingActivity != null && mPausingActivity == r) {
mPausingActivity = null;
}
+ }
- removeTimeoutsForActivityLocked(r);
+ void onActivityAddedToStack(ActivityRecord r) {
+ if(r.getState() == RESUMED) {
+ setResumedActivity(r, "onActivityAddedToStack");
+ }
}
/**
@@ -4631,7 +4640,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
// In LockTask mode, moving a locked task to the back of the stack may expose unlocked
// ones. Therefore we need to check if this operation is allowed.
- if (!mService.mLockTaskController.canMoveTaskToBack(tr)) {
+ if (!mService.getLockTaskController().canMoveTaskToBack(tr)) {
return false;
}
@@ -4744,30 +4753,32 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
mTmpBounds.clear();
mTmpInsetBounds.clear();
- for (int i = mTaskHistory.size() - 1; i >= 0; i--) {
- final TaskRecord task = mTaskHistory.get(i);
- if (task.isResizeable()) {
- if (inFreeformWindowingMode()) {
- // TODO: Can be removed now since each freeform task is in its own stack.
- // For freeform stack we don't adjust the size of the tasks to match that
- // of the stack, but we do try to make sure the tasks are still contained
- // with the bounds of the stack.
- mTmpRect2.set(task.getOverrideBounds());
- fitWithinBounds(mTmpRect2, bounds);
- task.updateOverrideConfiguration(mTmpRect2);
- } else {
- task.updateOverrideConfiguration(taskBounds, insetBounds);
+ synchronized (mWindowManager.getWindowManagerLock()) {
+ for (int i = mTaskHistory.size() - 1; i >= 0; i--) {
+ final TaskRecord task = mTaskHistory.get(i);
+ if (task.isResizeable()) {
+ if (inFreeformWindowingMode()) {
+ // TODO: Can be removed now since each freeform task is in its own stack.
+ // For freeform stack we don't adjust the size of the tasks to match that
+ // of the stack, but we do try to make sure the tasks are still contained
+ // with the bounds of the stack.
+ mTmpRect2.set(task.getOverrideBounds());
+ fitWithinBounds(mTmpRect2, bounds);
+ task.updateOverrideConfiguration(mTmpRect2);
+ } else {
+ task.updateOverrideConfiguration(taskBounds, insetBounds);
+ }
}
- }
- mTmpBounds.put(task.taskId, task.getOverrideBounds());
- if (tempTaskInsetBounds != null) {
- mTmpInsetBounds.put(task.taskId, tempTaskInsetBounds);
+ mTmpBounds.put(task.taskId, task.getOverrideBounds());
+ if (tempTaskInsetBounds != null) {
+ mTmpInsetBounds.put(task.taskId, tempTaskInsetBounds);
+ }
}
- }
- mWindowContainerController.resize(bounds, mTmpBounds, mTmpInsetBounds);
- setBounds(bounds);
+ mWindowContainerController.resize(bounds, mTmpBounds, mTmpInsetBounds);
+ setBounds(bounds);
+ }
}
@@ -5076,7 +5087,12 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
onActivityRemovedFromStack(record);
}
- mTaskHistory.remove(task);
+ final boolean removed = mTaskHistory.remove(task);
+
+ if (removed) {
+ EventLog.writeEvent(EventLogTags.AM_REMOVE_TASK, task.taskId, getStackId());
+ }
+
removeActivitiesFromLRUListLocked(task);
updateTaskMovement(task, true);
diff --git a/com/android/server/am/ActivityStackSupervisor.java b/com/android/server/am/ActivityStackSupervisor.java
index d5dfdcf0..cbf30bdd 100644
--- a/com/android/server/am/ActivityStackSupervisor.java
+++ b/com/android/server/am/ActivityStackSupervisor.java
@@ -667,6 +667,14 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
return mFocusedStack;
}
+ boolean isFocusable(ConfigurationContainer container, boolean alwaysFocusable) {
+ if (container.inSplitScreenPrimaryWindowingMode() && mIsDockMinimized) {
+ return false;
+ }
+
+ return container.getWindowConfiguration().canReceiveKeys() || alwaysFocusable;
+ }
+
ActivityStack getLastStack() {
return mLastFocusedStack;
}
@@ -1264,10 +1272,11 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId) {
- return resolveIntent(intent, resolvedType, userId, 0);
+ return resolveIntent(intent, resolvedType, userId, 0, Binder.getCallingUid());
}
- ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags) {
+ ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags,
+ int filterCallingUid) {
synchronized (mService) {
try {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "resolveIntent");
@@ -1278,7 +1287,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
modifiedFlags |= PackageManager.MATCH_INSTANT;
}
return mService.getPackageManagerInternalLocked().resolveIntent(
- intent, resolvedType, modifiedFlags, userId, true);
+ intent, resolvedType, modifiedFlags, userId, true, filterCallingUid);
} finally {
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -1287,8 +1296,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
ActivityInfo resolveActivity(Intent intent, String resolvedType, int startFlags,
- ProfilerInfo profilerInfo, int userId) {
- final ResolveInfo rInfo = resolveIntent(intent, resolvedType, userId);
+ ProfilerInfo profilerInfo, int userId, int filterCallingUid) {
+ final ResolveInfo rInfo = resolveIntent(intent, resolvedType, userId, 0, filterCallingUid);
return resolveActivity(intent, rInfo, startFlags, profilerInfo);
}
@@ -1371,12 +1380,13 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
mService.updateLruProcessLocked(app, true, null);
mService.updateOomAdjLocked();
+ final LockTaskController lockTaskController = mService.getLockTaskController();
if (task.mLockTaskAuth == LOCK_TASK_AUTH_LAUNCHABLE
|| task.mLockTaskAuth == LOCK_TASK_AUTH_LAUNCHABLE_PRIV
|| (task.mLockTaskAuth == LOCK_TASK_AUTH_WHITELISTED
- && mService.mLockTaskController.getLockTaskModeState()
- == LOCK_TASK_MODE_LOCKED)) {
- mService.mLockTaskController.startLockTaskMode(task, false, 0 /* blank UID */);
+ && lockTaskController.getLockTaskModeState()
+ == LOCK_TASK_MODE_LOCKED)) {
+ lockTaskController.startLockTaskMode(task, false, 0 /* blank UID */);
}
try {
@@ -1579,7 +1589,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
// to run in multiple processes, because this is actually
// part of the framework so doesn't make sense to track as a
// separate apk in the process.
- app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode,
+ app.addPackage(r.info.packageName, r.info.applicationInfo.longVersionCode,
mService.mProcessStats);
}
realStartActivityLocked(r, app, andResume, checkConfig);
@@ -2126,15 +2136,22 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
}
- TaskRecord finishTopRunningActivityLocked(ProcessRecord app, String reason) {
+ /**
+ * Finish the topmost activities in all stacks that belong to the crashed app.
+ * @param app The app that crashed.
+ * @param reason Reason to perform this action.
+ * @return The task that was finished in this stack, {@code null} if haven't found any.
+ */
+ TaskRecord finishTopCrashedActivitiesLocked(ProcessRecord app, String reason) {
TaskRecord finishedTask = null;
ActivityStack focusedStack = getFocusedStack();
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ActivityDisplay display = mActivityDisplays.valueAt(displayNdx);
- final int numStacks = display.getChildCount();
- for (int stackNdx = 0; stackNdx < numStacks; ++stackNdx) {
+ // It is possible that request to finish activity might also remove its task and stack,
+ // so we need to be careful with indexes in the loop and check child count every time.
+ for (int stackNdx = 0; stackNdx < display.getChildCount(); ++stackNdx) {
final ActivityStack stack = display.getChildAt(stackNdx);
- TaskRecord t = stack.finishTopRunningActivityLocked(app, reason);
+ final TaskRecord t = stack.finishTopCrashedActivityLocked(app, reason);
if (stack == focusedStack || finishedTask == null) {
finishedTask = t;
}
@@ -2892,7 +2909,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
if (tr != null) {
tr.removeTaskActivitiesLocked(pauseImmediately, reason);
cleanUpRemovedTaskLocked(tr, killProcess, removeFromRecents);
- mService.mLockTaskController.clearLockedTask(tr);
+ mService.getLockTaskController().clearLockedTask(tr);
if (tr.isPersistable) {
mService.notifyTaskPersisterLocked(null, true);
}
@@ -3806,7 +3823,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
pw.print(mRecentTasks.isRecentsComponentHomeActivity(mCurrentUser));
getKeyguardController().dump(pw, prefix);
- mService.mLockTaskController.dump(pw, prefix);
+ mService.getLockTaskController().dump(pw, prefix);
}
public void writeToProto(ProtoOutputStream proto, long fieldId) {
diff --git a/com/android/server/am/ActivityStartController.java b/com/android/server/am/ActivityStartController.java
index fb78838c..86a3fce9 100644
--- a/com/android/server/am/ActivityStartController.java
+++ b/com/android/server/am/ActivityStartController.java
@@ -23,7 +23,6 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.am.ActivityManagerService.ALLOW_FULL_ONLY;
-import android.app.ActivityOptions;
import android.app.IApplicationThread;
import android.content.ComponentName;
import android.content.ContentResolver;
@@ -33,7 +32,6 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Binder;
-import android.os.Bundle;
import android.os.FactoryTest;
import android.os.Handler;
import android.os.IBinder;
@@ -322,7 +320,7 @@ public class ActivityStartController {
// Collect information about the target of the Intent.
ActivityInfo aInfo = mSupervisor.resolveActivity(intent, resolvedTypes[i], 0,
- null, userId);
+ null, userId, realCallingUid);
// TODO: New, check if this is correct
aInfo = mService.getActivityInfoForUser(aInfo, userId);
diff --git a/com/android/server/am/ActivityStartInterceptor.java b/com/android/server/am/ActivityStartInterceptor.java
index b86a8a6f..5b6b5086 100644
--- a/com/android/server/am/ActivityStartInterceptor.java
+++ b/com/android/server/am/ActivityStartInterceptor.java
@@ -21,6 +21,8 @@ import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
+import static android.app.admin.DevicePolicyManager.EXTRA_RESTRICTION;
+import static android.app.admin.DevicePolicyManager.POLICY_SUSPEND_PACKAGES;
import static android.content.Context.KEYGUARD_SERVICE;
import static android.content.Intent.EXTRA_INTENT;
import static android.content.Intent.EXTRA_PACKAGE_NAME;
@@ -30,8 +32,10 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED;
+import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+
+import android.Manifest;
import android.app.ActivityOptions;
-import android.app.AppGlobals;
import android.app.KeyguardManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
@@ -39,6 +43,7 @@ import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.Binder;
@@ -49,6 +54,7 @@ import android.os.UserManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.HarmfulAppWarningActivity;
+import com.android.internal.app.SuspendedAppActivity;
import com.android.internal.app.UnlaunchableAppActivity;
import com.android.server.LocalServices;
@@ -149,7 +155,7 @@ class ActivityStartInterceptor {
mInTask = inTask;
mActivityOptions = activityOptions;
- if (interceptSuspendPackageIfNeed()) {
+ if (interceptSuspendedPackageIfNeeded()) {
// Skip the rest of interceptions as the package is suspended by device admin so
// no user action can undo this.
return true;
@@ -202,18 +208,15 @@ class ActivityStartInterceptor {
return true;
}
- private boolean interceptSuspendPackageIfNeed() {
- // Do not intercept if the admin did not suspend the package
- if (mAInfo == null || mAInfo.applicationInfo == null ||
- (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) == 0) {
- return false;
- }
+ private boolean interceptSuspendedByAdminPackage() {
DevicePolicyManagerInternal devicePolicyManager = LocalServices
.getService(DevicePolicyManagerInternal.class);
if (devicePolicyManager == null) {
return false;
}
mIntent = devicePolicyManager.createShowAdminSupportIntent(mUserId, true);
+ mIntent.putExtra(EXTRA_RESTRICTION, POLICY_SUSPEND_PACKAGES);
+
mCallingPid = mRealCallingPid;
mCallingUid = mRealCallingUid;
mResolvedType = null;
@@ -228,6 +231,56 @@ class ActivityStartInterceptor {
return true;
}
+ private Intent createSuspendedAppInterceptIntent(String suspendedPackage,
+ String suspendingPackage, String dialogMessage, int userId) {
+ final Intent interceptIntent = new Intent(mServiceContext, SuspendedAppActivity.class)
+ .putExtra(SuspendedAppActivity.EXTRA_SUSPENDED_PACKAGE, suspendedPackage)
+ .putExtra(SuspendedAppActivity.EXTRA_DIALOG_MESSAGE, dialogMessage)
+ .putExtra(SuspendedAppActivity.EXTRA_SUSPENDING_PACKAGE, suspendingPackage)
+ .putExtra(Intent.EXTRA_USER_ID, userId)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ final Intent moreDetailsIntent = new Intent(Intent.ACTION_SHOW_SUSPENDED_APP_DETAILS)
+ .setPackage(suspendingPackage);
+ final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS;
+ final ResolveInfo resolvedInfo = mSupervisor.resolveIntent(moreDetailsIntent, null, userId);
+ if (resolvedInfo != null && resolvedInfo.activityInfo != null
+ && requiredPermission.equals(resolvedInfo.activityInfo.permission)) {
+ moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, suspendedPackage)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ interceptIntent.putExtra(SuspendedAppActivity.EXTRA_MORE_DETAILS_INTENT,
+ moreDetailsIntent);
+ }
+ return interceptIntent;
+ }
+
+ private boolean interceptSuspendedPackageIfNeeded() {
+ // Do not intercept if the package is not suspended
+ if (mAInfo == null || mAInfo.applicationInfo == null ||
+ (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) == 0) {
+ return false;
+ }
+ final PackageManagerInternal pmi = mService.getPackageManagerInternalLocked();
+ if (pmi == null) {
+ return false;
+ }
+ final String suspendedPackage = mAInfo.applicationInfo.packageName;
+ final String suspendingPackage = pmi.getSuspendingPackage(suspendedPackage, mUserId);
+ if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) {
+ return interceptSuspendedByAdminPackage();
+ }
+ final String dialogMessage = pmi.getSuspendedDialogMessage(suspendedPackage, mUserId);
+ mIntent = createSuspendedAppInterceptIntent(suspendedPackage, suspendingPackage,
+ dialogMessage, mUserId);
+ mCallingPid = mRealCallingPid;
+ mCallingUid = mRealCallingUid;
+ mResolvedType = null;
+ mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, 0);
+ mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/);
+ return true;
+ }
+
private boolean interceptWorkProfileChallengeIfNeeded() {
final Intent interceptingIntent = interceptWithConfirmCredentialsIfNeeded(mAInfo, mUserId);
if (interceptingIntent == null) {
@@ -289,9 +342,9 @@ class ActivityStartInterceptor {
private boolean interceptHarmfulAppIfNeeded() {
CharSequence harmfulAppWarning;
try {
- harmfulAppWarning = AppGlobals.getPackageManager().getHarmfulAppWarning(
- mAInfo.packageName, mUserId);
- } catch (RemoteException e) {
+ harmfulAppWarning = mService.getPackageManager()
+ .getHarmfulAppWarning(mAInfo.packageName, mUserId);
+ } catch (RemoteException ex) {
return false;
}
diff --git a/com/android/server/am/ActivityStarter.java b/com/android/server/am/ActivityStarter.java
index 1b7e1ede..fb89e671 100644
--- a/com/android/server/am/ActivityStarter.java
+++ b/com/android/server/am/ActivityStarter.java
@@ -28,10 +28,10 @@ import static android.app.ActivityManager.START_SUCCESS;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.content.Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
@@ -903,14 +903,22 @@ class ActivityStarter {
final int clearTaskFlags = FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK;
boolean clearedTask = (mLaunchFlags & clearTaskFlags) == clearTaskFlags
&& mReuseTask != null;
- if (startedActivityStack.inPinnedWindowingMode()
- && (result == START_TASK_TO_FRONT || result == START_DELIVERED_TO_TOP
- || clearedTask)) {
- // The activity was already running in the pinned stack so it wasn't started, but either
- // brought to the front or the new intent was delivered to it since it was already in
- // front. Notify anyone interested in this piece of information.
- mService.mTaskChangeNotificationController.notifyPinnedActivityRestartAttempt(
- clearedTask);
+ if (result == START_TASK_TO_FRONT || result == START_DELIVERED_TO_TOP || clearedTask) {
+ // The activity was already running so it wasn't started, but either brought to the
+ // front or the new intent was delivered to it since it was already in front. Notify
+ // anyone interested in this piece of information.
+ switch (startedActivityStack.getWindowingMode()) {
+ case WINDOWING_MODE_PINNED:
+ mService.mTaskChangeNotificationController.notifyPinnedActivityRestartAttempt(
+ clearedTask);
+ break;
+ case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY:
+ final ActivityStack homeStack = mSupervisor.mHomeStack;
+ if (homeStack != null && homeStack.shouldBeVisible(null /* starting */)) {
+ mService.mWindowManager.showRecentApps();
+ }
+ break;
+ }
}
}
@@ -966,7 +974,8 @@ class ActivityStarter {
if (profileLockedAndParentUnlockingOrUnlocked) {
rInfo = mSupervisor.resolveIntent(intent, resolvedType, userId,
PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+ Binder.getCallingUid());
}
}
}
@@ -1147,9 +1156,10 @@ class ActivityStarter {
// If we are not able to proceed, disassociate the activity from the task. Leaving an
// activity in an incomplete state can lead to issues, such as performing operations
// without a window container.
- if (!ActivityManager.isStartResultSuccessful(result)
- && mStartActivity.getTask() != null) {
- mStartActivity.getTask().removeActivity(mStartActivity);
+ final ActivityStack stack = mStartActivity.getStack();
+ if (!ActivityManager.isStartResultSuccessful(result) && stack != null) {
+ stack.finishActivityLocked(mStartActivity, RESULT_CANCELED,
+ null /* intentResultData */, "startActivity", true /* oomAdj */);
}
mService.mWindowManager.continueSurfaceLayout();
}
@@ -1199,7 +1209,7 @@ class ActivityStarter {
// When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused but
// still needs to be a lock task mode violation since the task gets cleared out and
// the device would otherwise leave the locked task.
- if (mService.mLockTaskController.isLockTaskModeViolation(reusedActivity.getTask(),
+ if (mService.getLockTaskController().isLockTaskModeViolation(reusedActivity.getTask(),
(mLaunchFlags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))
== (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK))) {
Slog.e(TAG, "startActivityUnchecked: Attempt to violate Lock Task Mode");
@@ -2011,7 +2021,7 @@ class ActivityStarter {
mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
}
- if (mService.mLockTaskController.isLockTaskModeViolation(mStartActivity.getTask())) {
+ if (mService.getLockTaskController().isLockTaskModeViolation(mStartActivity.getTask())) {
Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
return START_RETURN_LOCK_TASK_MODE_VIOLATION;
}
@@ -2034,7 +2044,7 @@ class ActivityStarter {
}
private int setTaskFromSourceRecord() {
- if (mService.mLockTaskController.isLockTaskModeViolation(mSourceRecord.getTask())) {
+ if (mService.getLockTaskController().isLockTaskModeViolation(mSourceRecord.getTask())) {
Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
return START_RETURN_LOCK_TASK_MODE_VIOLATION;
}
@@ -2128,7 +2138,7 @@ class ActivityStarter {
private int setTaskFromInTask() {
// The caller is asking that the new activity be started in an explicit
// task it has provided to us.
- if (mService.mLockTaskController.isLockTaskModeViolation(mInTask)) {
+ if (mService.getLockTaskController().isLockTaskModeViolation(mInTask)) {
Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
return START_RETURN_LOCK_TASK_MODE_VIOLATION;
}
diff --git a/com/android/server/am/AppErrors.java b/com/android/server/am/AppErrors.java
index d5bb7ed6..bd1000ac 100644
--- a/com/android/server/am/AppErrors.java
+++ b/com/android/server/am/AppErrors.java
@@ -742,8 +742,8 @@ class AppErrors {
}
mService.mStackSupervisor.resumeFocusedStackTopActivityLocked();
} else {
- TaskRecord affectedTask =
- mService.mStackSupervisor.finishTopRunningActivityLocked(app, reason);
+ final TaskRecord affectedTask =
+ mService.mStackSupervisor.finishTopCrashedActivitiesLocked(app, reason);
if (data != null) {
data.task = affectedTask;
}
diff --git a/com/android/server/am/AppWarnings.java b/com/android/server/am/AppWarnings.java
index ab1d7bf2..ea0251e2 100644
--- a/com/android/server/am/AppWarnings.java
+++ b/com/android/server/am/AppWarnings.java
@@ -122,8 +122,9 @@ class AppWarnings {
return;
}
- if (ActivityManager.isRunningInTestHarness()
- && !mAlwaysShowUnsupportedCompileSdkWarningActivities.contains(r.realActivity)) {
+ // TODO(b/77862563): temp. fix while P is being finalized. To be reverted
+ if (/*ActivityManager.isRunningInTestHarness()
+ &&*/ !mAlwaysShowUnsupportedCompileSdkWarningActivities.contains(r.realActivity)) {
// Don't show warning if we are running in a test harness and we don't have to always
// show for this activity.
return;
diff --git a/com/android/server/am/BatteryStatsService.java b/com/android/server/am/BatteryStatsService.java
index 8ecd93e6..9c2b1a5d 100644
--- a/com/android/server/am/BatteryStatsService.java
+++ b/com/android/server/am/BatteryStatsService.java
@@ -19,16 +19,10 @@ package com.android.server.am;
import android.app.ActivityManager;
import android.app.job.JobProtoEnums;
import android.bluetooth.BluetoothActivityEnergyInfo;
-import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.hardware.usb.UsbManager;
import android.net.wifi.WifiActivityEnergyInfo;
-import android.os.PowerManager.ServiceType;
-import android.os.PowerSaveState;
import android.os.BatteryStats;
import android.os.BatteryStatsInternal;
import android.os.Binder;
@@ -37,18 +31,18 @@ import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFormatException;
+import android.os.PowerManager.ServiceType;
import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
import android.os.Process;
-import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManagerInternal;
import android.os.WorkSource;
-import android.os.WorkSource.WorkChain;
import android.os.connectivity.CellularBatteryStats;
-import android.os.connectivity.WifiBatteryStats;
import android.os.connectivity.GpsBatteryStats;
+import android.os.connectivity.WifiBatteryStats;
import android.os.health.HealthStatsParceler;
import android.os.health.HealthStatsWriter;
import android.os.health.UidHealthStats;
@@ -57,6 +51,7 @@ import android.telephony.ModemActivityInfo;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.util.Slog;
+import android.util.StatsLog;
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.BatteryStatsHelper;
@@ -65,7 +60,6 @@ import com.android.internal.os.PowerProfile;
import com.android.internal.os.RpmStats;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
-import android.util.StatsLog;
import java.io.File;
import java.io.FileDescriptor;
@@ -701,13 +695,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
- public void noteUsbConnectionState(boolean connected) {
- enforceCallingPermission();
- synchronized (mStats) {
- mStats.noteUsbConnectionStateLocked(connected);
- }
- }
-
public void notePhoneSignalStrength(SignalStrength signalStrength) {
enforceCallingPermission();
synchronized (mStats) {
@@ -1164,35 +1151,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub
Binder.getCallingPid(), Binder.getCallingUid(), null);
}
- public final static class UsbConnectionReceiver extends BroadcastReceiver {
- private static final String TAG = UsbConnectionReceiver.class.getSimpleName();
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
- final Intent usbState = context.registerReceiver(null, new IntentFilter(UsbManager.ACTION_USB_STATE));
- if (usbState != null) {
- handleUsbState(usbState);
- }
- } else if (UsbManager.ACTION_USB_STATE.equals(action)) {
- handleUsbState(intent);
- }
- }
- private void handleUsbState(Intent intent) {
- IBatteryStats bs = getService();
- if (bs == null) {
- Slog.w(TAG, "Could not access batterystats");
- return;
- }
- boolean connected = intent.getExtras().getBoolean(UsbManager.USB_CONNECTED);
- try {
- bs.noteUsbConnectionState(connected);
- } catch (RemoteException e) {
- Slog.w(TAG, "Could not access batterystats: ", e);
- }
- }
- }
-
final class WakeupReasonThread extends Thread {
private static final int MAX_REASON_SIZE = 512;
private CharsetDecoder mDecoder;
diff --git a/com/android/server/am/ProcessRecord.java b/com/android/server/am/ProcessRecord.java
index 03acb840..b7fde1da 100644
--- a/com/android/server/am/ProcessRecord.java
+++ b/com/android/server/am/ProcessRecord.java
@@ -496,7 +496,7 @@ final class ProcessRecord {
uid = _uid;
userId = UserHandle.getUserId(_uid);
processName = _processName;
- pkgList.put(_info.packageName, new ProcessStats.ProcessStateHolder(_info.versionCode));
+ pkgList.put(_info.packageName, new ProcessStats.ProcessStateHolder(_info.longVersionCode));
maxAdj = ProcessList.UNKNOWN_ADJ;
curRawAdj = setRawAdj = ProcessList.INVALID_ADJ;
curAdj = setAdj = verifiedAdj = ProcessList.INVALID_ADJ;
@@ -521,7 +521,7 @@ final class ProcessRecord {
origBase.makeInactive();
}
baseProcessTracker = tracker.getProcessStateLocked(info.packageName, uid,
- info.versionCode, processName);
+ info.longVersionCode, processName);
baseProcessTracker.makeActive();
for (int i=0; i<pkgList.size(); i++) {
ProcessStats.ProcessStateHolder holder = pkgList.valueAt(i);
@@ -529,7 +529,7 @@ final class ProcessRecord {
holder.state.makeInactive();
}
holder.state = tracker.getProcessStateLocked(pkgList.keyAt(i), uid,
- info.versionCode, processName);
+ info.longVersionCode, processName);
if (holder.state != baseProcessTracker) {
holder.state.makeActive();
}
@@ -828,9 +828,9 @@ final class ProcessRecord {
}
pkgList.clear();
ProcessState ps = tracker.getProcessStateLocked(
- info.packageName, uid, info.versionCode, processName);
+ info.packageName, uid, info.longVersionCode, processName);
ProcessStats.ProcessStateHolder holder = new ProcessStats.ProcessStateHolder(
- info.versionCode);
+ info.longVersionCode);
holder.state = ps;
pkgList.put(info.packageName, holder);
if (ps != baseProcessTracker) {
@@ -839,7 +839,7 @@ final class ProcessRecord {
}
} else if (N != 1) {
pkgList.clear();
- pkgList.put(info.packageName, new ProcessStats.ProcessStateHolder(info.versionCode));
+ pkgList.put(info.packageName, new ProcessStats.ProcessStateHolder(info.longVersionCode));
}
}
diff --git a/com/android/server/am/RecentTasks.java b/com/android/server/am/RecentTasks.java
index 1d305fb4..a20452bb 100644
--- a/com/android/server/am/RecentTasks.java
+++ b/com/android/server/am/RecentTasks.java
@@ -523,7 +523,7 @@ class RecentTasks {
}
for (int i = mTasks.size() - 1; i >= 0; --i) {
final TaskRecord tr = mTasks.get(i);
- if (tr.userId == userId && !mService.mLockTaskController.isTaskWhitelisted(tr)) {
+ if (tr.userId == userId && !mService.getLockTaskController().isTaskWhitelisted(tr)) {
remove(tr);
}
}
@@ -534,8 +534,8 @@ class RecentTasks {
final TaskRecord tr = mTasks.get(i);
final String taskPackageName =
tr.getBaseIntent().getComponent().getPackageName();
- if (tr.userId != userId) return;
- if (!taskPackageName.equals(packageName)) return;
+ if (tr.userId != userId) continue;
+ if (!taskPackageName.equals(packageName)) continue;
mService.mStackSupervisor.removeTaskByIdLocked(tr.taskId, true, REMOVE_FROM_RECENTS,
"remove-package-task");
@@ -1156,7 +1156,7 @@ class RecentTasks {
}
// If we're in lock task mode, ignore the root task
- if (task == mService.mLockTaskController.getRootTask()) {
+ if (task == mService.getLockTaskController().getRootTask()) {
return false;
}
@@ -1255,7 +1255,7 @@ class RecentTasks {
for (int i = 0; i < recentsCount; i++) {
final TaskRecord tr = mTasks.get(i);
if (task != tr) {
- if (!task.hasCompatibleActivityType(tr)) {
+ if (!task.hasCompatibleActivityType(tr) || task.userId != tr.userId) {
continue;
}
final Intent trIntent = tr.intent;
diff --git a/com/android/server/am/RecentsAnimation.java b/com/android/server/am/RecentsAnimation.java
index 9df321c6..06b5e20d 100644
--- a/com/android/server/am/RecentsAnimation.java
+++ b/com/android/server/am/RecentsAnimation.java
@@ -18,20 +18,25 @@ package com.android.server.am;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
import static android.view.WindowManager.TRANSIT_NONE;
import static com.android.server.am.ActivityStackSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.RecentsAnimationController.REORDER_KEEP_IN_PLACE;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_TOP;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Intent;
-import android.os.Handler;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Slog;
import android.view.IRecentsAnimationRunner;
+import com.android.server.wm.RecentsAnimationController;
import com.android.server.wm.RecentsAnimationController.RecentsAnimationCallbacks;
import com.android.server.wm.WindowManagerService;
@@ -41,22 +46,28 @@ import com.android.server.wm.WindowManagerService;
*/
class RecentsAnimation implements RecentsAnimationCallbacks {
private static final String TAG = RecentsAnimation.class.getSimpleName();
+ // TODO (b/73188263): Reset debugging flags
+ private static final boolean DEBUG = true;
private final ActivityManagerService mService;
private final ActivityStackSupervisor mStackSupervisor;
private final ActivityStartController mActivityStartController;
private final WindowManagerService mWindowManager;
private final UserController mUserController;
+ private final ActivityDisplay mDefaultDisplay;
private final int mCallingPid;
- // The stack to restore the home stack behind when the animation is finished
- private ActivityStack mRestoreHomeBehindStack;
+ private int mTargetActivityType;
+
+ // The stack to restore the target stack behind when the animation is finished
+ private ActivityStack mRestoreTargetBehindStack;
RecentsAnimation(ActivityManagerService am, ActivityStackSupervisor stackSupervisor,
ActivityStartController activityStartController, WindowManagerService wm,
UserController userController, int callingPid) {
mService = am;
mStackSupervisor = stackSupervisor;
+ mDefaultDisplay = stackSupervisor.getDefaultDisplay();
mActivityStartController = activityStartController;
mWindowManager = wm;
mUserController = userController;
@@ -65,25 +76,44 @@ class RecentsAnimation implements RecentsAnimationCallbacks {
void startRecentsActivity(Intent intent, IRecentsAnimationRunner recentsAnimationRunner,
ComponentName recentsComponent, int recentsUid) {
+ if (DEBUG) Slog.d(TAG, "startRecentsActivity(): intent=" + intent);
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "RecentsAnimation#startRecentsActivity");
if (!mWindowManager.canStartRecentsAnimation()) {
notifyAnimationCancelBeforeStart(recentsAnimationRunner);
+ if (DEBUG) Slog.d(TAG, "Can't start recents animation, nextAppTransition="
+ + mWindowManager.getPendingAppTransition());
return;
}
- // If the existing home activity is already on top, then cancel
- ActivityRecord homeActivity = mStackSupervisor.getHomeActivity();
- final boolean hasExistingHomeActivity = homeActivity != null;
- if (hasExistingHomeActivity) {
- final ActivityDisplay display = homeActivity.getDisplay();
- mRestoreHomeBehindStack = display.getStackAboveHome();
- if (mRestoreHomeBehindStack == null) {
+ // If the activity is associated with the recents stack, then try and get that first
+ mTargetActivityType = intent.getComponent() != null
+ && recentsComponent.equals(intent.getComponent())
+ ? ACTIVITY_TYPE_RECENTS
+ : ACTIVITY_TYPE_HOME;
+ final ActivityStack targetStack = mDefaultDisplay.getStack(WINDOWING_MODE_UNDEFINED,
+ mTargetActivityType);
+ ActivityRecord targetActivity = targetStack != null
+ ? targetStack.getTopActivity()
+ : null;
+ final boolean hasExistingActivity = targetActivity != null;
+ if (hasExistingActivity) {
+ final ActivityDisplay display = targetActivity.getDisplay();
+ mRestoreTargetBehindStack = display.getStackAbove(targetStack);
+ if (mRestoreTargetBehindStack == null) {
notifyAnimationCancelBeforeStart(recentsAnimationRunner);
+ if (DEBUG) Slog.d(TAG, "No stack above target stack=" + targetStack);
return;
}
}
+ // Send launch hint if we are actually launching the target. If it's already visible
+ // (shouldn't happen in general) we don't need to send it.
+ if (targetActivity == null || !targetActivity.visible) {
+ mStackSupervisor.sendPowerHintForLaunchStartIfNeeded(true /* forceSend */,
+ targetActivity);
+ }
+
mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunching();
mService.setRunningRemoteAnimation(mCallingPid, true);
@@ -91,48 +121,57 @@ class RecentsAnimation implements RecentsAnimationCallbacks {
mWindowManager.deferSurfaceLayout();
try {
final ActivityDisplay display;
- if (hasExistingHomeActivity) {
- // Move the home activity into place for the animation if it is not already top most
- display = homeActivity.getDisplay();
- display.moveHomeStackBehindBottomMostVisibleStack();
+ if (hasExistingActivity) {
+ // Move the recents activity into place for the animation if it is not top most
+ display = targetActivity.getDisplay();
+ display.moveStackBehindBottomMostVisibleStack(targetStack);
+ if (DEBUG) Slog.d(TAG, "Moved stack=" + targetStack + " behind stack="
+ + display.getStackAbove(targetStack));
} else {
- // No home activity
- final ActivityOptions opts = ActivityOptions.makeBasic();
- opts.setLaunchActivityType(ACTIVITY_TYPE_HOME);
- opts.setAvoidMoveToFront();
+ // No recents activity
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchActivityType(mTargetActivityType);
+ options.setAvoidMoveToFront();
intent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION);
mActivityStartController
- .obtainStarter(intent, "startRecentsActivity_noHomeActivity")
+ .obtainStarter(intent, "startRecentsActivity_noTargetActivity")
.setCallingUid(recentsUid)
.setCallingPackage(recentsComponent.getPackageName())
- .setActivityOptions(SafeActivityOptions.fromBundle(opts.toBundle()))
+ .setActivityOptions(SafeActivityOptions.fromBundle(options.toBundle()))
.setMayWait(mUserController.getCurrentUserId())
.execute();
mWindowManager.prepareAppTransition(TRANSIT_NONE, false);
- homeActivity = mStackSupervisor.getHomeActivity();
- display = homeActivity.getDisplay();
+ targetActivity = mDefaultDisplay.getStack(WINDOWING_MODE_UNDEFINED,
+ mTargetActivityType).getTopActivity();
+ display = targetActivity.getDisplay();
// TODO: Maybe wait for app to draw in this particular case?
+
+ if (DEBUG) Slog.d(TAG, "Started intent=" + intent);
}
- // Mark the home activity as launch-behind to bump its visibility for the
+ // Mark the target activity as launch-behind to bump its visibility for the
// duration of the gesture that is driven by the recents component
- homeActivity.mLaunchTaskBehind = true;
+ targetActivity.mLaunchTaskBehind = true;
// Fetch all the surface controls and pass them to the client to get the animation
// started
- mWindowManager.cancelRecentsAnimation();
- mWindowManager.initializeRecentsAnimation(recentsAnimationRunner, this,
- display.mDisplayId, mStackSupervisor.mRecentTasks.getRecentTaskIds());
+ mWindowManager.cancelRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION,
+ "startRecentsActivity");
+ mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner,
+ this, display.mDisplayId, mStackSupervisor.mRecentTasks.getRecentTaskIds());
// If we updated the launch-behind state, update the visibility of the activities after
// we fetch the visible tasks to be controlled by the animation
mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, PRESERVE_WINDOWS);
mStackSupervisor.getActivityMetricsLogger().notifyActivityLaunched(START_TASK_TO_FRONT,
- homeActivity);
+ targetActivity);
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to start recents activity", e);
+ throw e;
} finally {
mWindowManager.continueSurfaceLayout();
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -140,10 +179,19 @@ class RecentsAnimation implements RecentsAnimationCallbacks {
}
@Override
- public void onAnimationFinished(boolean moveHomeToTop) {
+ public void onAnimationFinished(@RecentsAnimationController.ReorderMode int reorderMode) {
synchronized (mService) {
+ if (DEBUG) Slog.d(TAG, "onAnimationFinished(): controller="
+ + mWindowManager.getRecentsAnimationController()
+ + " reorderMode=" + reorderMode);
if (mWindowManager.getRecentsAnimationController() == null) return;
+ // Just to be sure end the launch hint in case the target activity was never launched.
+ // However, if we're keeping the activity and making it visible, we can leave it on.
+ if (reorderMode != REORDER_KEEP_IN_PLACE) {
+ mStackSupervisor.sendPowerHintForLaunchEndIfNeeded();
+ }
+
mService.setRunningRemoteAnimation(mCallingPid, false);
mWindowManager.inSurfaceTransaction(() -> {
@@ -151,26 +199,50 @@ class RecentsAnimation implements RecentsAnimationCallbacks {
"RecentsAnimation#onAnimationFinished_inSurfaceTransaction");
mWindowManager.deferSurfaceLayout();
try {
- mWindowManager.cleanupRecentsAnimation(moveHomeToTop);
+ mWindowManager.cleanupRecentsAnimation(reorderMode);
- // Move the home stack to the front
- final ActivityRecord homeActivity = mStackSupervisor.getHomeActivity();
- if (homeActivity == null) {
+ final ActivityStack targetStack = mDefaultDisplay.getStack(
+ WINDOWING_MODE_UNDEFINED, mTargetActivityType);
+ final ActivityRecord targetActivity = targetStack.getTopActivity();
+ if (DEBUG) Slog.d(TAG, "onAnimationFinished(): targetStack=" + targetStack
+ + " targetActivity=" + targetActivity
+ + " mRestoreTargetBehindStack=" + mRestoreTargetBehindStack);
+ if (targetActivity == null) {
return;
}
// Restore the launched-behind state
- homeActivity.mLaunchTaskBehind = false;
+ targetActivity.mLaunchTaskBehind = false;
- if (moveHomeToTop) {
- // Bring the home stack to the front
- final ActivityStack homeStack = homeActivity.getStack();
- mStackSupervisor.mNoAnimActivities.add(homeActivity);
- homeStack.moveToFront("RecentsAnimation.onAnimationFinished()");
+ if (reorderMode == REORDER_MOVE_TO_TOP) {
+ // Bring the target stack to the front
+ mStackSupervisor.mNoAnimActivities.add(targetActivity);
+ targetStack.moveToFront("RecentsAnimation.onAnimationFinished()");
+ if (DEBUG) {
+ final ActivityStack topStack = getTopNonAlwaysOnTopStack();
+ if (topStack != targetStack) {
+ Slog.w(TAG, "Expected target stack=" + targetStack
+ + " to be top most but found stack=" + topStack);
+ }
+ }
+ } else if (reorderMode == REORDER_MOVE_TO_ORIGINAL_POSITION){
+ // Restore the target stack to its previous position
+ final ActivityDisplay display = targetActivity.getDisplay();
+ display.moveStackBehindStack(targetStack, mRestoreTargetBehindStack);
+ if (DEBUG) {
+ final ActivityStack aboveTargetStack =
+ mDefaultDisplay.getStackAbove(targetStack);
+ if (mRestoreTargetBehindStack != null
+ && aboveTargetStack != mRestoreTargetBehindStack) {
+ Slog.w(TAG, "Expected target stack=" + targetStack
+ + " to restored behind stack=" + mRestoreTargetBehindStack
+ + " but it is behind stack=" + aboveTargetStack);
+ }
+ }
} else {
- // Restore the home stack to its previous position
- final ActivityDisplay display = homeActivity.getDisplay();
- display.moveHomeStackBehindStack(mRestoreHomeBehindStack);
+ // Keep target stack in place, nothing changes, so ignore the transition
+ // logic below
+ return;
}
mWindowManager.prepareAppTransition(TRANSIT_NONE, false);
@@ -180,6 +252,15 @@ class RecentsAnimation implements RecentsAnimationCallbacks {
// No reason to wait for the pausing activity in this case, as the hiding of
// surfaces needs to be done immediately.
mWindowManager.executeAppTransition();
+
+ // After reordering the stacks, reset the minimized state. At this point, either
+ // the target activity is now top-most and we will stay minimized (if in
+ // split-screen), or we will have returned to the app, and the minimized state
+ // should be reset
+ mWindowManager.checkSplitScreenMinimizedChanged(true /* animate */);
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to clean up recents activity", e);
+ throw e;
} finally {
mWindowManager.continueSurfaceLayout();
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
@@ -198,4 +279,18 @@ class RecentsAnimation implements RecentsAnimationCallbacks {
Slog.e(TAG, "Failed to cancel recents animation before start", e);
}
}
+
+ /**
+ * @return The top stack that is not always-on-top.
+ */
+ private ActivityStack getTopNonAlwaysOnTopStack() {
+ for (int i = mDefaultDisplay.getChildCount() - 1; i >= 0; i--) {
+ final ActivityStack s = mDefaultDisplay.getChildAt(i);
+ if (s.getWindowConfiguration().isAlwaysOnTop()) {
+ continue;
+ }
+ return s;
+ }
+ return null;
+ }
}
diff --git a/com/android/server/am/SafeActivityOptions.java b/com/android/server/am/SafeActivityOptions.java
index ac6f01fa..2de75273 100644
--- a/com/android/server/am/SafeActivityOptions.java
+++ b/com/android/server/am/SafeActivityOptions.java
@@ -210,7 +210,7 @@ class SafeActivityOptions {
// Check if someone tries to launch an unwhitelisted activity into LockTask mode.
final boolean lockTaskMode = options.getLockTaskMode();
if (aInfo != null && lockTaskMode
- && !supervisor.mService.mLockTaskController.isPackageWhitelisted(
+ && !supervisor.mService.getLockTaskController().isPackageWhitelisted(
UserHandle.getUserId(callingUid), aInfo.packageName)) {
final String msg = "Permission Denial: starting " + getIntentString(intent)
+ " from " + callerApp + " (pid=" + callingPid
diff --git a/com/android/server/am/TaskRecord.java b/com/android/server/am/TaskRecord.java
index 034cb2e3..0e418ad4 100644
--- a/com/android/server/am/TaskRecord.java
+++ b/com/android/server/am/TaskRecord.java
@@ -451,7 +451,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
}
void removeWindowContainer() {
- mService.mLockTaskController.clearLockedTask(this);
+ mService.getLockTaskController().clearLockedTask(this);
mWindowContainerController.removeContainer();
if (!getWindowConfiguration().persistTaskBounds()) {
// Reset current bounds for task whose bounds shouldn't be persisted so it uses
@@ -927,7 +927,26 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
if (stack != null && !stack.isInStackLocked(this)) {
throw new IllegalStateException("Task must be added as a Stack child first.");
}
+ final ActivityStack oldStack = mStack;
mStack = stack;
+
+ // If the new {@link TaskRecord} is from a different {@link ActivityStack}, remove this
+ // {@link ActivityRecord} from its current {@link ActivityStack}.
+
+ if (oldStack != mStack) {
+ for (int i = getChildCount() - 1; i >= 0; --i) {
+ final ActivityRecord activity = getChildAt(i);
+
+ if (oldStack != null) {
+ oldStack.onActivityRemovedFromStack(activity);
+ }
+
+ if (mStack != null) {
+ stack.onActivityAddedToStack(activity);
+ }
+ }
+ }
+
onParentChanged();
}
@@ -1232,6 +1251,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
index = Math.min(size, index);
mActivities.add(index, r);
+
updateEffectiveIntent();
if (r.isPersistable()) {
mService.notifyTaskPersisterLocked(this, false);
@@ -1257,7 +1277,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
* @return true if this was the last activity in the task.
*/
boolean removeActivity(ActivityRecord r) {
- return removeActivity(r, false /*reparenting*/);
+ return removeActivity(r, false /* reparenting */);
}
boolean removeActivity(ActivityRecord r, boolean reparenting) {
@@ -1266,7 +1286,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
"Activity=" + r + " does not belong to task=" + this);
}
- r.setTask(null /*task*/, reparenting);
+ r.setTask(null /* task */, reparenting /* reparenting */);
if (mActivities.remove(r) && r.fullscreen) {
// Was previously in list.
@@ -1446,9 +1466,10 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
}
final String pkg = (realActivity != null) ? realActivity.getPackageName() : null;
+ final LockTaskController lockTaskController = mService.getLockTaskController();
switch (r.lockTaskLaunchMode) {
case LOCK_TASK_LAUNCH_MODE_DEFAULT:
- mLockTaskAuth = mService.mLockTaskController.isPackageWhitelisted(userId, pkg)
+ mLockTaskAuth = lockTaskController.isPackageWhitelisted(userId, pkg)
? LOCK_TASK_AUTH_WHITELISTED : LOCK_TASK_AUTH_PINNABLE;
break;
@@ -1461,7 +1482,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
break;
case LOCK_TASK_LAUNCH_MODE_IF_WHITELISTED:
- mLockTaskAuth = mService.mLockTaskController.isPackageWhitelisted(userId, pkg)
+ mLockTaskAuth = lockTaskController.isPackageWhitelisted(userId, pkg)
? LOCK_TASK_AUTH_LAUNCHABLE : LOCK_TASK_AUTH_PINNABLE;
break;
}
diff --git a/com/android/server/am/UserController.java b/com/android/server/am/UserController.java
index a2943346..fecb9345 100644
--- a/com/android/server/am/UserController.java
+++ b/com/android/server/am/UserController.java
@@ -1184,11 +1184,6 @@ class UserController implements Handler.Callback {
Slog.w(TAG, "No user info for user #" + targetUserId);
return false;
}
- if (!targetUserInfo.isDemo() && UserManager.isDeviceInDemoMode(mInjector.getContext())) {
- Slog.w(TAG, "Cannot switch to non-demo user #" + targetUserId
- + " when device is in demo mode");
- return false;
- }
if (!targetUserInfo.supportsSwitchTo()) {
Slog.w(TAG, "Cannot switch to User #" + targetUserId + ": not supported");
return false;
@@ -2220,7 +2215,7 @@ class UserController implements Handler.Callback {
protected void clearAllLockedTasks(String reason) {
synchronized (mService) {
- mService.mLockTaskController.clearLockedTasks(reason);
+ mService.getLockTaskController().clearLockedTasks(reason);
}
}
diff --git a/com/android/server/audio/AudioService.java b/com/android/server/audio/AudioService.java
index c8b6b505..82124636 100644
--- a/com/android/server/audio/AudioService.java
+++ b/com/android/server/audio/AudioService.java
@@ -320,13 +320,13 @@ public class AudioService extends IAudioService.Stub
0, // STREAM_SYSTEM
0, // STREAM_RING
0, // STREAM_MUSIC
- 0, // STREAM_ALARM
+ 1, // STREAM_ALARM
0, // STREAM_NOTIFICATION
0, // STREAM_BLUETOOTH_SCO
0, // STREAM_SYSTEM_ENFORCED
0, // STREAM_DTMF
0, // STREAM_TTS
- 0 // STREAM_ACCESSIBILITY
+ 1 // STREAM_ACCESSIBILITY
};
/* mStreamVolumeAlias[] indicates for each stream if it uses the volume settings
@@ -588,7 +588,7 @@ public class AudioService extends IAudioService.Stub
AudioSystem.DEVICE_OUT_HDMI_ARC |
AudioSystem.DEVICE_OUT_SPDIF |
AudioSystem.DEVICE_OUT_AUX_LINE;
- int mFullVolumeDevices = AudioSystem.DEVICE_OUT_HEARING_AID;
+ int mFullVolumeDevices = 0;
private final boolean mMonitorRotation;
@@ -1208,6 +1208,8 @@ public class AudioService extends IAudioService.Stub
System.VOLUME_SETTINGS_INT[a11yStreamAlias];
mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].setAllIndexes(
mStreamStates[a11yStreamAlias], caller);
+ mStreamStates[AudioSystem.STREAM_ACCESSIBILITY].refreshRange(
+ mStreamVolumeAlias[AudioSystem.STREAM_ACCESSIBILITY]);
}
}
if (sIndependentA11yVolume) {
@@ -1389,7 +1391,15 @@ public class AudioService extends IAudioService.Stub
}
private int rescaleIndex(int index, int srcStream, int dstStream) {
- return (index * mStreamStates[dstStream].getMaxIndex() + mStreamStates[srcStream].getMaxIndex() / 2) / mStreamStates[srcStream].getMaxIndex();
+ final int rescaled =
+ (index * mStreamStates[dstStream].getMaxIndex()
+ + mStreamStates[srcStream].getMaxIndex() / 2)
+ / mStreamStates[srcStream].getMaxIndex();
+ if (rescaled < mStreamStates[dstStream].getMinIndex()) {
+ return mStreamStates[dstStream].getMinIndex();
+ } else {
+ return rescaled;
+ }
}
///////////////////////////////////////////////////////////////////////////
@@ -4602,8 +4612,8 @@ public class AudioService extends IAudioService.Stub
// 4 VolumeStreamState.class
public class VolumeStreamState {
private final int mStreamType;
- private final int mIndexMin;
- private final int mIndexMax;
+ private int mIndexMin;
+ private int mIndexMax;
private boolean mIsMuted;
private String mVolumeIndexSettingName;
@@ -4755,6 +4765,8 @@ public class AudioService extends IAudioService.Stub
index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
} else if ((device & mFullVolumeDevices) != 0) {
index = (mIndexMax + 5)/10;
+ } else if ((device & AudioSystem.DEVICE_OUT_HEARING_AID) != 0) {
+ index = (mIndexMax + 5)/10;
} else {
index = (getIndex(device) + 5)/10;
}
@@ -4775,6 +4787,8 @@ public class AudioService extends IAudioService.Stub
index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
} else if ((device & mFullVolumeDevices) != 0) {
index = (mIndexMax + 5)/10;
+ } else if ((device & AudioSystem.DEVICE_OUT_HEARING_AID) != 0) {
+ index = (mIndexMax + 5)/10;
} else {
index = (mIndexMap.valueAt(i) + 5)/10;
}
@@ -4889,6 +4903,24 @@ public class AudioService extends IAudioService.Stub
}
/**
+ * Updates the min/max index values from another stream. Use this when changing the alias
+ * for the current stream type.
+ * @param sourceStreamType
+ */
+ // must be sync'd on mSettingsLock before VolumeStreamState.class
+ @GuardedBy("VolumeStreamState.class")
+ public void refreshRange(int sourceStreamType) {
+ mIndexMin = MIN_STREAM_VOLUME[sourceStreamType] * 10;
+ mIndexMax = MAX_STREAM_VOLUME[sourceStreamType] * 10;
+ // verify all current volumes are within bounds
+ for (int i = 0 ; i < mIndexMap.size(); i++) {
+ final int device = mIndexMap.keyAt(i);
+ final int index = mIndexMap.valueAt(i);
+ mIndexMap.put(device, getValidIndex(index));
+ }
+ }
+
+ /**
* Copies all device/index pairs from the given VolumeStreamState after initializing
* them with the volume for DEVICE_OUT_DEFAULT. No-op if the source VolumeStreamState
* has the same stream type as this instance.
diff --git a/com/android/server/autofill/Session.java b/com/android/server/autofill/Session.java
index e14584f9..06707daa 100644
--- a/com/android/server/autofill/Session.java
+++ b/com/android/server/autofill/Session.java
@@ -99,6 +99,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -1400,6 +1401,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
* the current values of all fields in the screen.
*/
if (saveInfo == null) {
+ if (sVerbose) Slog.w(TAG, "showSaveLocked(): no saveInfo from service");
return true;
}
@@ -1970,8 +1972,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return;
}
- if (value != null && !value.equals(viewState.getCurrentValue())) {
- if (value.isEmpty()
+ if (!Objects.equals(value, viewState.getCurrentValue())) {
+ if ((value == null || value.isEmpty())
&& viewState.getCurrentValue() != null
&& viewState.getCurrentValue().isText()
&& viewState.getCurrentValue().getTextValue() != null
@@ -1992,18 +1994,26 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// Must check if this update was caused by autofilling the view, in which
// case we just update the value, but not the UI.
final AutofillValue filledValue = viewState.getAutofilledValue();
- if (value.equals(filledValue)) {
+ if (filledValue != null && filledValue.equals(value)) {
+ if (sVerbose) {
+ Slog.v(TAG, "ignoring autofilled change on id " + id);
+ }
return;
}
// Update the internal state...
viewState.setState(ViewState.STATE_CHANGED);
//..and the UI
- if (value.isText()) {
- getUiForShowing().filterFillUi(value.getTextValue().toString(), this);
+ final String filterText;
+ if (value == null || !value.isText()) {
+ filterText = null;
} else {
- getUiForShowing().filterFillUi(null, this);
+ final CharSequence text = value.getTextValue();
+ // Text should never be null, but it doesn't hurt to check to avoid a
+ // system crash...
+ filterText = (text == null) ? null : text.toString();
}
+ getUiForShowing().filterFillUi(filterText, this);
}
break;
case ACTION_VIEW_ENTERED:
diff --git a/com/android/server/autofill/ViewState.java b/com/android/server/autofill/ViewState.java
index 9210de23..2a760555 100644
--- a/com/android/server/autofill/ViewState.java
+++ b/com/android/server/autofill/ViewState.java
@@ -237,12 +237,7 @@ final class ViewState {
}
pw.print(prefix); pw.print("state:" ); pw.println(getStateAsString());
if (mResponse != null) {
- pw.print(prefix); pw.print("response:");
- if (sVerbose) {
- pw.println(mResponse);
- } else {
- pw.print("id=");pw.println(mResponse.getRequestId());
- }
+ pw.print(prefix); pw.print("response id:");pw.println(mResponse.getRequestId());
}
if (mCurrentValue != null) {
pw.print(prefix); pw.print("currentValue:" ); pw.println(mCurrentValue);
diff --git a/com/android/server/autofill/ui/FillUi.java b/com/android/server/autofill/ui/FillUi.java
index 7c0671ff..d29ca051 100644
--- a/com/android/server/autofill/ui/FillUi.java
+++ b/com/android/server/autofill/ui/FillUi.java
@@ -656,6 +656,8 @@ final class FillUi {
private final WindowManager mWm;
private final View mContentView;
private boolean mShowing;
+ // Used on dump only
+ private WindowManager.LayoutParams mShowParams;
/**
* Constructor.
@@ -672,16 +674,13 @@ final class FillUi {
* Shows the window.
*/
public void show(WindowManager.LayoutParams params) {
+ mShowParams = params;
if (sVerbose) {
Slog.v(TAG, "show(): showing=" + mShowing + ", params=" + paramsToString(params));
}
try {
- // Okay here is a bit of voodoo - we want to show the window as system
- // controlled one so it covers app windows - adjust the params accordingly.
- params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
- params.token = null;
params.packageName = "android";
- params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+ params.setTitle("Autofill UI"); // Title is set for debugging purposes
if (!mShowing) {
params.accessibilityTitle = mContentView.getContext()
.getString(R.string.autofill_picker_accessibility_title);
@@ -760,6 +759,9 @@ final class FillUi {
pw.println();
pw.print(prefix2); pw.print("showing: "); pw.println(mWindow.mShowing);
pw.print(prefix2); pw.print("view: "); pw.println(mWindow.mContentView);
+ if (mWindow.mShowParams != null) {
+ pw.print(prefix2); pw.print("params: "); pw.println(mWindow.mShowParams);
+ }
pw.print(prefix2); pw.print("screen coordinates: ");
if (mWindow.mContentView == null) {
pw.println("N/A");
diff --git a/com/android/server/backup/BackupUtils.java b/com/android/server/backup/BackupUtils.java
index d8175345..96c56210 100644
--- a/com/android/server/backup/BackupUtils.java
+++ b/com/android/server/backup/BackupUtils.java
@@ -20,6 +20,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
import android.util.Slog;
import com.android.internal.util.ArrayUtils;
@@ -55,16 +56,16 @@ public class BackupUtils {
return false;
}
- Signature[][] deviceHistorySigs = target.signingCertificateHistory;
- if (ArrayUtils.isEmpty(deviceHistorySigs)) {
- Slog.w(TAG, "signingCertificateHistory is empty, app was either unsigned or the flag" +
+ SigningInfo signingInfo = target.signingInfo;
+ if (signingInfo == null) {
+ Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
" PackageManager#GET_SIGNING_CERTIFICATES was not specified");
return false;
}
if (DEBUG) {
Slog.v(TAG, "signaturesMatch(): stored=" + storedSigHashes
- + " device=" + deviceHistorySigs);
+ + " device=" + signingInfo.getApkContentsSigners());
}
final int nStored = storedSigHashes.size();
@@ -78,8 +79,9 @@ public class BackupUtils {
} else {
// the app couldn't have rotated keys, since it was signed with multiple sigs - do
// a check to see if we find a match for all stored sigs
- // since app hasn't rotated key, we only need to check with deviceHistorySigs[0]
- ArrayList<byte[]> deviceHashes = hashSignatureArray(deviceHistorySigs[0]);
+ // since app hasn't rotated key, we only need to check with current signers
+ ArrayList<byte[]> deviceHashes =
+ hashSignatureArray(signingInfo.getApkContentsSigners());
int nDevice = deviceHashes.size();
// ensure that each stored sig matches an on-device sig
for (int i = 0; i < nStored; i++) {
diff --git a/com/android/server/backup/KeyValueAdbRestoreEngine.java b/com/android/server/backup/KeyValueAdbRestoreEngine.java
index a2de8e73..fbec5cb2 100644
--- a/com/android/server/backup/KeyValueAdbRestoreEngine.java
+++ b/com/android/server/backup/KeyValueAdbRestoreEngine.java
@@ -64,8 +64,7 @@ public class KeyValueAdbRestoreEngine implements Runnable {
try {
File restoreData = prepareRestoreData(mInfo, mInFD);
- // TODO: version ?
- invokeAgentForAdbRestore(mAgent, mInfo, restoreData, 0);
+ invokeAgentForAdbRestore(mAgent, mInfo, restoreData);
} catch (IOException e) {
e.printStackTrace();
}
@@ -83,8 +82,8 @@ public class KeyValueAdbRestoreEngine implements Runnable {
return sortedDataName;
}
- private void invokeAgentForAdbRestore(IBackupAgent agent, FileMetadata info, File restoreData,
- int versionCode) throws IOException {
+ private void invokeAgentForAdbRestore(IBackupAgent agent, FileMetadata info, File restoreData)
+ throws IOException {
String pkg = info.packageName;
File newStateName = new File(mDataDir, pkg + ".new");
try {
@@ -95,9 +94,9 @@ public class KeyValueAdbRestoreEngine implements Runnable {
if (DEBUG) {
Slog.i(TAG, "Starting restore of package " + pkg + " for version code "
- + versionCode);
+ + info.version);
}
- agent.doRestore(backupData, versionCode, newState, mToken,
+ agent.doRestore(backupData, info.version, newState, mToken,
mBackupManagerService.getBackupManagerBinder());
} catch (IOException e) {
Slog.e(TAG, "Exception opening file. " + e);
diff --git a/com/android/server/backup/PackageManagerBackupAgent.java b/com/android/server/backup/PackageManagerBackupAgent.java
index dc28cd17..e4ce62d0 100644
--- a/com/android/server/backup/PackageManagerBackupAgent.java
+++ b/com/android/server/backup/PackageManagerBackupAgent.java
@@ -27,6 +27,7 @@ import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Slog;
@@ -240,12 +241,13 @@ public class PackageManagerBackupAgent extends BackupAgent {
PackageManager.GET_SIGNING_CERTIFICATES);
homeInstaller = mPackageManager.getInstallerPackageName(home.getPackageName());
homeVersion = homeInfo.getLongVersionCode();
- Signature[][] signingHistory = homeInfo.signingCertificateHistory;
- if (signingHistory == null || signingHistory.length == 0) {
- Slog.e(TAG, "Home app has no signing history");
+ SigningInfo signingInfo = homeInfo.signingInfo;
+ if (signingInfo == null) {
+ Slog.e(TAG, "Home app has no signing information");
} else {
// retrieve the newest sigs to back up
- Signature[] homeInfoSignatures = signingHistory[signingHistory.length - 1];
+ // TODO (b/73988180) use entire signing history in case of rollbacks
+ Signature[] homeInfoSignatures = signingInfo.getApkContentsSigners();
homeSigHashes = BackupUtils.hashSignatureArray(homeInfoSignatures);
}
} catch (NameNotFoundException e) {
@@ -334,8 +336,8 @@ public class PackageManagerBackupAgent extends BackupAgent {
}
}
- Signature[][] signingHistory = info.signingCertificateHistory;
- if (signingHistory == null || signingHistory.length == 0) {
+ SigningInfo signingInfo = info.signingInfo;
+ if (signingInfo == null) {
Slog.w(TAG, "Not backing up package " + packName
+ " since it appears to have no signatures.");
continue;
@@ -358,7 +360,7 @@ public class PackageManagerBackupAgent extends BackupAgent {
outputBufferStream.writeInt(info.versionCode);
}
// retrieve the newest sigs to back up
- Signature[] infoSignatures = signingHistory[signingHistory.length - 1];
+ Signature[] infoSignatures = signingInfo.getApkContentsSigners();
writeSignatureHashArray(outputBufferStream,
BackupUtils.hashSignatureArray(infoSignatures));
diff --git a/com/android/server/backup/restore/PerformAdbRestoreTask.java b/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 77163d34..0c99b440 100644
--- a/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -99,6 +99,7 @@ public class PerformAdbRestoreTask implements Runnable {
private FullBackupObbConnection mObbConnection = null;
private ParcelFileDescriptor[] mPipes = null;
private byte[] mWidgetData = null;
+ private long mAppVersion;
private long mBytes;
private final BackupAgentTimeoutParameters mAgentTimeoutParameters;
@@ -476,6 +477,9 @@ public class PerformAdbRestoreTask implements Runnable {
if (info.path.equals(BACKUP_MANIFEST_FILENAME)) {
Signature[] signatures = tarBackupReader.readAppManifestAndReturnSignatures(
info);
+ // readAppManifestAndReturnSignatures() will have extracted the version from
+ // the manifest, so we save it to use in key-value restore later.
+ mAppVersion = info.version;
PackageManagerInternal pmi = LocalServices.getService(
PackageManagerInternal.class);
RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy(
@@ -667,6 +671,8 @@ public class PerformAdbRestoreTask implements Runnable {
Slog.d(TAG, "Restoring key-value file for " + pkg
+ " : " + info.path);
}
+ // Set the version saved from manifest entry.
+ info.version = mAppVersion;
KeyValueAdbRestoreEngine restoreEngine =
new KeyValueAdbRestoreEngine(
mBackupManagerService,
diff --git a/com/android/server/backup/transport/TransportClient.java b/com/android/server/backup/transport/TransportClient.java
index fa881a97..e4dcb25c 100644
--- a/com/android/server/backup/transport/TransportClient.java
+++ b/com/android/server/backup/transport/TransportClient.java
@@ -439,8 +439,17 @@ public class TransportClient {
synchronized (mStateLock) {
log(Priority.ERROR, "Service disconnected: client UNUSABLE");
setStateLocked(State.UNUSABLE, null);
- // After unbindService() no calls back to mConnection
- mContext.unbindService(mConnection);
+ try {
+ // After unbindService() no calls back to mConnection
+ mContext.unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ // TODO: Investigate why this is happening
+ // We're UNUSABLE, so any calls to mConnection will be no-op, so it's safe to
+ // swallow this one
+ log(
+ Priority.WARN,
+ "Exception trying to unbind onServiceDisconnected(): " + e.getMessage());
+ }
}
}
diff --git a/com/android/server/backup/utils/AppBackupUtils.java b/com/android/server/backup/utils/AppBackupUtils.java
index 5518374e..c39cceb5 100644
--- a/com/android/server/backup/utils/AppBackupUtils.java
+++ b/com/android/server/backup/utils/AppBackupUtils.java
@@ -27,6 +27,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
import android.os.Process;
import android.util.Slog;
@@ -203,15 +204,16 @@ public class AppBackupUtils {
return false;
}
- Signature[][] deviceHistorySigs = target.signingCertificateHistory;
- if (ArrayUtils.isEmpty(deviceHistorySigs)) {
- Slog.w(TAG, "signingCertificateHistory is empty, app was either unsigned or the flag" +
+ SigningInfo signingInfo = target.signingInfo;
+ if (signingInfo == null) {
+ Slog.w(TAG, "signingInfo is empty, app was either unsigned or the flag" +
" PackageManager#GET_SIGNING_CERTIFICATES was not specified");
return false;
}
if (DEBUG) {
- Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device=" + deviceHistorySigs);
+ Slog.v(TAG, "signaturesMatch(): stored=" + storedSigs + " device="
+ + signingInfo.getApkContentsSigners());
}
final int nStored = storedSigs.length;
@@ -225,8 +227,8 @@ public class AppBackupUtils {
} else {
// the app couldn't have rotated keys, since it was signed with multiple sigs - do
// a check to see if we find a match for all stored sigs
- // since app hasn't rotated key, we only need to check with deviceHistorySigs[0]
- Signature[] deviceSigs = deviceHistorySigs[0];
+ // since app hasn't rotated key, we only need to check with its current signers
+ Signature[] deviceSigs = signingInfo.getApkContentsSigners();
int nDevice = deviceSigs.length;
// ensure that each stored sig matches an on-device sig
diff --git a/com/android/server/backup/utils/FullBackupUtils.java b/com/android/server/backup/utils/FullBackupUtils.java
index 994d5a96..a3d56011 100644
--- a/com/android/server/backup/utils/FullBackupUtils.java
+++ b/com/android/server/backup/utils/FullBackupUtils.java
@@ -22,6 +22,7 @@ import static com.android.server.backup.BackupManagerService.TAG;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Slog;
@@ -106,12 +107,13 @@ public class FullBackupUtils {
printer.println(withApk ? "1" : "0");
// write the signature block
- Signature[][] signingHistory = pkg.signingCertificateHistory;
- if (signingHistory == null) {
+ SigningInfo signingInfo = pkg.signingInfo;
+ if (signingInfo == null) {
printer.println("0");
} else {
// retrieve the newest sigs to write
- Signature[] signatures = signingHistory[signingHistory.length - 1];
+ // TODO (b/73988180) use entire signing history in case of rollbacks
+ Signature[] signatures = signingInfo.getApkContentsSigners();
printer.println(Integer.toString(signatures.length));
for (Signature sig : signatures) {
printer.println(sig.toCharsString());
diff --git a/com/android/server/connectivity/DnsManager.java b/com/android/server/connectivity/DnsManager.java
index 36f5a6c3..7aaac060 100644
--- a/com/android/server/connectivity/DnsManager.java
+++ b/com/android/server/connectivity/DnsManager.java
@@ -34,27 +34,28 @@ import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkUtils;
import android.net.Uri;
+import android.net.dns.ResolvUtil;
import android.os.Binder;
import android.os.INetworkManagementService;
-import android.os.Handler;
import android.os.UserHandle;
import android.provider.Settings;
-import android.system.GaiException;
-import android.system.OsConstants;
-import android.system.StructAddrinfo;
import android.text.TextUtils;
+import android.util.Pair;
import android.util.Slog;
import com.android.server.connectivity.MockableSystemProperties;
-import libcore.io.Libcore;
-
import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
+import java.util.Set;
import java.util.StringJoiner;
@@ -64,10 +65,56 @@ import java.util.StringJoiner;
* This class it NOT designed for concurrent access. Furthermore, all non-static
* methods MUST be called from ConnectivityService's thread.
*
+ * [ Private DNS ]
+ * The code handling Private DNS is spread across several components, but this
+ * seems like the least bad place to collect all the observations.
+ *
+ * Private DNS handling and updating occurs in response to several different
+ * events. Each is described here with its corresponding intended handling.
+ *
+ * [A] Event: A new network comes up.
+ * Mechanics:
+ * [1] ConnectivityService gets notifications from NetworkAgents.
+ * [2] in updateNetworkInfo(), the first time the NetworkAgent goes into
+ * into CONNECTED state, the Private DNS configuration is retrieved,
+ * programmed, and strict mode hostname resolution (if applicable) is
+ * enqueued in NetworkAgent's NetworkMonitor, via a call to
+ * handlePerNetworkPrivateDnsConfig().
+ * [3] Re-resolution of strict mode hostnames that fail to return any
+ * IP addresses happens inside NetworkMonitor; it sends itself a
+ * delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff
+ * schedule.
+ * [4] Successfully resolved hostnames are sent to ConnectivityService
+ * inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved
+ * IP addresses are programmed into netd via:
+ *
+ * updatePrivateDns() -> updateDnses()
+ *
+ * both of which make calls into DnsManager.
+ * [5] Upon a successful hostname resolution NetworkMonitor initiates a
+ * validation attempt in the form of a lookup for a one-time hostname
+ * that uses Private DNS.
+ *
+ * [B] Event: Private DNS settings are changed.
+ * Mechanics:
+ * [1] ConnectivityService gets notifications from its SettingsObserver.
+ * [2] handlePrivateDnsSettingsChanged() is called, which calls
+ * handlePerNetworkPrivateDnsConfig() and the process proceeds
+ * as if from A.3 above.
+ *
+ * [C] Event: An application calls ConnectivityManager#reportBadNetwork().
+ * Mechanics:
+ * [1] NetworkMonitor is notified and initiates a reevaluation, which
+ * always bypasses Private DNS.
+ * [2] Once completed, NetworkMonitor checks if strict mode is in operation
+ * and if so enqueues another evaluation of Private DNS, as if from
+ * step A.5 above.
+ *
* @hide
*/
public class DnsManager {
private static final String TAG = DnsManager.class.getSimpleName();
+ private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig();
/* Defaults for resolver parameters. */
private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
@@ -126,35 +173,104 @@ public class DnsManager {
}
public static PrivateDnsConfig tryBlockingResolveOf(Network network, String name) {
- final StructAddrinfo hints = new StructAddrinfo();
- // Unnecessary, but expressly no AI_ADDRCONFIG.
- hints.ai_flags = 0;
- // Fetch all IP addresses at once to minimize re-resolution.
- hints.ai_family = OsConstants.AF_UNSPEC;
- hints.ai_socktype = OsConstants.SOCK_DGRAM;
-
try {
- final InetAddress[] ips = Libcore.os.android_getaddrinfo(name, hints, network.netId);
- if (ips != null && ips.length > 0) {
- return new PrivateDnsConfig(name, ips);
- }
- } catch (GaiException ignored) {}
-
- return null;
+ final InetAddress[] ips = ResolvUtil.blockingResolveAllLocally(network, name);
+ return new PrivateDnsConfig(name, ips);
+ } catch (UnknownHostException uhe) {
+ return new PrivateDnsConfig(name, null);
+ }
}
public static Uri[] getPrivateDnsSettingsUris() {
- final Uri[] uris = new Uri[2];
- uris[0] = Settings.Global.getUriFor(PRIVATE_DNS_MODE);
- uris[1] = Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER);
- return uris;
+ return new Uri[]{
+ Settings.Global.getUriFor(PRIVATE_DNS_MODE),
+ Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER),
+ };
+ }
+
+ public static class PrivateDnsValidationUpdate {
+ final public int netId;
+ final public InetAddress ipAddress;
+ final public String hostname;
+ final public boolean validated;
+
+ public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress,
+ String hostname, boolean validated) {
+ this.netId = netId;
+ this.ipAddress = ipAddress;
+ this.hostname = hostname;
+ this.validated = validated;
+ }
+ }
+
+ private static class PrivateDnsValidationStatuses {
+ enum ValidationStatus {
+ IN_PROGRESS,
+ FAILED,
+ SUCCEEDED
+ }
+
+ // Validation statuses of <hostname, ipAddress> pairs for a single netId
+ private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap;
+
+ private PrivateDnsValidationStatuses() {
+ mValidationMap = new HashMap<>();
+ }
+
+ private boolean hasValidatedServer() {
+ for (ValidationStatus status : mValidationMap.values()) {
+ if (status == ValidationStatus.SUCCEEDED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void updateTrackedDnses(String[] ipAddresses, String hostname) {
+ Set<Pair<String, InetAddress>> latestDnses = new HashSet<>();
+ for (String ipAddress : ipAddresses) {
+ try {
+ latestDnses.add(new Pair(hostname,
+ InetAddress.parseNumericAddress(ipAddress)));
+ } catch (IllegalArgumentException e) {}
+ }
+ // Remove <hostname, ipAddress> pairs that should not be tracked.
+ for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it =
+ mValidationMap.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next();
+ if (!latestDnses.contains(entry.getKey())) {
+ it.remove();
+ }
+ }
+ // Add new <hostname, ipAddress> pairs that should be tracked.
+ for (Pair<String, InetAddress> p : latestDnses) {
+ if (!mValidationMap.containsKey(p)) {
+ mValidationMap.put(p, ValidationStatus.IN_PROGRESS);
+ }
+ }
+ }
+
+ private void updateStatus(PrivateDnsValidationUpdate update) {
+ Pair<String, InetAddress> p = new Pair(update.hostname,
+ update.ipAddress);
+ if (!mValidationMap.containsKey(p)) {
+ return;
+ }
+ if (update.validated) {
+ mValidationMap.put(p, ValidationStatus.SUCCEEDED);
+ } else {
+ mValidationMap.put(p, ValidationStatus.FAILED);
+ }
+ }
}
private final Context mContext;
private final ContentResolver mContentResolver;
private final INetworkManagementService mNMS;
private final MockableSystemProperties mSystemProperties;
+ // TODO: Replace these Maps with SparseArrays.
private final Map<Integer, PrivateDnsConfig> mPrivateDnsMap;
+ private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
private int mNumDnsEntries;
private int mSampleValidity;
@@ -170,6 +286,7 @@ public class DnsManager {
mNMS = nms;
mSystemProperties = sp;
mPrivateDnsMap = new HashMap<>();
+ mPrivateDnsValidationMap = new HashMap<>();
// TODO: Create and register ContentObservers to track every setting
// used herein, posting messages to respond to changes.
@@ -181,6 +298,7 @@ public class DnsManager {
public void removeNetwork(Network network) {
mPrivateDnsMap.remove(network.netId);
+ mPrivateDnsValidationMap.remove(network.netId);
}
public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
@@ -190,6 +308,40 @@ public class DnsManager {
: mPrivateDnsMap.remove(network.netId);
}
+ public void updatePrivateDnsStatus(int netId, LinkProperties lp) {
+ // Use the PrivateDnsConfig data pushed to this class instance
+ // from ConnectivityService.
+ final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+ PRIVATE_DNS_OFF);
+
+ final boolean useTls = privateDnsCfg.useTls;
+ final boolean strictMode = privateDnsCfg.inStrictMode();
+ final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";
+
+ if (strictMode) {
+ lp.setUsePrivateDns(true);
+ lp.setPrivateDnsServerName(tlsHostname);
+ } else if (useTls) {
+ // We are in opportunistic mode. Private DNS should be used if there
+ // is a known DNS-over-TLS validated server.
+ boolean validated = mPrivateDnsValidationMap.containsKey(netId) &&
+ mPrivateDnsValidationMap.get(netId).hasValidatedServer();
+ lp.setUsePrivateDns(validated);
+ lp.setPrivateDnsServerName(null);
+ } else {
+ // Private DNS is disabled.
+ lp.setUsePrivateDns(false);
+ lp.setPrivateDnsServerName(null);
+ }
+ }
+
+ public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) {
+ final PrivateDnsValidationStatuses statuses =
+ mPrivateDnsValidationMap.get(update.netId);
+ if (statuses == null) return;
+ statuses.updateStatus(update);
+ }
+
public void setDnsConfigurationForNetwork(
int netId, LinkProperties lp, boolean isDefaultNetwork) {
final String[] assignedServers = NetworkUtils.makeStrings(lp.getDnsServers());
@@ -203,12 +355,13 @@ public class DnsManager {
// NetworkMonitor to decide which networks need validation and runs the
// blocking calls to resolve Private DNS strict mode hostnames.
//
- // At this time we do attempt to enable Private DNS on non-Internet
+ // At this time we do not attempt to enable Private DNS on non-Internet
// networks like IMS.
- final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.get(netId);
+ final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+ PRIVATE_DNS_OFF);
- final boolean useTls = (privateDnsCfg != null) && privateDnsCfg.useTls;
- final boolean strictMode = (privateDnsCfg != null) && privateDnsCfg.inStrictMode();
+ final boolean useTls = privateDnsCfg.useTls;
+ final boolean strictMode = privateDnsCfg.inStrictMode();
final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";
final String[] tlsServers =
strictMode ? NetworkUtils.makeStrings(
@@ -218,6 +371,17 @@ public class DnsManager {
: useTls ? assignedServers // Opportunistic
: new String[0]; // Off
+ // Prepare to track the validation status of the DNS servers in the
+ // resolver config when private DNS is in opportunistic or strict mode.
+ if (useTls) {
+ if (!mPrivateDnsValidationMap.containsKey(netId)) {
+ mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses());
+ }
+ mPrivateDnsValidationMap.get(netId).updateTrackedDnses(tlsServers, tlsHostname);
+ } else {
+ mPrivateDnsValidationMap.remove(netId);
+ }
+
Slog.d(TAG, String.format("setDnsConfigurationForNetwork(%d, %s, %s, %s, %s, %s)",
netId, Arrays.toString(assignedServers), Arrays.toString(domainStrs),
Arrays.toString(params), tlsHostname, Arrays.toString(tlsServers)));
diff --git a/com/android/server/connectivity/MultipathPolicyTracker.java b/com/android/server/connectivity/MultipathPolicyTracker.java
index 4eb19306..6fa999cb 100644
--- a/com/android/server/connectivity/MultipathPolicyTracker.java
+++ b/com/android/server/connectivity/MultipathPolicyTracker.java
@@ -20,35 +20,63 @@ import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER;
import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkPolicy.LIMIT_DISABLED;
+import static android.net.NetworkPolicy.WARNING_DISABLED;
+import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
+import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
import android.app.usage.NetworkStatsManager;
import android.app.usage.NetworkStatsManager.UsageCallback;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
+import android.net.NetworkPolicy;
import android.net.NetworkPolicyManager;
import android.net.NetworkRequest;
import android.net.NetworkStats;
import android.net.NetworkTemplate;
import android.net.StringNetworkSpecifier;
+import android.os.BestClock;
import android.os.Handler;
+import android.os.SystemClock;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.telephony.TelephonyManager;
+import android.util.DataUnit;
import android.util.DebugUtils;
+import android.util.Pair;
+import android.util.Range;
import android.util.Slog;
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.net.NetworkPolicyManagerInternal;
-import com.android.server.net.NetworkPolicyManagerService;
import com.android.server.net.NetworkStatsManagerInternal;
-import java.util.Calendar;
+import java.time.Clock;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
/**
* Manages multipath data budgets.
@@ -69,6 +97,13 @@ public class MultipathPolicyTracker {
private final Context mContext;
private final Handler mHandler;
+ private final Clock mClock;
+ private final Dependencies mDeps;
+ private final ContentResolver mResolver;
+ private final ConfigChangeReceiver mConfigChangeReceiver;
+
+ @VisibleForTesting
+ final ContentObserver mSettingsObserver;
private ConnectivityManager mCM;
private NetworkPolicyManager mNPM;
@@ -77,12 +112,32 @@ public class MultipathPolicyTracker {
private NetworkCallback mMobileNetworkCallback;
private NetworkPolicyManager.Listener mPolicyListener;
- // STOPSHIP: replace this with a configurable mechanism.
- private static final long DEFAULT_DAILY_MULTIPATH_QUOTA = 2_500_000;
+
+ /**
+ * Divider to calculate opportunistic quota from user-set data limit or warning: 5% of user-set
+ * limit.
+ */
+ private static final int OPQUOTA_USER_SETTING_DIVIDER = 20;
+
+ public static class Dependencies {
+ public Clock getClock() {
+ return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
+ Clock.systemUTC());
+ }
+ }
public MultipathPolicyTracker(Context ctx, Handler handler) {
+ this(ctx, handler, new Dependencies());
+ }
+
+ public MultipathPolicyTracker(Context ctx, Handler handler, Dependencies deps) {
mContext = ctx;
mHandler = handler;
+ mClock = deps.getClock();
+ mDeps = deps;
+ mResolver = mContext.getContentResolver();
+ mSettingsObserver = new SettingsObserver(mHandler);
+ mConfigChangeReceiver = new ConfigChangeReceiver();
// Because we are initialized by the ConnectivityService constructor, we can't touch any
// connectivity APIs. Service initialization is done in start().
}
@@ -94,6 +149,14 @@ public class MultipathPolicyTracker {
registerTrackMobileCallback();
registerNetworkPolicyListener();
+ final Uri defaultSettingUri =
+ Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
+ mResolver.registerContentObserver(defaultSettingUri, false, mSettingsObserver);
+
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ mContext.registerReceiverAsUser(
+ mConfigChangeReceiver, UserHandle.ALL, intentFilter, null, mHandler);
}
public void shutdown() {
@@ -103,6 +166,8 @@ public class MultipathPolicyTracker {
t.shutdown();
}
mMultipathTrackers.clear();
+ mResolver.unregisterContentObserver(mSettingsObserver);
+ mContext.unregisterReceiver(mConfigChangeReceiver);
}
// Called on an arbitrary binder thread.
@@ -128,9 +193,11 @@ public class MultipathPolicyTracker {
private long mMultipathBudget;
private final NetworkTemplate mNetworkTemplate;
private final UsageCallback mUsageCallback;
+ private NetworkCapabilities mNetworkCapabilities;
public MultipathTracker(Network network, NetworkCapabilities nc) {
this.network = network;
+ this.mNetworkCapabilities = new NetworkCapabilities(nc);
try {
subId = Integer.parseInt(
((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString());
@@ -167,42 +234,111 @@ public class MultipathPolicyTracker {
updateMultipathBudget();
}
+ public void setNetworkCapabilities(NetworkCapabilities nc) {
+ mNetworkCapabilities = new NetworkCapabilities(nc);
+ }
+
+ // TODO: calculate with proper timezone information
private long getDailyNonDefaultDataUsage() {
- Calendar start = Calendar.getInstance();
- Calendar end = (Calendar) start.clone();
- start.set(Calendar.HOUR_OF_DAY, 0);
- start.set(Calendar.MINUTE, 0);
- start.set(Calendar.SECOND, 0);
- start.set(Calendar.MILLISECOND, 0);
+ final ZonedDateTime end =
+ ZonedDateTime.ofInstant(mClock.instant(), ZoneId.systemDefault());
+ final ZonedDateTime start = end.truncatedTo(ChronoUnit.DAYS);
+
+ final long bytes = getNetworkTotalBytes(
+ start.toInstant().toEpochMilli(),
+ end.toInstant().toEpochMilli());
+ if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes);
+ return bytes;
+ }
+ private long getNetworkTotalBytes(long start, long end) {
try {
- final long bytes = LocalServices.getService(NetworkStatsManagerInternal.class)
- .getNetworkTotalBytes(mNetworkTemplate, start.getTimeInMillis(),
- end.getTimeInMillis());
- if (DBG) Slog.d(TAG, "Non-default data usage: " + bytes);
- return bytes;
+ return LocalServices.getService(NetworkStatsManagerInternal.class)
+ .getNetworkTotalBytes(mNetworkTemplate, start, end);
} catch (RuntimeException e) {
Slog.w(TAG, "Failed to get data usage: " + e);
return -1;
}
}
+ private NetworkIdentity getTemplateMatchingNetworkIdentity(NetworkCapabilities nc) {
+ return new NetworkIdentity(
+ ConnectivityManager.TYPE_MOBILE,
+ 0 /* subType, unused for template matching */,
+ subscriberId,
+ null /* networkId, unused for matching mobile networks */,
+ !nc.hasCapability(NET_CAPABILITY_NOT_ROAMING),
+ !nc.hasCapability(NET_CAPABILITY_NOT_METERED),
+ false /* defaultNetwork, templates should have DEFAULT_NETWORK_ALL */);
+ }
+
+ private long getRemainingDailyBudget(long limitBytes,
+ Range<ZonedDateTime> cycle) {
+ final long start = cycle.getLower().toInstant().toEpochMilli();
+ final long end = cycle.getUpper().toInstant().toEpochMilli();
+ final long totalBytes = getNetworkTotalBytes(start, end);
+ final long remainingBytes = totalBytes == -1 ? 0 : Math.max(0, limitBytes - totalBytes);
+ // 1 + ((end - now - 1) / millisInDay with integers is equivalent to:
+ // ceil((double)(end - now) / millisInDay)
+ final long remainingDays =
+ 1 + ((end - mClock.millis() - 1) / TimeUnit.DAYS.toMillis(1));
+
+ return remainingBytes / Math.max(1, remainingDays);
+ }
+
+ private long getUserPolicyOpportunisticQuotaBytes() {
+ // Keep the most restrictive applicable policy
+ long minQuota = Long.MAX_VALUE;
+ final NetworkIdentity identity = getTemplateMatchingNetworkIdentity(
+ mNetworkCapabilities);
+
+ final NetworkPolicy[] policies = mNPM.getNetworkPolicies();
+ for (NetworkPolicy policy : policies) {
+ if (policy.hasCycle() && policy.template.matches(identity)) {
+ final long cycleStart = policy.cycleIterator().next().getLower()
+ .toInstant().toEpochMilli();
+ // Prefer user-defined warning, otherwise use hard limit
+ final long activeWarning = getActiveWarning(policy, cycleStart);
+ final long policyBytes = (activeWarning == WARNING_DISABLED)
+ ? getActiveLimit(policy, cycleStart)
+ : activeWarning;
+
+ if (policyBytes != LIMIT_DISABLED && policyBytes != WARNING_DISABLED) {
+ final long policyBudget = getRemainingDailyBudget(policyBytes,
+ policy.cycleIterator().next());
+ minQuota = Math.min(minQuota, policyBudget);
+ }
+ }
+ }
+
+ if (minQuota == Long.MAX_VALUE) {
+ return OPPORTUNISTIC_QUOTA_UNKNOWN;
+ }
+
+ return minQuota / OPQUOTA_USER_SETTING_DIVIDER;
+ }
+
void updateMultipathBudget() {
long quota = LocalServices.getService(NetworkPolicyManagerInternal.class)
.getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
- if (quota == NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN) {
- // STOPSHIP: replace this with a configurable mechanism.
- quota = DEFAULT_DAILY_MULTIPATH_QUOTA;
+ // Fallback to user settings-based quota if not available from phone plan
+ if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
+ quota = getUserPolicyOpportunisticQuotaBytes();
+ if (DBG) Slog.d(TAG, "Opportunistic quota from user policy: " + quota + " bytes");
+ }
+
+ if (quota == OPPORTUNISTIC_QUOTA_UNKNOWN) {
+ quota = getDefaultDailyMultipathQuotaBytes();
if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
}
+ // TODO: re-register if day changed: budget may have run out but should be refreshed.
if (haveMultipathBudget() && quota == mQuota) {
- // If we already have a usage callback pending , there's no need to re-register it
+ // If there is already a usage callback pending , there's no need to re-register it
// if the quota hasn't changed. The callback will simply fire as expected when the
- // budget is spent. Also: if we re-register the callback when we're below the
- // UsageCallback's minimum value of 2MB, we'll overshoot the budget.
+ // budget is spent.
if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating.");
return;
}
@@ -212,7 +348,17 @@ public class MultipathPolicyTracker {
// ourselves any budget to work with.
final long usage = getDailyNonDefaultDataUsage();
final long budget = (usage == -1) ? 0 : Math.max(0, quota - usage);
- if (budget > 0) {
+
+ // Only consider budgets greater than MIN_THRESHOLD_BYTES, otherwise the callback will
+ // fire late, after data usage went over budget. Also budget should be 0 if remaining
+ // data is close to 0.
+ // This is necessary because the usage callback does not accept smaller thresholds.
+ // Because it snaps everything to MIN_THRESHOLD_BYTES, the lesser of the two evils is
+ // to snap to 0 here.
+ // This will only be called if the total quota for the day changed, not if usage changed
+ // since last time, so even if this is called very often the budget will not snap to 0
+ // as soon as there are less than 2MB left for today.
+ if (budget > NetworkStatsManager.MIN_THRESHOLD_BYTES) {
if (DBG) Slog.d(TAG, "Setting callback for " + budget +
" bytes on network " + network);
registerUsageCallback(budget);
@@ -262,11 +408,38 @@ public class MultipathPolicyTracker {
}
}
+ private static long getActiveWarning(NetworkPolicy policy, long cycleStart) {
+ return policy.lastWarningSnooze < cycleStart
+ ? policy.warningBytes
+ : WARNING_DISABLED;
+ }
+
+ private static long getActiveLimit(NetworkPolicy policy, long cycleStart) {
+ return policy.lastLimitSnooze < cycleStart
+ ? policy.limitBytes
+ : LIMIT_DISABLED;
+ }
+
// Only ever updated on the handler thread. Accessed from other binder threads to retrieve
// the tracker for a specific network.
private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers =
new ConcurrentHashMap<>();
+ private long getDefaultDailyMultipathQuotaBytes() {
+ final String setting = Settings.Global.getString(mContext.getContentResolver(),
+ NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES);
+ if (setting != null) {
+ try {
+ return Long.parseLong(setting);
+ } catch(NumberFormatException e) {
+ // fall through
+ }
+ }
+
+ return mContext.getResources().getInteger(
+ R.integer.config_networkDefaultDailyMultipathQuotaBytes);
+ }
+
// TODO: this races with app code that might respond to onAvailable() by immediately calling
// getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly
// invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its
@@ -281,6 +454,7 @@ public class MultipathPolicyTracker {
public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
MultipathTracker existing = mMultipathTrackers.get(network);
if (existing != null) {
+ existing.setNetworkCapabilities(nc);
existing.updateMultipathBudget();
return;
}
@@ -307,6 +481,15 @@ public class MultipathPolicyTracker {
mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler);
}
+ /**
+ * Update multipath budgets for all trackers. To be called on the mHandler thread.
+ */
+ private void updateAllMultipathBudgets() {
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ t.updateMultipathBudget();
+ }
+ }
+
private void maybeUnregisterTrackMobileCallback() {
if (mMobileNetworkCallback != null) {
mCM.unregisterNetworkCallback(mMobileNetworkCallback);
@@ -319,11 +502,7 @@ public class MultipathPolicyTracker {
@Override
public void onMeteredIfacesChanged(String[] meteredIfaces) {
// Dispatched every time opportunistic quota is recalculated.
- mHandler.post(() -> {
- for (MultipathTracker t : mMultipathTrackers.values()) {
- t.updateMultipathBudget();
- }
- });
+ mHandler.post(() -> updateAllMultipathBudgets());
}
};
mNPM.registerListener(mPolicyListener);
@@ -333,6 +512,35 @@ public class MultipathPolicyTracker {
mNPM.unregisterListener(mPolicyListener);
}
+ private final class SettingsObserver extends ContentObserver {
+ public SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Slog.wtf(TAG, "Should never be reached.");
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (!Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES)
+ .equals(uri)) {
+ Slog.wtf(TAG, "Unexpected settings observation: " + uri);
+ }
+ if (DBG) Slog.d(TAG, "Settings change: updating budgets.");
+ updateAllMultipathBudgets();
+ }
+ }
+
+ private final class ConfigChangeReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DBG) Slog.d(TAG, "Configuration change: updating budgets.");
+ updateAllMultipathBudgets();
+ }
+ }
+
public void dump(IndentingPrintWriter pw) {
// Do not use in production. Access to class data is only safe on the handler thrad.
pw.println("MultipathPolicyTracker:");
diff --git a/com/android/server/connectivity/NetworkMonitor.java b/com/android/server/connectivity/NetworkMonitor.java
index 8a2e71c1..28453834 100644
--- a/com/android/server/connectivity/NetworkMonitor.java
+++ b/com/android/server/connectivity/NetworkMonitor.java
@@ -34,6 +34,7 @@ import android.net.NetworkRequest;
import android.net.ProxyInfo;
import android.net.TrafficStats;
import android.net.Uri;
+import android.net.dns.ResolvUtil;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.NetworkEvent;
import android.net.metrics.ValidationProbeEvent;
@@ -64,6 +65,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -77,6 +79,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Random;
+import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -165,7 +168,7 @@ public class NetworkMonitor extends StateMachine {
* Force evaluation even if it has succeeded in the past.
* arg1 = UID responsible for requesting this reeval. Will be billed for data.
*/
- public static final int CMD_FORCE_REEVALUATION = BASE + 8;
+ private static final int CMD_FORCE_REEVALUATION = BASE + 8;
/**
* Message to self indicating captive portal app finished.
@@ -205,9 +208,15 @@ public class NetworkMonitor extends StateMachine {
* Private DNS. If a DNS resolution is required, e.g. for DNS-over-TLS in
* strict mode, then an event is sent back to ConnectivityService with the
* result of the resolution attempt.
+ *
+ * A separate message is used to trigger (re)evaluation of the Private DNS
+ * configuration, so that the message can be handled as needed in different
+ * states, including being ignored until after an ongoing captive portal
+ * validation phase is completed.
*/
private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = BASE + 13;
public static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = BASE + 14;
+ private static final int CMD_EVALUATE_PRIVATE_DNS = BASE + 15;
// Start mReevaluateDelayMs at this value and double.
private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
@@ -215,6 +224,7 @@ public class NetworkMonitor extends StateMachine {
// Before network has been evaluated this many times, ignore repeated reevaluate requests.
private static final int IGNORE_REEVALUATE_ATTEMPTS = 5;
private int mReevaluateToken = 0;
+ private static final int NO_UID = 0;
private static final int INVALID_UID = -1;
private int mUidResponsibleForReeval = INVALID_UID;
// Stop blaming UID that requested re-evaluation after this many attempts.
@@ -224,6 +234,8 @@ public class NetworkMonitor extends StateMachine {
private static final int NUM_VALIDATION_LOG_LINES = 20;
+ private String mPrivateDnsProviderHostname = "";
+
public static boolean isValidationRequired(
NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
// TODO: Consider requiring validation for DUN networks.
@@ -261,13 +273,12 @@ public class NetworkMonitor extends StateMachine {
public boolean systemReady = false;
- private DnsManager.PrivateDnsConfig mPrivateDnsCfg = null;
-
private final State mDefaultState = new DefaultState();
private final State mValidatedState = new ValidatedState();
private final State mMaybeNotifyState = new MaybeNotifyState();
private final State mEvaluatingState = new EvaluatingState();
private final State mCaptivePortalState = new CaptivePortalState();
+ private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState();
private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
@@ -293,6 +304,10 @@ public class NetworkMonitor extends StateMachine {
// Add suffix indicating which NetworkMonitor we're talking about.
super(TAG + networkAgentInfo.name());
+ // Logs with a tag of the form given just above, e.g.
+ // <timestamp> 862 2402 D NetworkMonitor/NetworkAgentInfo [WIFI () - 100]: ...
+ setDbg(VDBG);
+
mContext = context;
mMetricsLog = logger;
mConnectivityServiceHandler = handler;
@@ -305,10 +320,11 @@ public class NetworkMonitor extends StateMachine {
mDefaultRequest = defaultRequest;
addState(mDefaultState);
- addState(mValidatedState, mDefaultState);
addState(mMaybeNotifyState, mDefaultState);
addState(mEvaluatingState, mMaybeNotifyState);
addState(mCaptivePortalState, mMaybeNotifyState);
+ addState(mEvaluatingPrivateDnsState, mDefaultState);
+ addState(mValidatedState, mDefaultState);
setInitialState(mDefaultState);
mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
@@ -321,6 +337,17 @@ public class NetworkMonitor extends StateMachine {
start();
}
+ public void forceReevaluation(int responsibleUid) {
+ sendMessage(CMD_FORCE_REEVALUATION, responsibleUid, 0);
+ }
+
+ public void notifyPrivateDnsSettingsChanged(PrivateDnsConfig newCfg) {
+ // Cancel any outstanding resolutions.
+ removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
+ // Send the update to the proper thread.
+ sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
+ }
+
@Override
protected void log(String s) {
if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s);
@@ -349,6 +376,12 @@ public class NetworkMonitor extends StateMachine {
mDefaultRequest.networkCapabilities, mNetworkAgentInfo.networkCapabilities);
}
+
+ private void notifyNetworkTestResultInvalid(Object obj) {
+ mConnectivityServiceHandler.sendMessage(obtainMessage(
+ EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, obj));
+ }
+
// DefaultState is the parent of all States. It exists only to handle CMD_* messages but
// does not entail any real state (hence no enter() or exit() routines).
private class DefaultState extends State {
@@ -392,41 +425,66 @@ public class NetworkMonitor extends StateMachine {
switch (message.arg1) {
case APP_RETURN_DISMISSED:
- sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0);
+ sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0);
break;
case APP_RETURN_WANTED_AS_IS:
mDontDisplaySigninNotification = true;
// TODO: Distinguish this from a network that actually validates.
- // Displaying the "!" on the system UI icon may still be a good idea.
- transitionTo(mValidatedState);
+ // Displaying the "x" on the system UI icon may still be a good idea.
+ transitionTo(mEvaluatingPrivateDnsState);
break;
case APP_RETURN_UNWANTED:
mDontDisplaySigninNotification = true;
mUserDoesNotWant = true;
- mConnectivityServiceHandler.sendMessage(obtainMessage(
- EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID,
- mNetId, null));
+ notifyNetworkTestResultInvalid(null);
// TODO: Should teardown network.
mUidResponsibleForReeval = 0;
transitionTo(mEvaluatingState);
break;
}
return HANDLED;
- case CMD_PRIVATE_DNS_SETTINGS_CHANGED:
- if (isValidationRequired()) {
- // This performs a blocking DNS resolution of the
- // strict mode hostname, if required.
- resolvePrivateDnsConfig((DnsManager.PrivateDnsConfig) message.obj);
- if ((mPrivateDnsCfg != null) && mPrivateDnsCfg.inStrictMode()) {
- mConnectivityServiceHandler.sendMessage(obtainMessage(
- EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId,
- new DnsManager.PrivateDnsConfig(mPrivateDnsCfg)));
- }
+ case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
+ final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
+ if (!isValidationRequired() || cfg == null || !cfg.inStrictMode()) {
+ // No DNS resolution required.
+ //
+ // We don't force any validation in opportunistic mode
+ // here. Opportunistic mode nameservers are validated
+ // separately within netd.
+ //
+ // Reset Private DNS settings state.
+ mPrivateDnsProviderHostname = "";
+ break;
}
- return HANDLED;
+
+ mPrivateDnsProviderHostname = cfg.hostname;
+
+ // DNS resolutions via Private DNS strict mode block for a
+ // few seconds (~4.2) checking for any IP addresses to
+ // arrive and validate. Initiating a (re)evaluation now
+ // should not significantly alter the validation outcome.
+ //
+ // No matter what: enqueue a validation request; one of
+ // three things can happen with this request:
+ // [1] ignored (EvaluatingState or CaptivePortalState)
+ // [2] transition to EvaluatingPrivateDnsState
+ // (DefaultState and ValidatedState)
+ // [3] handled (EvaluatingPrivateDnsState)
+ //
+ // The Private DNS configuration to be evaluated will:
+ // [1] be skipped (not in strict mode), or
+ // [2] validate (huzzah), or
+ // [3] encounter some problem (invalid hostname,
+ // no resolved IP addresses, IPs unreachable,
+ // port 853 unreachable, port 853 is not running a
+ // DNS-over-TLS server, et cetera).
+ sendMessage(CMD_EVALUATE_PRIVATE_DNS);
+ break;
+ }
default:
- return HANDLED;
+ break;
}
+ return HANDLED;
}
}
@@ -440,7 +498,7 @@ public class NetworkMonitor extends StateMachine {
maybeLogEvaluationResult(
networkEventType(validationStage(), EvaluationResult.VALIDATED));
mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
- NETWORK_TEST_RESULT_VALID, mNetId, mPrivateDnsCfg));
+ NETWORK_TEST_RESULT_VALID, mNetId, null));
mValidations++;
}
@@ -449,10 +507,14 @@ public class NetworkMonitor extends StateMachine {
switch (message.what) {
case CMD_NETWORK_CONNECTED:
transitionTo(mValidatedState);
- return HANDLED;
+ break;
+ case CMD_EVALUATE_PRIVATE_DNS:
+ transitionTo(mEvaluatingPrivateDnsState);
+ break;
default:
return NOT_HANDLED;
}
+ return HANDLED;
}
}
@@ -569,11 +631,11 @@ public class NetworkMonitor extends StateMachine {
case CMD_REEVALUATE:
if (message.arg1 != mReevaluateToken || mUserDoesNotWant)
return HANDLED;
- // Don't bother validating networks that don't satisify the default request.
+ // Don't bother validating networks that don't satisfy the default request.
// This includes:
// - VPNs which can be considered explicitly desired by the user and the
// user's desire trumps whether the network validates.
- // - Networks that don't provide internet access. It's unclear how to
+ // - Networks that don't provide Internet access. It's unclear how to
// validate such networks.
// - Untrusted networks. It's unsafe to prompt the user to sign-in to
// such networks and the user didn't express interest in connecting to
@@ -588,7 +650,6 @@ public class NetworkMonitor extends StateMachine {
// expensive metered network, or unwanted leaking of the User Agent string.
if (!isValidationRequired()) {
validationLog("Network would not satisfy default request, not validating");
- mPrivateDnsCfg = null;
transitionTo(mValidatedState);
return HANDLED;
}
@@ -601,20 +662,18 @@ public class NetworkMonitor extends StateMachine {
// if this is found to cause problems.
CaptivePortalProbeResult probeResult = isCaptivePortal();
if (probeResult.isSuccessful()) {
- resolvePrivateDnsConfig();
- transitionTo(mValidatedState);
+ // Transit EvaluatingPrivateDnsState to get to Validated
+ // state (even if no Private DNS validation required).
+ transitionTo(mEvaluatingPrivateDnsState);
} else if (probeResult.isPortal()) {
- mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
- NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.redirectUrl));
+ notifyNetworkTestResultInvalid(probeResult.redirectUrl);
mLastPortalProbeResult = probeResult;
transitionTo(mCaptivePortalState);
} else {
final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
sendMessageDelayed(msg, mReevaluateDelayMs);
logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
- mConnectivityServiceHandler.sendMessage(obtainMessage(
- EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId,
- probeResult.redirectUrl));
+ notifyNetworkTestResultInvalid(probeResult.redirectUrl);
if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
// Don't continue to blame UID forever.
TrafficStats.clearThreadStatsUid();
@@ -700,6 +759,110 @@ public class NetworkMonitor extends StateMachine {
}
}
+ private class EvaluatingPrivateDnsState extends State {
+ private int mPrivateDnsReevalDelayMs;
+ private PrivateDnsConfig mPrivateDnsConfig;
+
+ @Override
+ public void enter() {
+ mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
+ mPrivateDnsConfig = null;
+ sendMessage(CMD_EVALUATE_PRIVATE_DNS);
+ }
+
+ @Override
+ public boolean processMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_EVALUATE_PRIVATE_DNS:
+ if (inStrictMode()) {
+ if (!isStrictModeHostnameResolved()) {
+ resolveStrictModeHostname();
+
+ if (isStrictModeHostnameResolved()) {
+ notifyPrivateDnsConfigResolved();
+ } else {
+ handlePrivateDnsEvaluationFailure();
+ break;
+ }
+ }
+
+ // Look up a one-time hostname, to bypass caching.
+ //
+ // Note that this will race with ConnectivityService
+ // code programming the DNS-over-TLS server IP addresses
+ // into netd (if invoked, above). If netd doesn't know
+ // the IP addresses yet, or if the connections to the IP
+ // addresses haven't yet been validated, netd will block
+ // for up to a few seconds before failing the lookup.
+ if (!sendPrivateDnsProbe()) {
+ handlePrivateDnsEvaluationFailure();
+ break;
+ }
+ }
+
+ // All good!
+ transitionTo(mValidatedState);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ private boolean inStrictMode() {
+ return !TextUtils.isEmpty(mPrivateDnsProviderHostname);
+ }
+
+ private boolean isStrictModeHostnameResolved() {
+ return (mPrivateDnsConfig != null) &&
+ mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname) &&
+ (mPrivateDnsConfig.ips.length > 0);
+ }
+
+ private void resolveStrictModeHostname() {
+ try {
+ // Do a blocking DNS resolution using the network-assigned nameservers.
+ mPrivateDnsConfig = new PrivateDnsConfig(
+ mPrivateDnsProviderHostname,
+ mNetwork.getAllByName(mPrivateDnsProviderHostname));
+ } catch (UnknownHostException uhe) {
+ mPrivateDnsConfig = null;
+ }
+ }
+
+ private void notifyPrivateDnsConfigResolved() {
+ mConnectivityServiceHandler.sendMessage(obtainMessage(
+ EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId, mPrivateDnsConfig));
+ }
+
+ private void handlePrivateDnsEvaluationFailure() {
+ notifyNetworkTestResultInvalid(null);
+
+ // Queue up a re-evaluation with backoff.
+ //
+ // TODO: Consider abandoning this state after a few attempts and
+ // transitioning back to EvaluatingState, to perhaps give ourselves
+ // the opportunity to (re)detect a captive portal or something.
+ sendMessageDelayed(CMD_EVALUATE_PRIVATE_DNS, mPrivateDnsReevalDelayMs);
+ mPrivateDnsReevalDelayMs *= 2;
+ if (mPrivateDnsReevalDelayMs > MAX_REEVALUATE_DELAY_MS) {
+ mPrivateDnsReevalDelayMs = MAX_REEVALUATE_DELAY_MS;
+ }
+ }
+
+ private boolean sendPrivateDnsProbe() {
+ // q.v. system/netd/server/dns/DnsTlsTransport.cpp
+ final String ONE_TIME_HOSTNAME_SUFFIX = "-dnsotls-ds.metric.gstatic.com";
+ final String host = UUID.randomUUID().toString().substring(0, 8) +
+ ONE_TIME_HOSTNAME_SUFFIX;
+ try {
+ final InetAddress[] ips = mNetworkAgentInfo.network().getAllByName(host);
+ return (ips != null && ips.length > 0);
+ } catch (UnknownHostException uhe) {}
+ return false;
+ }
+ }
+
// Limits the list of IP addresses returned by getAllByName or tried by openConnection to at
// most one per address family. This ensures we only wait up to 20 seconds for TCP connections
// to complete, regardless of how many IP addresses a host has.
@@ -710,7 +873,9 @@ public class NetworkMonitor extends StateMachine {
@Override
public InetAddress[] getAllByName(String host) throws UnknownHostException {
- List<InetAddress> addrs = Arrays.asList(super.getAllByName(host));
+ // Always bypass Private DNS.
+ final List<InetAddress> addrs = Arrays.asList(
+ ResolvUtil.blockingResolveAllLocally(this, host));
// Ensure the address family of the first address is tried first.
LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
@@ -1065,44 +1230,6 @@ public class NetworkMonitor extends StateMachine {
return null;
}
- public void notifyPrivateDnsSettingsChanged(DnsManager.PrivateDnsConfig newCfg) {
- // Cancel any outstanding resolutions.
- removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
- // Send the update to the proper thread.
- sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
- }
-
- private void resolvePrivateDnsConfig() {
- resolvePrivateDnsConfig(DnsManager.getPrivateDnsConfig(mContext.getContentResolver()));
- }
-
- private void resolvePrivateDnsConfig(DnsManager.PrivateDnsConfig cfg) {
- // Nothing to do.
- if (cfg == null) {
- mPrivateDnsCfg = null;
- return;
- }
-
- // No DNS resolution required.
- if (!cfg.inStrictMode()) {
- mPrivateDnsCfg = cfg;
- return;
- }
-
- if ((mPrivateDnsCfg != null) && mPrivateDnsCfg.inStrictMode() &&
- (mPrivateDnsCfg.ips.length > 0) && mPrivateDnsCfg.hostname.equals(cfg.hostname)) {
- // We have already resolved this strict mode hostname. Assume that
- // Private DNS services won't be changing serving IP addresses very
- // frequently and save ourselves one re-resolve.
- return;
- }
-
- mPrivateDnsCfg = cfg;
- final DnsManager.PrivateDnsConfig resolvedCfg = DnsManager.tryBlockingResolveOf(
- mNetwork, mPrivateDnsCfg.hostname);
- if (resolvedCfg != null) mPrivateDnsCfg = resolvedCfg;
- }
-
/**
* @param responseReceived - whether or not we received a valid HTTP response to our request.
* If false, isCaptivePortal and responseTimestampMs are ignored
diff --git a/com/android/server/connectivity/Tethering.java b/com/android/server/connectivity/Tethering.java
index d37dd185..df6a6f8b 100644
--- a/com/android/server/connectivity/Tethering.java
+++ b/com/android/server/connectivity/Tethering.java
@@ -249,6 +249,7 @@ public class Tethering extends BaseNetworkObserver {
"CarrierConfigChangeListener", mContext, smHandler, filter,
(Intent ignored) -> {
mLog.log("OBSERVED carrier config change");
+ updateConfiguration();
reevaluateSimCardProvisioning();
});
// TODO: Remove SimChangeListener altogether. For now, we retain it
@@ -261,28 +262,35 @@ public class Tethering extends BaseNetworkObserver {
});
mStateReceiver = new StateReceiver();
- filter = new IntentFilter();
+
+ // Load tethering configuration.
+ updateConfiguration();
+
+ startStateMachineUpdaters();
+ }
+
+ private void startStateMachineUpdaters() {
+ mCarrierConfigChange.startListening();
+
+ final Handler handler = mTetherMasterSM.getHandler();
+ IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_STATE);
filter.addAction(CONNECTIVITY_ACTION);
filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
- mContext.registerReceiver(mStateReceiver, filter, null, smHandler);
+ mContext.registerReceiver(mStateReceiver, filter, null, handler);
filter = new IntentFilter();
filter.addAction(Intent.ACTION_MEDIA_SHARED);
filter.addAction(Intent.ACTION_MEDIA_UNSHARED);
filter.addDataScheme("file");
- mContext.registerReceiver(mStateReceiver, filter, null, smHandler);
+ mContext.registerReceiver(mStateReceiver, filter, null, handler);
- UserManagerInternal userManager = LocalServices.getService(UserManagerInternal.class);
-
- // this check is useful only for some unit tests; example: ConnectivityServiceTest
- if (userManager != null) {
- userManager.addUserRestrictionsListener(new TetheringUserRestrictionListener(this));
+ final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ // This check is useful only for some unit tests; example: ConnectivityServiceTest.
+ if (umi != null) {
+ umi.addUserRestrictionsListener(new TetheringUserRestrictionListener(this));
}
-
- // load device config info
- updateConfiguration();
}
private WifiManager getWifiManager() {
@@ -384,17 +392,15 @@ public class Tethering extends BaseNetworkObserver {
*/
@VisibleForTesting
protected boolean isTetherProvisioningRequired() {
- String[] provisionApp = mContext.getResources().getStringArray(
- com.android.internal.R.array.config_mobile_hotspot_provision_app);
+ final TetheringConfiguration cfg = mConfig;
if (mSystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false)
- || provisionApp == null) {
+ || cfg.provisioningApp.length == 0) {
return false;
}
-
if (carrierConfigAffirmsEntitlementCheckNotRequired()) {
return false;
}
- return (provisionApp.length == 2);
+ return (cfg.provisioningApp.length == 2);
}
// The logic here is aimed solely at confirming that a CarrierConfig exists
@@ -417,20 +423,6 @@ public class Tethering extends BaseNetworkObserver {
return !isEntitlementCheckRequired;
}
- // Used by the SIM card change observation code.
- // TODO: De-duplicate above code.
- private boolean hasMobileHotspotProvisionApp() {
- try {
- if (!mContext.getResources().getString(com.android.internal.R.string.
- config_mobile_hotspot_provision_app_no_ui).isEmpty()) {
- Log.d(TAG, "re-evaluate provisioning");
- return true;
- }
- } catch (Resources.NotFoundException e) {}
- Log.d(TAG, "no prov-check needed for new SIM");
- return false;
- }
-
/**
* Enables or disables tethering for the given type. This should only be called once
* provisioning has succeeded or is not necessary. It will also schedule provisioning rechecks
@@ -1187,7 +1179,7 @@ public class Tethering extends BaseNetworkObserver {
}
private void reevaluateSimCardProvisioning() {
- if (!hasMobileHotspotProvisionApp()) return;
+ if (!mConfig.hasMobileHotspotProvisionApp()) return;
if (carrierConfigAffirmsEntitlementCheckNotRequired()) return;
ArrayList<Integer> tethered = new ArrayList<>();
@@ -1546,7 +1538,6 @@ public class Tethering extends BaseNetworkObserver {
return;
}
- mCarrierConfigChange.startListening();
mSimChange.startListening();
mUpstreamNetworkMonitor.start();
@@ -1564,7 +1555,6 @@ public class Tethering extends BaseNetworkObserver {
mOffload.stop();
mUpstreamNetworkMonitor.stop();
mSimChange.stopListening();
- mCarrierConfigChange.stopListening();
notifyDownstreamsOfNewUpstreamIface(null);
handleNewUpstreamNetworkState(null);
}
diff --git a/com/android/server/connectivity/tethering/TetheringConfiguration.java b/com/android/server/connectivity/tethering/TetheringConfiguration.java
index 09bce7f4..454c579e 100644
--- a/com/android/server/connectivity/tethering/TetheringConfiguration.java
+++ b/com/android/server/connectivity/tethering/TetheringConfiguration.java
@@ -21,14 +21,23 @@ import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static com.android.internal.R.array.config_mobile_hotspot_provision_app;
+import static com.android.internal.R.array.config_tether_bluetooth_regexs;
+import static com.android.internal.R.array.config_tether_dhcp_range;
+import static com.android.internal.R.array.config_tether_usb_regexs;
+import static com.android.internal.R.array.config_tether_upstream_types;
+import static com.android.internal.R.array.config_tether_wifi_regexs;
+import static com.android.internal.R.string.config_mobile_hotspot_provision_app_no_ui;
import android.content.Context;
import android.content.res.Resources;
import android.net.ConnectivityManager;
-import android.telephony.TelephonyManager;
import android.net.util.SharedLog;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -51,6 +60,8 @@ import java.util.StringJoiner;
public class TetheringConfiguration {
private static final String TAG = TetheringConfiguration.class.getSimpleName();
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
@VisibleForTesting
public static final int DUN_NOT_REQUIRED = 0;
public static final int DUN_REQUIRED = 1;
@@ -79,18 +90,18 @@ public class TetheringConfiguration {
public final String[] dhcpRanges;
public final String[] defaultIPv4DNS;
+ public final String[] provisioningApp;
+ public final String provisioningAppNoUi;
+
public TetheringConfiguration(Context ctx, SharedLog log) {
final SharedLog configLog = log.forSubComponent("config");
- tetherableUsbRegexs = ctx.getResources().getStringArray(
- com.android.internal.R.array.config_tether_usb_regexs);
+ tetherableUsbRegexs = getResourceStringArray(ctx, config_tether_usb_regexs);
// TODO: Evaluate deleting this altogether now that Wi-Fi always passes
// us an interface name. Careful consideration needs to be given to
// implications for Settings and for provisioning checks.
- tetherableWifiRegexs = ctx.getResources().getStringArray(
- com.android.internal.R.array.config_tether_wifi_regexs);
- tetherableBluetoothRegexs = ctx.getResources().getStringArray(
- com.android.internal.R.array.config_tether_bluetooth_regexs);
+ tetherableWifiRegexs = getResourceStringArray(ctx, config_tether_wifi_regexs);
+ tetherableBluetoothRegexs = getResourceStringArray(ctx, config_tether_bluetooth_regexs);
dunCheck = checkDunRequired(ctx);
configLog.log("DUN check returned: " + dunCheckString(dunCheck));
@@ -101,6 +112,9 @@ public class TetheringConfiguration {
dhcpRanges = getDhcpRanges(ctx);
defaultIPv4DNS = copy(DEFAULT_IPV4_DNS);
+ provisioningApp = getResourceStringArray(ctx, config_mobile_hotspot_provision_app);
+ provisioningAppNoUi = getProvisioningAppNoUi(ctx);
+
configLog.log(toString());
}
@@ -116,6 +130,10 @@ public class TetheringConfiguration {
return matchesDownstreamRegexs(iface, tetherableBluetoothRegexs);
}
+ public boolean hasMobileHotspotProvisionApp() {
+ return !TextUtils.isEmpty(provisioningAppNoUi);
+ }
+
public void dump(PrintWriter pw) {
dumpStringArray(pw, "tetherableUsbRegexs", tetherableUsbRegexs);
dumpStringArray(pw, "tetherableWifiRegexs", tetherableWifiRegexs);
@@ -129,6 +147,10 @@ public class TetheringConfiguration {
dumpStringArray(pw, "dhcpRanges", dhcpRanges);
dumpStringArray(pw, "defaultIPv4DNS", defaultIPv4DNS);
+
+ dumpStringArray(pw, "provisioningApp", provisioningApp);
+ pw.print("provisioningAppNoUi: ");
+ pw.println(provisioningAppNoUi);
}
public String toString() {
@@ -140,6 +162,8 @@ public class TetheringConfiguration {
sj.add(String.format("isDunRequired:%s", isDunRequired));
sj.add(String.format("preferredUpstreamIfaceTypes:%s",
makeString(preferredUpstreamNames(preferredUpstreamIfaceTypes))));
+ sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
+ sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi));
return String.format("TetheringConfiguration{%s}", sj.toString());
}
@@ -159,6 +183,7 @@ public class TetheringConfiguration {
}
private static String makeString(String[] strings) {
+ if (strings == null) return "null";
final StringJoiner sj = new StringJoiner(",", "[", "]");
for (String s : strings) sj.add(s);
return sj.toString();
@@ -195,8 +220,7 @@ public class TetheringConfiguration {
}
private static Collection<Integer> getUpstreamIfaceTypes(Context ctx, int dunCheck) {
- final int ifaceTypes[] = ctx.getResources().getIntArray(
- com.android.internal.R.array.config_tether_upstream_types);
+ final int ifaceTypes[] = ctx.getResources().getIntArray(config_tether_upstream_types);
final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length);
for (int i : ifaceTypes) {
switch (i) {
@@ -247,14 +271,30 @@ public class TetheringConfiguration {
}
private static String[] getDhcpRanges(Context ctx) {
- final String[] fromResource = ctx.getResources().getStringArray(
- com.android.internal.R.array.config_tether_dhcp_range);
+ final String[] fromResource = getResourceStringArray(ctx, config_tether_dhcp_range);
if ((fromResource.length > 0) && (fromResource.length % 2 == 0)) {
return fromResource;
}
return copy(DHCP_DEFAULT_RANGE);
}
+ private static String getProvisioningAppNoUi(Context ctx) {
+ try {
+ return ctx.getResources().getString(config_mobile_hotspot_provision_app_no_ui);
+ } catch (Resources.NotFoundException e) {
+ return "";
+ }
+ }
+
+ private static String[] getResourceStringArray(Context ctx, int resId) {
+ try {
+ final String[] strArray = ctx.getResources().getStringArray(resId);
+ return (strArray != null) ? strArray : EMPTY_STRING_ARRAY;
+ } catch (Resources.NotFoundException e404) {
+ return EMPTY_STRING_ARRAY;
+ }
+ }
+
private static String[] copy(String[] strarray) {
return Arrays.copyOf(strarray, strarray.length);
}
diff --git a/com/android/server/content/ContentService.java b/com/android/server/content/ContentService.java
index b7bbb3b4..e3d0bddc 100644
--- a/com/android/server/content/ContentService.java
+++ b/com/android/server/content/ContentService.java
@@ -79,7 +79,7 @@ import java.util.List;
*/
public final class ContentService extends IContentService.Stub {
static final String TAG = "ContentService";
- static final boolean DEBUG = true;
+ static final boolean DEBUG = false;
public static class Lifecycle extends SystemService {
private ContentService mService;
diff --git a/com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java b/com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java
new file mode 100644
index 00000000..3d3d0a9b
--- /dev/null
+++ b/com/android/server/devicepolicy/ClockworkDevicePolicyManagerWrapperService.java
@@ -0,0 +1,1397 @@
+package com.android.server.devicepolicy;
+
+import static android.app.admin.DevicePolicyManager.CODE_DEVICE_ADMIN_NOT_SUPPORTED;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.IApplicationThread;
+import android.app.IServiceConnection;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.NetworkEvent;
+import android.app.admin.PasswordMetrics;
+import android.app.admin.SystemUpdateInfo;
+import android.app.admin.SystemUpdatePolicy;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.StringParceledListSlice;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A thin wrapper around {@link DevicePolicyManagerService} for granual enabling and disabling of
+ * management functionality on Wear.
+ */
+public class ClockworkDevicePolicyManagerWrapperService extends BaseIDevicePolicyManager {
+
+ private static final String NOT_SUPPORTED_MESSAGE = "The operation is not supported on Wear.";
+
+ private DevicePolicyManagerService mDpmsDelegate;
+
+ /**
+ * If true, throw {@link UnsupportedOperationException} when unsupported setter methods are
+ * called. Otherwise make the unsupported methods no-op.
+ *
+ * It should be normally set to false. Enable throwing of the exception when needed for debug
+ * purposes.
+ */
+ private final boolean mThrowUnsupportedException;
+
+ public ClockworkDevicePolicyManagerWrapperService(Context context) {
+ this(context, false);
+ }
+
+ public ClockworkDevicePolicyManagerWrapperService(
+ Context context, boolean throwUnsupportedException) {
+ mDpmsDelegate = new DevicePolicyManagerService(new ClockworkInjector(context));
+
+ if (Build.TYPE.equals("userdebug") || Build.TYPE.equals("eng")) {
+ mThrowUnsupportedException = true;
+ } else {
+ mThrowUnsupportedException = throwUnsupportedException;
+ }
+ }
+
+ static class ClockworkInjector extends DevicePolicyManagerService.Injector {
+
+ ClockworkInjector(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean hasFeature() {
+ return getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+ }
+ }
+
+ @Override
+ void systemReady(int phase) {
+ mDpmsDelegate.systemReady(phase);
+ }
+
+ @Override
+ void handleStartUser(int userId) {
+ mDpmsDelegate.handleStartUser(userId);
+ }
+
+ @Override
+ void handleUnlockUser(int userId) {
+ mDpmsDelegate.handleUnlockUser(userId);
+ }
+
+ @Override
+ void handleStopUser(int userId) {
+ mDpmsDelegate.handleStopUser(userId);
+ }
+
+ @Override
+ public void setPasswordQuality(ComponentName who, int quality, boolean parent) {
+ mDpmsDelegate.setPasswordQuality(who, quality, parent);
+ }
+
+ @Override
+ public int getPasswordQuality(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordQuality(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumLength(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumLength(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumLength(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumLength(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumUpperCase(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumUpperCase(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumUpperCase(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumUpperCase(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumLowerCase(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumLowerCase(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumLowerCase(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumLowerCase(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumLetters(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumLetters(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumLetters(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumLetters(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumNumeric(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumNumeric(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumNumeric(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumNumeric(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumSymbols(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumSymbols(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumSymbols(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumSymbols(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordMinimumNonLetter(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordMinimumNonLetter(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordMinimumNonLetter(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordMinimumNonLetter(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordHistoryLength(ComponentName who, int length, boolean parent) {
+ mDpmsDelegate.setPasswordHistoryLength(who, length, parent);
+ }
+
+ @Override
+ public int getPasswordHistoryLength(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordHistoryLength(who, userHandle, parent);
+ }
+
+ @Override
+ public void setPasswordExpirationTimeout(ComponentName who, long timeout, boolean parent) {
+ mDpmsDelegate.setPasswordExpirationTimeout(who, timeout, parent);
+ }
+
+ @Override
+ public long getPasswordExpirationTimeout(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordExpirationTimeout(who, userHandle, parent);
+ }
+
+ @Override
+ public long getPasswordExpiration(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getPasswordExpiration(who, userHandle, parent);
+ }
+
+ @Override
+ public boolean isActivePasswordSufficient(int userHandle, boolean parent) {
+ return mDpmsDelegate.isActivePasswordSufficient(userHandle, parent);
+ }
+
+ @Override
+ public boolean isProfileActivePasswordSufficientForParent(int userHandle) {
+ return false;
+ }
+
+ @Override
+ public int getCurrentFailedPasswordAttempts(int userHandle, boolean parent) {
+ return mDpmsDelegate.getCurrentFailedPasswordAttempts(userHandle, parent);
+ }
+
+ @Override
+ public int getProfileWithMinimumFailedPasswordsForWipe(int userHandle, boolean parent) {
+ return UserHandle.USER_NULL;
+ }
+
+ @Override
+ public void setMaximumFailedPasswordsForWipe(ComponentName who, int num, boolean parent) {
+ mDpmsDelegate.setMaximumFailedPasswordsForWipe(who, num, parent);
+ }
+
+ @Override
+ public int getMaximumFailedPasswordsForWipe(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getMaximumFailedPasswordsForWipe(who, userHandle, parent);
+ }
+
+ @Override
+ public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException {
+ return mDpmsDelegate.resetPassword(passwordOrNull, flags);
+ }
+
+ @Override
+ public void setMaximumTimeToLock(ComponentName who, long timeMs, boolean parent) {
+ mDpmsDelegate.setMaximumTimeToLock(who, timeMs, parent);
+ }
+
+ @Override
+ public long getMaximumTimeToLock(ComponentName who, int userHandle, boolean parent) {
+ return mDpmsDelegate.getMaximumTimeToLock(who, userHandle, parent);
+ }
+
+ @Override
+ public void setRequiredStrongAuthTimeout(ComponentName who, long timeoutMs, boolean parent) {
+ mDpmsDelegate.setRequiredStrongAuthTimeout(who, timeoutMs, parent);
+ }
+
+ @Override
+ public long getRequiredStrongAuthTimeout(ComponentName who, int userId, boolean parent) {
+ return mDpmsDelegate.getRequiredStrongAuthTimeout(who, userId, parent);
+ }
+
+ @Override
+ public void lockNow(int flags, boolean parent) {
+ mDpmsDelegate.lockNow(flags, parent);
+ }
+
+ @Override
+ public ComponentName setGlobalProxy(ComponentName who, String proxySpec, String exclusionList) {
+ maybeThrowUnsupportedOperationException();
+ return null;
+ }
+
+ @Override
+ public ComponentName getGlobalProxyAdmin(int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return null;
+ }
+
+ @Override
+ public void setRecommendedGlobalProxy(ComponentName who, ProxyInfo proxyInfo) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public int setStorageEncryption(ComponentName who, boolean encrypt) {
+ maybeThrowUnsupportedOperationException();
+ return DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
+ }
+
+ @Override
+ public boolean getStorageEncryption(ComponentName who, int userHandle) {
+ return false;
+ }
+
+ @Override
+ public int getStorageEncryptionStatus(String callerPackage, int userHandle) {
+ // Ok to return current status even though setting encryption is not supported in Wear.
+ return mDpmsDelegate.getStorageEncryptionStatus(callerPackage, userHandle);
+ }
+
+ @Override
+ public boolean requestBugreport(ComponentName who) {
+ return mDpmsDelegate.requestBugreport(who);
+ }
+
+ @Override
+ public void setCameraDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getCameraDisabled(ComponentName who, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public void setScreenCaptureDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getScreenCaptureDisabled(ComponentName who, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public void setKeyguardDisabledFeatures(ComponentName who, int which, boolean parent) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public int getKeyguardDisabledFeatures(ComponentName who, int userHandle, boolean parent) {
+ return 0;
+ }
+
+ @Override
+ public void setActiveAdmin(ComponentName adminReceiver, boolean refreshing, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isAdminActive(ComponentName adminReceiver, int userHandle) {
+ return false;
+ }
+
+ @Override
+ public List<ComponentName> getActiveAdmins(int userHandle) {
+ return null;
+ }
+
+ @Override
+ public boolean packageHasActiveAdmins(String packageName, int userHandle) {
+ return false;
+ }
+
+ @Override
+ public void getRemoveWarning(ComponentName comp, RemoteCallback result, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void removeActiveAdmin(ComponentName adminReceiver, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void forceRemoveActiveAdmin(ComponentName adminReceiver, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasGrantedPolicy(ComponentName adminReceiver, int policyId, int userHandle) {
+ return false;
+ }
+
+ @Override
+ public void setActivePasswordState(PasswordMetrics metrics, int userHandle) {
+ mDpmsDelegate.setActivePasswordState(metrics, userHandle);
+ }
+
+ @Override
+ public void reportPasswordChanged(@UserIdInt int userId) {
+ mDpmsDelegate.reportPasswordChanged(userId);
+ }
+
+ @Override
+ public void reportFailedPasswordAttempt(int userHandle) {
+ mDpmsDelegate.reportFailedPasswordAttempt(userHandle);
+ }
+
+ @Override
+ public void reportSuccessfulPasswordAttempt(int userHandle) {
+ mDpmsDelegate.reportSuccessfulPasswordAttempt(userHandle);
+ }
+
+ @Override
+ public void reportFailedFingerprintAttempt(int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void reportSuccessfulFingerprintAttempt(int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void reportKeyguardDismissed(int userHandle) {
+ mDpmsDelegate.reportKeyguardDismissed(userHandle);
+ }
+
+ @Override
+ public void reportKeyguardSecured(int userHandle) {
+ mDpmsDelegate.reportKeyguardSecured(userHandle);
+ }
+
+ @Override
+ public boolean setDeviceOwner(ComponentName admin, String ownerName, int userId) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean hasDeviceOwner() {
+ return false;
+ }
+
+ @Override
+ public ComponentName getDeviceOwnerComponent(boolean callingUserOnly) {
+ return null;
+ }
+
+ @Override
+ public String getDeviceOwnerName() {
+ return null;
+ }
+
+ @Override
+ public void clearDeviceOwner(String packageName) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public int getDeviceOwnerUserId() {
+ return UserHandle.USER_NULL;
+ }
+
+ @Override
+ public boolean setProfileOwner(ComponentName who, String ownerName, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public ComponentName getProfileOwner(int userHandle) {
+ return null;
+ }
+
+ @Override
+ public String getProfileOwnerName(int userHandle) {
+ return null;
+ }
+
+ @Override
+ public void setProfileEnabled(ComponentName who) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setProfileName(ComponentName who, String profileName) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void clearProfileOwner(ComponentName who) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasUserSetupCompleted() {
+ return mDpmsDelegate.hasUserSetupCompleted();
+ }
+
+ @Override
+ public void setDeviceOwnerLockScreenInfo(ComponentName who, CharSequence info) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public CharSequence getDeviceOwnerLockScreenInfo() {
+ return null;
+ }
+
+ @Override
+ public String[] setPackagesSuspended(
+ ComponentName who, String callerPackage, String[] packageNames, boolean suspended) {
+ maybeThrowUnsupportedOperationException();
+ return packageNames;
+ }
+
+ @Override
+ public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) {
+ return false;
+ }
+
+ @Override
+ public boolean installCaCert(ComponentName admin, String callerPackage, byte[] certBuffer)
+ throws RemoteException {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public void uninstallCaCerts(ComponentName admin, String callerPackage, String[] aliases) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void enforceCanManageCaCerts(ComponentName who, String callerPackage) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean approveCaCert(String alias, int userId, boolean appproval) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean isCaCertApproved(String alias, int userId) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean installKeyPair(ComponentName who, String callerPackage, byte[] privKey,
+ byte[] cert, byte[] chain, String alias, boolean requestAccess,
+ boolean isUserSelectable) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean removeKeyPair(ComponentName who, String callerPackage, String alias) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public void choosePrivateKeyAlias(int uid, Uri uri, String alias, IBinder response) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setDelegatedScopes(ComponentName who, String delegatePackage,
+ List<String> scopes) throws SecurityException {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ @NonNull
+ public List<String> getDelegatedScopes(ComponentName who, String delegatePackage)
+ throws SecurityException {
+ return Collections.EMPTY_LIST;
+ }
+
+ @NonNull
+ public List<String> getDelegatePackages(ComponentName who, String scope)
+ throws SecurityException {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public void setCertInstallerPackage(ComponentName who, String installerPackage) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public String getCertInstallerPackage(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage, boolean lockdown) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public String getAlwaysOnVpnPackage(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public void wipeDataWithReason(int flags, String wipeReasonForUser) {
+ mDpmsDelegate.wipeDataWithReason(flags, wipeReasonForUser);
+ }
+
+
+ @Override
+ public void addPersistentPreferredActivity(
+ ComponentName who, IntentFilter filter, ComponentName activity) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void clearPackagePersistentPreferredActivities(ComponentName who, String packageName) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setApplicationRestrictions(ComponentName who, String callerPackage,
+ String packageName, Bundle settings) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle getApplicationRestrictions(ComponentName who, String callerPackage,
+ String packageName) {
+ return null;
+ }
+
+ @Override
+ public boolean setApplicationRestrictionsManagingPackage(
+ ComponentName admin, String packageName) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public String getApplicationRestrictionsManagingPackage(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public boolean isCallerApplicationRestrictionsManagingPackage(String callerPackage) {
+ return false;
+ }
+
+ @Override
+ public void setRestrictionsProvider(ComponentName who, ComponentName permissionProvider) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public ComponentName getRestrictionsProvider(int userHandle) {
+ return null;
+ }
+
+ @Override
+ public void setUserRestriction(ComponentName who, String key, boolean enabledFromThisOwner) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle getUserRestrictions(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public void addCrossProfileIntentFilter(ComponentName who, IntentFilter filter, int flags) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void clearCrossProfileIntentFilters(ComponentName who) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean setPermittedCrossProfileNotificationListeners(
+ ComponentName who, List<String> packageList) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public List<String> getPermittedCrossProfileNotificationListeners(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public boolean isNotificationListenerServicePermitted(String packageName, int userId) {
+ return true;
+ }
+
+ @Override
+ public void setCrossProfileCallerIdDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getCrossProfileCallerIdDisabled(ComponentName who) {
+ return false;
+ }
+
+ @Override
+ public boolean getCrossProfileCallerIdDisabledForUser(int userId) {
+ return false;
+ }
+
+ @Override
+ public void setCrossProfileContactsSearchDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getCrossProfileContactsSearchDisabled(ComponentName who) {
+ return false;
+ }
+
+ @Override
+ public boolean getCrossProfileContactsSearchDisabledForUser(int userId) {
+ return false;
+ }
+
+ @Override
+ public boolean addCrossProfileWidgetProvider(ComponentName admin, String packageName) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean removeCrossProfileWidgetProvider(ComponentName admin, String packageName) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public List<String> getCrossProfileWidgetProviders(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public boolean setPermittedAccessibilityServices(ComponentName who, List packageList) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public List getPermittedAccessibilityServices(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public List getPermittedAccessibilityServicesForUser(int userId) {
+ return null;
+ }
+
+ @Override
+ public boolean isAccessibilityServicePermittedByAdmin(
+ ComponentName who, String packageName, int userHandle) {
+ return true;
+ }
+
+ @Override
+ public boolean setPermittedInputMethods(ComponentName who, List packageList) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public List getPermittedInputMethods(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public List getPermittedInputMethodsForCurrentUser() {
+ return null;
+ }
+
+ @Override
+ public boolean isInputMethodPermittedByAdmin(
+ ComponentName who, String packageName, int userHandle) {
+ return true;
+ }
+
+ @Override
+ public boolean setApplicationHidden(ComponentName who, String callerPackage, String packageName,
+ boolean hidden) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean isApplicationHidden(ComponentName who, String callerPackage,
+ String packageName) {
+ return false;
+ }
+
+ @Override
+ public UserHandle createAndManageUser(ComponentName admin, String name,
+ ComponentName profileOwner, PersistableBundle adminExtras, int flags) {
+ maybeThrowUnsupportedOperationException();
+ return null;
+ }
+
+ @Override
+ public boolean removeUser(ComponentName who, UserHandle userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean switchUser(ComponentName who, UserHandle userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public int startUserInBackground(ComponentName who, UserHandle userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return UserManager.USER_OPERATION_ERROR_UNKNOWN;
+ }
+
+ @Override
+ public int stopUser(ComponentName who, UserHandle userHandle) {
+ maybeThrowUnsupportedOperationException();
+ return UserManager.USER_OPERATION_ERROR_UNKNOWN;
+ }
+
+ @Override
+ public int logoutUser(ComponentName who) {
+ maybeThrowUnsupportedOperationException();
+ return UserManager.USER_OPERATION_ERROR_UNKNOWN;
+ }
+
+ @Override
+ public List<UserHandle> getSecondaryUsers(ComponentName who) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isEphemeralUser(ComponentName who) {
+ return false;
+ }
+
+ @Override
+ public void enableSystemApp(ComponentName who, String callerPackage, String packageName) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public int enableSystemAppWithIntent(ComponentName who, String callerPackage, Intent intent) {
+ maybeThrowUnsupportedOperationException();
+ return 0;
+ }
+
+ @Override
+ public boolean installExistingPackage(ComponentName who, String callerPackage,
+ String packageName) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public void setAccountManagementDisabled(
+ ComponentName who, String accountType, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getAccountTypesWithManagementDisabled() {
+ return null;
+ }
+
+ @Override
+ public String[] getAccountTypesWithManagementDisabledAsUser(int userId) {
+ return null;
+ }
+
+ @Override
+ public void setLockTaskPackages(ComponentName who, String[] packages) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public String[] getLockTaskPackages(ComponentName who) {
+ return new String[0];
+ }
+
+ @Override
+ public boolean isLockTaskPermitted(String pkg) {
+ return false;
+ }
+
+ public void setLockTaskFeatures(ComponentName admin, int flags) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ public int getLockTaskFeatures(ComponentName admin) {
+ return 0;
+ }
+
+ @Override
+ public void setGlobalSetting(ComponentName who, String setting, String value) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean setTime(ComponentName who, long millis) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean setTimeZone(ComponentName who, String timeZone) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public void setSecureSetting(ComponentName who, String setting, String value) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setMasterVolumeMuted(ComponentName who, boolean on) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isMasterVolumeMuted(ComponentName who) {
+ return false;
+ }
+
+ @Override
+ public void notifyLockTaskModeChanged(boolean isEnabled, String pkg, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setUninstallBlocked(ComponentName who, String callerPackage, String packageName,
+ boolean uninstallBlocked) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isUninstallBlocked(ComponentName who, String packageName) {
+ return false;
+ }
+
+ @Override
+ public void startManagedQuickContact(String actualLookupKey, long actualContactId,
+ boolean isContactIdIgnored, long actualDirectoryId, Intent originalIntent) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setBluetoothContactSharingDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getBluetoothContactSharingDisabled(ComponentName who) {
+ return false;
+ }
+
+ @Override
+ public boolean getBluetoothContactSharingDisabledForUser(int userId) {
+ return false;
+ }
+
+ @Override
+ public void setTrustAgentConfiguration(ComponentName admin, ComponentName agent,
+ PersistableBundle args, boolean parent) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public List<PersistableBundle> getTrustAgentConfiguration(ComponentName admin,
+ ComponentName agent, int userHandle, boolean parent) {
+ return new ArrayList<PersistableBundle>();
+ }
+
+ @Override
+ public void setAutoTimeRequired(ComponentName who, boolean required) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getAutoTimeRequired() {
+ return false;
+ }
+
+ @Override
+ public void setForceEphemeralUsers(ComponentName who, boolean forceEphemeralUsers) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getForceEphemeralUsers(ComponentName who) {
+ return false;
+ }
+
+ @Override
+ public boolean isRemovingAdmin(ComponentName adminReceiver, int userHandle) {
+ return false;
+ }
+
+ @Override
+ public void setUserIcon(ComponentName who, Bitmap icon) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public Intent createAdminSupportIntent(String restriction) {
+ return null;
+ }
+
+ @Override
+ public void setSystemUpdatePolicy(ComponentName who, SystemUpdatePolicy policy) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public SystemUpdatePolicy getSystemUpdatePolicy() {
+ return null;
+ }
+
+ @Override
+ public boolean setKeyguardDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean setStatusBarDisabled(ComponentName who, boolean disabled) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean getDoNotAskCredentialsOnBoot() {
+ return false;
+ }
+
+ @Override
+ public void notifyPendingSystemUpdate(@Nullable SystemUpdateInfo info) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public SystemUpdateInfo getPendingSystemUpdate(ComponentName admin) {
+ maybeThrowUnsupportedOperationException();
+ return null;
+ }
+
+ @Override
+ public void setPermissionPolicy(ComponentName admin, String callerPackage, int policy)
+ throws RemoteException {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public int getPermissionPolicy(ComponentName admin) throws RemoteException {
+ return mDpmsDelegate.getPermissionPolicy(admin);
+ }
+
+ @Override
+ public boolean setPermissionGrantState(ComponentName admin, String callerPackage,
+ String packageName, String permission, int grantState) throws RemoteException {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public int getPermissionGrantState(ComponentName admin, String callerPackage,
+ String packageName, String permission) throws RemoteException {
+ return mDpmsDelegate.getPermissionGrantState(admin, callerPackage, packageName, permission);
+ }
+
+ @Override
+ public boolean isProvisioningAllowed(String action, String packageName) {
+ return false;
+ }
+
+ @Override
+ public int checkProvisioningPreCondition(String action, String packageName) {
+ return CODE_DEVICE_ADMIN_NOT_SUPPORTED;
+ }
+
+ @Override
+ public void setKeepUninstalledPackages(
+ ComponentName who, String callerPackage, List<String> packageList) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public List<String> getKeepUninstalledPackages(ComponentName who, String callerPackage) {
+ return null;
+ }
+
+ @Override
+ public boolean isManagedProfile(ComponentName admin) {
+ return mDpmsDelegate.isManagedProfile(admin);
+ }
+
+ @Override
+ public boolean isSystemOnlyUser(ComponentName admin) {
+ return mDpmsDelegate.isSystemOnlyUser(admin);
+ }
+
+ @Override
+ public String getWifiMacAddress(ComponentName admin) {
+ return mDpmsDelegate.getWifiMacAddress(admin);
+ }
+
+ @Override
+ public void reboot(ComponentName admin) {
+ mDpmsDelegate.reboot(admin);
+ }
+
+ @Override
+ public void setShortSupportMessage(ComponentName who, CharSequence message) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public CharSequence getShortSupportMessage(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public void setLongSupportMessage(ComponentName who, CharSequence message) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public CharSequence getLongSupportMessage(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public CharSequence getShortSupportMessageForUser(ComponentName who, int userHandle) {
+ return null;
+ }
+
+ @Override
+ public CharSequence getLongSupportMessageForUser(ComponentName who, int userHandle) {
+ return null;
+ }
+
+ @Override
+ public boolean isSeparateProfileChallengeAllowed(int userHandle) {
+ return false;
+ }
+
+ @Override
+ public void setOrganizationColor(ComponentName who, int color) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setOrganizationColorForUser(int color, int userId) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public int getOrganizationColor(ComponentName who) {
+ return Color.parseColor("#00796B");
+ }
+
+ @Override
+ public int getOrganizationColorForUser(int userHandle) {
+ return Color.parseColor("#00796B");
+ }
+
+ @Override
+ public void setOrganizationName(ComponentName who, CharSequence text) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public CharSequence getOrganizationName(ComponentName who) {
+ return null;
+ }
+
+ @Override
+ public CharSequence getDeviceOwnerOrganizationName() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getOrganizationNameForUser(int userHandle) {
+ return null;
+ }
+
+ @Override
+ public int getUserProvisioningState() {
+ return DevicePolicyManager.STATE_USER_UNMANAGED;
+ }
+
+ @Override
+ public void setUserProvisioningState(int newState, int userHandle) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setAffiliationIds(ComponentName admin, List<String> ids) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public List<String> getAffiliationIds(ComponentName admin) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isAffiliatedUser() {
+ return false;
+ }
+
+ @Override
+ public void setSecurityLoggingEnabled(ComponentName admin, boolean enabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSecurityLoggingEnabled(ComponentName admin) {
+ return false;
+ }
+
+ @Override
+ public ParceledListSlice retrieveSecurityLogs(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public ParceledListSlice retrievePreRebootSecurityLogs(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public boolean isUninstallInQueue(String packageName) {
+ return false;
+ }
+
+ @Override
+ public void uninstallPackageWithActiveAdmins(String packageName) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isDeviceProvisioned() {
+ return mDpmsDelegate.isDeviceProvisioned();
+ }
+
+ @Override
+ public boolean isDeviceProvisioningConfigApplied() {
+ return false;
+ }
+
+ @Override
+ public void setDeviceProvisioningConfigApplied() {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void forceUpdateUserSetupComplete() {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public void setBackupServiceEnabled(ComponentName admin, boolean enabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isBackupServiceEnabled(ComponentName admin) {
+ return false;
+ }
+
+ @Override
+ public boolean bindDeviceAdminServiceAsUser(
+ @NonNull ComponentName admin, @NonNull IApplicationThread caller,
+ @Nullable IBinder activtityToken, @NonNull Intent serviceIntent,
+ @NonNull IServiceConnection connection, int flags, @UserIdInt int targetUserId) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public @NonNull List<UserHandle> getBindDeviceAdminTargetUsers(@NonNull ComponentName admin) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isNetworkLoggingEnabled(ComponentName admin) {
+ return false;
+ }
+
+ @Override
+ public List<NetworkEvent> retrieveNetworkLogs(ComponentName admin, long batchToken) {
+ return null;
+ }
+
+ @Override
+ public long getLastSecurityLogRetrievalTime() {
+ return -1;
+ }
+
+ @Override
+ public long getLastBugReportRequestTime() {
+ return -1;
+ }
+
+ @Override
+ public long getLastNetworkLogRetrievalTime() {
+ return -1;
+ }
+
+ @Override
+ public boolean setResetPasswordToken(ComponentName admin, byte[] token) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean clearResetPasswordToken(ComponentName admin) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean isResetPasswordTokenActive(ComponentName admin) {
+ return false;
+ }
+
+ @Override
+ public boolean resetPasswordWithToken(ComponentName admin, String passwordOrNull, byte[] token,
+ int flags) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public boolean isCurrentInputMethodSetByOwner() {
+ return false;
+ }
+
+ @Override
+ public StringParceledListSlice getOwnerInstalledCaCerts(@NonNull UserHandle user) {
+ return new StringParceledListSlice(new ArrayList<>(new ArraySet<>()));
+ }
+
+ @Override
+ public void clearApplicationUserData(ComponentName admin, String packageName,
+ IPackageDataObserver callback) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public synchronized void setLogoutEnabled(ComponentName admin, boolean enabled) {
+ maybeThrowUnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isLogoutEnabled() {
+ return false;
+ }
+
+ @Override
+ public List<String> getDisallowedSystemApps(ComponentName admin, int userId,
+ String provisioningAction) throws RemoteException {
+ return null;
+ }
+
+ @Override
+ public boolean setMandatoryBackupTransport(
+ ComponentName admin, ComponentName backupTransportComponent) {
+ maybeThrowUnsupportedOperationException();
+ return false;
+ }
+
+ @Override
+ public ComponentName getMandatoryBackupTransport() {
+ return null;
+ }
+
+ private void maybeThrowUnsupportedOperationException() throws UnsupportedOperationException {
+ if (mThrowUnsupportedException) {
+ throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE);
+ }
+ }
+}
diff --git a/com/android/server/devicepolicy/DevicePolicyManagerService.java b/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 90e8a9cd..e07b89f2 100644
--- a/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -108,7 +108,6 @@ import android.app.admin.SecurityLog;
import android.app.admin.SecurityLog.SecurityEvent;
import android.app.admin.SystemUpdateInfo;
import android.app.admin.SystemUpdatePolicy;
-import android.app.backup.BackupManager;
import android.app.backup.IBackupManager;
import android.app.backup.ISelectBackupTransportCallback;
import android.app.trust.TrustManager;
@@ -220,6 +219,8 @@ import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.LocalServices;
+import com.android.server.LockGuard;
+import com.android.internal.util.StatLogger;
import com.android.server.SystemServerInitThreadPool;
import com.android.server.SystemService;
import com.android.server.devicepolicy.DevicePolicyManagerService.ActiveAdmin.TrustAgentInfo;
@@ -462,21 +463,60 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
* Whether or not device admin feature is supported. If it isn't return defaults for all
* public methods.
*/
- boolean mHasFeature;
+ final boolean mHasFeature;
/**
* Whether or not this device is a watch.
*/
- boolean mIsWatch;
+ final boolean mIsWatch;
private final CertificateMonitor mCertificateMonitor;
private final SecurityLogMonitor mSecurityLogMonitor;
+
+ @GuardedBy("getLockObject()")
private NetworkLogger mNetworkLogger;
private final AtomicBoolean mRemoteBugreportServiceIsActive = new AtomicBoolean();
private final AtomicBoolean mRemoteBugreportSharingAccepted = new AtomicBoolean();
- private SetupContentObserver mSetupContentObserver;
+ private final SetupContentObserver mSetupContentObserver;
+
+ private static boolean ENABLE_LOCK_GUARD = Build.IS_ENG
+ || (SystemProperties.getInt("debug.dpm.lock_guard", 0) == 1);
+
+ interface Stats {
+ int LOCK_GUARD_GUARD = 0;
+
+ int COUNT = LOCK_GUARD_GUARD + 1;
+ }
+
+ private final StatLogger mStatLogger = new StatLogger(new String[] {
+ "LockGuard.guard()",
+ });
+
+ private final Object mLockDoNoUseDirectly = LockGuard.installNewLock(
+ LockGuard.INDEX_DPMS, /* doWtf=*/ true);
+
+ final Object getLockObject() {
+ if (ENABLE_LOCK_GUARD) {
+ final long start = mStatLogger.getTime();
+ LockGuard.guard(LockGuard.INDEX_DPMS);
+ mStatLogger.logDurationStat(Stats.LOCK_GUARD_GUARD, start);
+ }
+ return mLockDoNoUseDirectly;
+ }
+
+ /**
+ * Check if the current thread holds the DPMS lock, and if not, do a WTF.
+ *
+ * (Doing this check too much may be costly, so don't call it in a hot path.)
+ */
+ final void ensureLocked() {
+ if (Thread.holdsLock(mLockDoNoUseDirectly)) {
+ return;
+ }
+ Slog.wtfStack(LOG_TAG, "Not holding DPMS lock.");
+ }
@VisibleForTesting
final TransferOwnershipMetadataManager mTransferOwnershipMetadataManager;
@@ -627,8 +667,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ @GuardedBy("getLockObject()")
final SparseArray<DevicePolicyData> mUserData = new SparseArray<>();
- @GuardedBy("DevicePolicyManagerService.this")
+
+ @GuardedBy("getLockObject()")
final SparseArray<PasswordMetrics> mUserPasswordMetrics = new SparseArray<>();
final Handler mHandler;
@@ -649,7 +691,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
*/
if (Intent.ACTION_USER_STARTED.equals(action)
&& userHandle == mOwners.getDeviceOwnerUserId()) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
if (isNetworkLoggingEnabledInternalLocked()) {
setNetworkLoggingActiveInternal(true);
}
@@ -684,14 +726,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (Intent.ACTION_USER_ADDED.equals(action)) {
sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle);
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
// It might take a while for the user to become affiliated. Make security
// and network logging unavailable in the meantime.
maybePauseDeviceWideLoggingLocked();
}
} else if (Intent.ACTION_USER_REMOVED.equals(action)) {
sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_REMOVED, userHandle);
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
// Check whether the user is affiliated, *before* removing its data.
boolean isRemovedUserAffiliated = isUserAffiliatedWithDeviceLocked(userHandle);
removeUserData(userHandle);
@@ -705,7 +747,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STARTED, userHandle);
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
maybeSendAdminEnabledBroadcastLocked(userHandle);
// Reset the policy data
mUserData.remove(userHandle);
@@ -716,7 +758,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
} else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_SWITCHED, userHandle);
} else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
maybeSendAdminEnabledBroadcastLocked(userHandle);
}
} else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
@@ -741,7 +783,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void sendDeviceOwnerUserCommand(String action, int userHandle) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
if (deviceOwner != null) {
Bundle extras = new Bundle();
@@ -1662,7 +1704,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
+ " for user " + userHandle);
}
DevicePolicyData policy = getUserData(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
for (int i = policy.mAdminList.size() - 1; i >= 0; i--) {
ActiveAdmin aa = policy.mAdminList.get(i);
try {
@@ -2091,6 +2133,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
// Skip the rest of the initialization
+ mSetupContentObserver = null;
return;
}
@@ -2132,7 +2175,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
*/
@NonNull
DevicePolicyData getUserData(int userHandle) {
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = mUserData.get(userHandle);
if (policy == null) {
policy = new DevicePolicyData(userHandle);
@@ -2173,7 +2216,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
void removeUserData(int userHandle) {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (userHandle == UserHandle.USER_SYSTEM) {
Slog.w(LOG_TAG, "Tried to remove device policy file for user 0! Ignoring.");
return;
@@ -2199,7 +2242,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
void loadOwners() {
- synchronized (this) {
+ synchronized (getLockObject()) {
mOwners.load();
setDeviceOwnerSystemPropertyLocked();
findOwnerComponentIfNecessaryLocked();
@@ -2223,7 +2266,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
/** Apply default restrictions that haven't been applied to profile owners yet. */
private void maybeSetDefaultProfileOwnerUserRestrictions() {
- synchronized (this) {
+ synchronized (getLockObject()) {
for (final int userId : mOwners.getProfileOwnerKeys()) {
final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userId);
// The following restrictions used to be applied to managed profiles by different
@@ -2513,6 +2556,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
ActiveAdmin getActiveAdminUncheckedLocked(ComponentName who, int userHandle) {
+ ensureLocked();
ActiveAdmin admin = getUserData(userHandle).mAdminMap.get(who);
if (admin != null
&& who.getPackageName().equals(admin.info.getActivityInfo().packageName)
@@ -2523,6 +2567,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
ActiveAdmin getActiveAdminUncheckedLocked(ComponentName who, int userHandle, boolean parent) {
+ ensureLocked();
if (parent) {
enforceManagedProfile(userHandle, "call APIs on the parent profile");
}
@@ -2535,6 +2580,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ActiveAdmin getActiveAdminForCallerLocked(ComponentName who, int reqPolicy)
throws SecurityException {
+ ensureLocked();
final int callingUid = mInjector.binderGetCallingUid();
ActiveAdmin result = getActiveAdminWithPolicyForUidLocked(who, reqPolicy, callingUid);
@@ -2565,6 +2611,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ActiveAdmin getActiveAdminForCallerLocked(ComponentName who, int reqPolicy, boolean parent)
throws SecurityException {
+ ensureLocked();
if (parent) {
enforceManagedProfile(mInjector.userHandleGetCallingUserId(),
"call APIs on the parent profile");
@@ -2577,6 +2624,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
* the admin's uid matches the uid.
*/
private ActiveAdmin getActiveAdminForUidLocked(ComponentName who, int uid) {
+ ensureLocked();
final int userId = UserHandle.getUserId(uid);
final DevicePolicyData policy = getUserData(userId);
ActiveAdmin admin = policy.mAdminMap.get(who);
@@ -2591,6 +2639,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private ActiveAdmin getActiveAdminWithPolicyForUidLocked(ComponentName who, int reqPolicy,
int uid) {
+ ensureLocked();
// Try to find an admin which can use reqPolicy
final int userId = UserHandle.getUserId(uid);
final DevicePolicyData policy = getUserData(userId);
@@ -2620,6 +2669,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@VisibleForTesting
boolean isActiveAdminWithPolicyForUserLocked(ActiveAdmin admin, int reqPolicy,
int userId) {
+ ensureLocked();
final boolean ownsDevice = isDeviceOwner(admin.info.getComponent(), userId);
final boolean ownsProfile = isProfileOwner(admin.info.getComponent(), userId);
@@ -3319,14 +3369,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
updateUserSetupCompleteAndPaired();
List<String> packageList;
- synchronized (this) {
+ synchronized (getLockObject()) {
packageList = getKeepUninstalledPackagesLocked();
}
if (packageList != null) {
mInjector.getPackageManagerInternal().setKeepUninstalledPackages(packageList);
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
if (deviceOwner != null) {
// Push the force-ephemeral-users policy to the user manager.
@@ -3379,7 +3429,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private void ensureDeviceOwnerUserStarted() {
final int userId;
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!mOwners.hasDeviceOwner()) {
return;
}
@@ -3436,7 +3486,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// before reboot
Set<Integer> usersWithProfileOwners;
Set<Integer> usersWithData;
- synchronized(this) {
+ synchronized (getLockObject()) {
usersWithProfileOwners = mOwners.getProfileOwnerKeys();
usersWithData = new ArraySet<>();
for (int i = 0; i < mUserData.size(); i++) {
@@ -3460,7 +3510,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final Bundle adminExtras = new Bundle();
adminExtras.putParcelable(Intent.EXTRA_USER, UserHandle.of(userHandle));
- synchronized (this) {
+ synchronized (getLockObject()) {
final long now = System.currentTimeMillis();
List<ActiveAdmin> admins = getActiveAdminsForLockscreenPoliciesLocked(
@@ -3496,7 +3546,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
final DevicePolicyData policy = getUserData(userHandle.getIdentifier());
boolean changed = false;
@@ -3515,7 +3565,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return Collections.<String> emptySet();
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final DevicePolicyData policy = getUserData(userHandle.getIdentifier());
return policy.mAcceptedCaCertificates;
}
@@ -3542,7 +3592,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
DevicePolicyData policy = getUserData(userHandle);
DeviceAdminInfo info = findAdmin(adminReceiver, userHandle,
/* throwForMissingPermission= */ true);
- synchronized (this) {
+ synchronized (getLockObject()) {
checkActiveAdminPrecondition(adminReceiver, info, policy);
long ident = mInjector.binderClearCallingIdentity();
try {
@@ -3592,7 +3642,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void pushActiveAdminPackages() {
- synchronized (this) {
+ synchronized (getLockObject()) {
final List<UserInfo> users = mUserManager.getUsers();
for (int i = users.size() - 1; i >= 0; --i) {
final int userId = users.get(i).id;
@@ -3603,7 +3653,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void pushAllMeteredRestrictedPackages() {
- synchronized (this) {
+ synchronized (getLockObject()) {
final List<UserInfo> users = mUserManager.getUsers();
for (int i = users.size() - 1; i >= 0; --i) {
final int userId = users.get(i).id;
@@ -3681,7 +3731,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
return getActiveAdminUncheckedLocked(adminReceiver, userHandle) != null;
}
}
@@ -3692,7 +3742,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policyData = getUserData(userHandle);
return policyData.mRemovingAdmins.contains(adminReceiver);
}
@@ -3704,7 +3754,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin administrator = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
if (administrator == null) {
throw new SecurityException("No active admin " + adminReceiver);
@@ -3721,7 +3771,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(userHandle);
final int N = policy.mAdminList.size();
if (N <= 0) {
@@ -3741,7 +3791,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(userHandle);
final int N = policy.mAdminList.size();
for (int i=0; i<N; i++) {
@@ -3762,7 +3812,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceShell("forceRemoveActiveAdmin");
long ident = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!isAdminTestOnlyLocked(adminReceiver, userHandle)) {
throw new SecurityException("Attempt to remove non-test admin "
+ adminReceiver + " " + userHandle);
@@ -3842,7 +3892,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
enforceFullCrossUsersPermission(userHandle);
enforceUserUnlocked(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
if (admin == null) {
return;
@@ -3884,7 +3934,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
validateQualityConstant(quality);
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -3903,7 +3953,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
* be the correct one upon boot.
* This should be called whenever the password or the admin policies have changed.
*/
- @GuardedBy("DevicePolicyManagerService.this")
private void updatePasswordValidityCheckpointLocked(int userHandle, boolean parent) {
final int credentialOwner = getCredentialOwner(userHandle, parent);
DevicePolicyData policy = getUserData(credentialOwner);
@@ -3926,7 +3975,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return PASSWORD_QUALITY_UNSPECIFIED;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
int mode = PASSWORD_QUALITY_UNSPECIFIED;
if (who != null) {
@@ -3998,7 +4047,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4023,7 +4072,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
if (ap.passwordHistoryLength != length) {
@@ -4052,7 +4101,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkArgumentNonnegative(timeout, "Timeout must be >= 0 ms");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD, parent);
// Calling this API automatically bumps the expiration date
@@ -4086,7 +4135,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return 0L;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
long timeout = 0L;
if (who != null) {
@@ -4114,7 +4163,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final int userId = UserHandle.getCallingUserId();
List<String> changedProviders = null;
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(admin,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (activeAdmin.crossProfileWidgetProviders == null) {
@@ -4141,7 +4190,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final int userId = UserHandle.getCallingUserId();
List<String> changedProviders = null;
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(admin,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (activeAdmin.crossProfileWidgetProviders == null
@@ -4165,7 +4214,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public List<String> getCrossProfileWidgetProviders(ComponentName admin) {
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(admin,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (activeAdmin.crossProfileWidgetProviders == null
@@ -4211,7 +4260,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return 0L;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
return getPasswordExpirationLocked(who, userHandle, parent);
}
}
@@ -4223,7 +4272,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4245,7 +4294,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void setPasswordMinimumLowerCase(ComponentName who, int length, boolean parent) {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4270,7 +4319,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4295,7 +4344,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4320,7 +4369,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4345,7 +4394,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
final PasswordMetrics metrics = ap.minimumPasswordMetrics;
@@ -4372,7 +4421,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return 0;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
return admin != null ? getter.apply(admin) : 0;
@@ -4404,7 +4453,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceFullCrossUsersPermission(userHandle);
enforceUserUnlocked(userHandle, parent);
- synchronized (this) {
+ synchronized (getLockObject()) {
// This API can only be called by an active device admin,
// so try to retrieve it to check that the caller is one.
getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
@@ -4435,7 +4484,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceFullCrossUsersPermission(userHandle);
enforceManagedProfile(userHandle, "call APIs refering to the parent profile");
- synchronized (this) {
+ synchronized (getLockObject()) {
final int targetUser = getProfileParentId(userHandle);
enforceUserUnlocked(targetUser, false);
int credentialOwner = getCredentialOwner(userHandle, false);
@@ -4502,7 +4551,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public int getCurrentFailedPasswordAttempts(int userHandle, boolean parent) {
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!isCallerWithSystemUid()) {
// This API can only be called by an active device admin,
// so try to retrieve it to check that the caller is one.
@@ -4523,7 +4572,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// This API can only be called by an active device admin,
// so try to retrieve it to check that the caller is one.
getActiveAdminForCallerLocked(
@@ -4548,7 +4597,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return 0;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = (who != null)
? getActiveAdminUncheckedLocked(who, userHandle, parent)
: getAdminWithMinimumFailedPasswordsForWipeLocked(userHandle, parent);
@@ -4562,7 +4611,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return UserHandle.USER_NULL;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getAdminWithMinimumFailedPasswordsForWipeLocked(
userHandle, parent);
return admin != null ? admin.getUserHandle().getIdentifier() : UserHandle.USER_NULL;
@@ -4623,7 +4672,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
/* PO or DO could do an untrusted reset in certain conditions. */
private boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
// An active DO or PO might be able to fo an untrusted credential reset
for (final ActiveAdmin admin : getUserData(userId).mAdminList) {
if (!isActiveAdminWithPolicyForUserLocked(admin,
@@ -4649,7 +4698,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceNotManagedProfile(userHandle, "clear the active password");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
// If caller has PO (or DO) it can change the password, so see if that's the case first.
ActiveAdmin admin = getActiveAdminWithPolicyForUidLocked(
null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, callingUid);
@@ -4719,7 +4768,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private boolean resetPasswordInternal(String password, long tokenHandle, byte[] token,
int flags, int callingUid, int userHandle) {
int quality;
- synchronized (this) {
+ synchronized (getLockObject()) {
quality = getPasswordQuality(null, userHandle, /* parent */ false);
if (quality == DevicePolicyManager.PASSWORD_QUALITY_MANAGED) {
quality = PASSWORD_QUALITY_UNSPECIFIED;
@@ -4829,7 +4878,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mLockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW,
UserHandle.USER_ALL);
}
- synchronized (this) {
+ synchronized (getLockObject()) {
int newOwner = requireEntry ? callingUid : -1;
if (policy.mPasswordOwner != newOwner) {
policy.mPasswordOwner = newOwner;
@@ -4852,7 +4901,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void setDoNotAskCredentialsOnBoot() {
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
if (!policyData.doNotAskCredentialsOnBoot) {
policyData.doNotAskCredentialsOnBoot = true;
@@ -4865,7 +4914,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean getDoNotAskCredentialsOnBoot() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.QUERY_DO_NOT_ASK_CREDENTIALS_ON_BOOT, null);
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
return policyData.doNotAskCredentialsOnBoot;
}
@@ -4878,7 +4927,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_FORCE_LOCK, parent);
if (ap.maximumTimeToUnlock != timeMs) {
@@ -4952,7 +5001,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return 0;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
return admin != null ? admin.maximumTimeToUnlock : 0;
@@ -4994,7 +5043,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, parent);
if (ap.strongAuthUnlockTimeout != timeoutMs) {
@@ -5015,7 +5064,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return DevicePolicyManager.DEFAULT_STRONG_AUTH_TIMEOUT_MS;
}
enforceFullCrossUsersPermission(userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userId, parent);
return admin != null ? admin.strongAuthUnlockTimeout : 0;
@@ -5053,7 +5102,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final int callingUserId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// This API can only be called by an active device admin,
// so try to retrieve it to check that the caller is one.
final ActiveAdmin admin = getActiveAdminForCallerLocked(
@@ -5122,7 +5171,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void enforceProfileOrDeviceOwner(ComponentName who) {
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
}
}
@@ -5130,7 +5179,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean approveCaCert(String alias, int userId, boolean approval) {
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
Set<String> certs = getUserData(userId).mAcceptedCaCertificates;
boolean changed = (approval ? certs.add(alias) : certs.remove(alias));
if (!changed) {
@@ -5145,7 +5194,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isCaCertApproved(String alias, int userId) {
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
return getUserData(userId).mAcceptedCaCertificates.contains(alias);
}
}
@@ -5157,7 +5206,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
isSecure |= mLockPatternUtils.isSecure(getProfileParentId(userInfo.id));
}
if (!isSecure) {
- synchronized (this) {
+ synchronized (getLockObject()) {
getUserData(userInfo.id).mAcceptedCaCertificates.clear();
saveSettingsLocked(userInfo.id);
}
@@ -5188,7 +5237,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mInjector.binderRestoreCallingIdentity(id);
}
- synchronized (this) {
+ synchronized (getLockObject()) {
getUserData(userHandle.getIdentifier()).mOwnerInstalledCaCerts.add(alias);
saveSettingsLocked(userHandle.getIdentifier());
}
@@ -5210,7 +5259,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mInjector.binderRestoreCallingIdentity(id);
}
- synchronized (this) {
+ synchronized (getLockObject()) {
if (getUserData(userId).mOwnerInstalledCaCerts.removeAll(Arrays.asList(aliases))) {
saveSettingsLocked(userId);
}
@@ -5295,7 +5344,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
} else {
// Caller provided - check it is the device owner.
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
}
@@ -5555,7 +5604,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// Retrieve the user ID of the calling process.
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure calling process is device/profile owner.
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
// Ensure the delegate is installed (skip this for DELEGATION_CERT_INSTALL in pre-N).
@@ -5616,7 +5665,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// Retrieve the user ID of the calling process.
final int callingUid = mInjector.binderGetCallingUid();
final int userId = UserHandle.getUserId(callingUid);
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure calling process is device/profile owner.
if (who != null) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -5659,7 +5708,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// Retrieve the user ID of the calling process.
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure calling process is device/profile owner.
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final DevicePolicyData policy = getUserData(userId);
@@ -5698,7 +5747,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// Retrieve the UID and user ID of the calling process.
final int callingUid = mInjector.binderGetCallingUid();
final int userId = UserHandle.getUserId(callingUid);
- synchronized (this) {
+ synchronized (getLockObject()) {
// Retrieve user policy data.
final DevicePolicyData policy = getUserData(userId);
// Retrieve the list of delegation scopes granted to callerPackage.
@@ -5737,7 +5786,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
String scope) {
// If a ComponentName is given ensure it is a device or profile owner according to policy.
if (who != null) {
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, reqPolicy);
}
// If no ComponentName is given ensure calling process has scope delegation.
@@ -5756,7 +5805,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized(this) {
+ synchronized (getLockObject()) {
// Ensure calling process is device/profile owner.
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final DevicePolicyData policy = getUserData(userId);
@@ -5887,7 +5936,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceFullCrossUsersPermission(mInjector.userHandleGetCallingUserId());
final ActiveAdmin admin;
- synchronized (this) {
+ synchronized (getLockObject()) {
admin = getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_WIPE_DATA);
}
String internalReason = "DevicePolicyManager.wipeDataWithReason() from "
@@ -5970,7 +6019,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.BIND_DEVICE_ADMIN, null);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(comp, userHandle);
if (admin == null) {
result.sendResult(null);
@@ -6009,7 +6058,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
validateQualityConstant(metrics.quality);
- synchronized (this) {
+ synchronized (getLockObject()) {
mUserPasswordMetrics.put(userHandle, metrics);
}
}
@@ -6033,7 +6082,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
long ident = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
policy.mFailedPasswordAttempts = 0;
updatePasswordValidityCheckpointLocked(userId, /* parent */ false);
updatePasswordExpirationsLocked(userId);
@@ -6086,7 +6135,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ActiveAdmin strictestAdmin = null;
final long ident = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(userHandle);
policy.mFailedPasswordAttempts++;
saveSettingsLocked(userHandle);
@@ -6146,7 +6195,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.BIND_DEVICE_ADMIN, null);
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(userHandle);
if (policy.mFailedPasswordAttempts != 0 || policy.mPasswordOwner >= 0) {
long ident = mInjector.binderClearCallingIdentity();
@@ -6221,7 +6270,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return null;
}
- synchronized(this) {
+ synchronized (getLockObject()) {
Preconditions.checkNotNull(who, "ComponentName is null");
// Only check if system user has set global proxy. We don't allow other users to set it.
@@ -6276,7 +6325,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized(this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
// Scan through active admins and find if anyone has already
// set the global proxy.
@@ -6296,7 +6345,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setRecommendedGlobalProxy(ComponentName who, ProxyInfo proxyInfo) {
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
long token = mInjector.binderClearCallingIdentity();
@@ -6365,7 +6414,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Check for permissions
// Only system user can set storage encryption
if (userHandle != UserHandle.USER_SYSTEM) {
@@ -6416,7 +6465,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
// Check for permissions if a particular caller is specified
if (who != null) {
// When checking for a single caller, status is based on caller's request
@@ -6519,7 +6568,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (ap.disableScreenCapture != disabled) {
@@ -6539,7 +6588,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
return (admin != null) ? admin.disableScreenCapture : false;
@@ -6581,7 +6630,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (admin.requireAutoTime != required) {
@@ -6609,7 +6658,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
if (deviceOwner != null && deviceOwner.requireAutoTime) {
// If the device owner enforces auto time, we don't need to check the PO's
@@ -6640,7 +6689,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
"Cannot force ephemeral users on systems without split system user.");
}
boolean removeAllUsers = false;
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (deviceOwner.forceEphemeralUsers != forceEphemeralUsers) {
@@ -6666,7 +6715,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
return deviceOwner.forceEphemeralUsers;
@@ -6674,7 +6723,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void ensureDeviceOwnerAndAllUsersAffiliated(ComponentName who) throws SecurityException {
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (!areAllUsersAffiliatedWithDeviceLocked()) {
throw new SecurityException("Not all users are affiliated.");
@@ -6701,7 +6750,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final long currentTime = System.currentTimeMillis();
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
if (currentTime > policyData.mLastBugReportRequestTime) {
policyData.mLastBugReportRequestTime = currentTime;
@@ -6736,7 +6785,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
void sendDeviceOwnerCommand(String action, Bundle extras) {
int deviceOwnerUserId;
ComponentName deviceOwnerComponent;
- synchronized (this) {
+ synchronized (getLockObject()) {
deviceOwnerUserId = mOwners.getDeviceOwnerUserId();
deviceOwnerComponent = mOwners.getDeviceOwnerComponent();
}
@@ -6765,13 +6814,17 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
}
- private synchronized String getDeviceOwnerRemoteBugreportUri() {
- return mOwners.getDeviceOwnerRemoteBugreportUri();
+ private String getDeviceOwnerRemoteBugreportUri() {
+ synchronized (getLockObject()) {
+ return mOwners.getDeviceOwnerRemoteBugreportUri();
+ }
}
- private synchronized void setDeviceOwnerRemoteBugreportUriAndHash(String bugreportUri,
+ private void setDeviceOwnerRemoteBugreportUriAndHash(String bugreportUri,
String bugreportHash) {
- mOwners.setDeviceOwnerRemoteBugreportUriAndHash(bugreportUri, bugreportHash);
+ synchronized (getLockObject()) {
+ mOwners.setDeviceOwnerRemoteBugreportUriAndHash(bugreportUri, bugreportHash);
+ }
}
private void registerRemoteBugreportReceivers() {
@@ -6833,7 +6886,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mRemoteBugreportSharingAccepted.set(true);
String bugreportUriString = null;
String bugreportHash = null;
- synchronized (this) {
+ synchronized (getLockObject()) {
bugreportUriString = getDeviceOwnerRemoteBugreportUri();
bugreportHash = mOwners.getDeviceOwnerRemoteBugreportHash();
}
@@ -6870,7 +6923,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Uri bugreportUri = Uri.parse(bugreportUriString);
pfd = mContext.getContentResolver().openFileDescriptor(bugreportUri, "r");
- synchronized (this) {
+ synchronized (getLockObject()) {
Intent intent = new Intent(DeviceAdminReceiver.ACTION_BUGREPORT_SHARE);
intent.setComponent(mOwners.getDeviceOwnerComponent());
intent.setDataAndType(bugreportUri, RemoteBugreportUtils.BUGREPORT_MIMETYPE);
@@ -6911,7 +6964,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA);
if (ap.disableCamera != disabled) {
@@ -6937,7 +6990,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
return (admin != null) ? admin.disableCamera : false;
@@ -6978,7 +7031,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
which = which & PROFILE_KEYGUARD_FEATURES;
}
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES, parent);
if (ap.disabledKeyguardFeatures != which) {
@@ -7005,7 +7058,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceFullCrossUsersPermission(userHandle);
final long ident = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle, parent);
return (admin != null) ? admin.disabledKeyguardFeatures : 0;
@@ -7053,7 +7106,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(packageList, "packageList is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO or a keep uninstalled packages delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
DELEGATION_KEEP_UNINSTALLED_PACKAGES);
@@ -7074,7 +7127,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
// TODO In split system user mode, allow apps on user 0 to query the list
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO or a keep uninstalled packages delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
DELEGATION_KEEP_UNINSTALLED_PACKAGES);
@@ -7099,7 +7152,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final boolean hasIncompatibleAccountsOrNonAdb =
hasIncompatibleAccountsOrNonAdbNoLock(userId, admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceCanSetDeviceOwnerLocked(admin, userId, hasIncompatibleAccountsOrNonAdb);
final ActiveAdmin activeAdmin = getActiveAdminUncheckedLocked(admin, userId);
if (activeAdmin == null
@@ -7168,7 +7221,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
public boolean isDeviceOwner(ComponentName who, int userId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
return mOwners.hasDeviceOwner()
&& mOwners.getDeviceOwnerUserId() == userId
&& mOwners.getDeviceOwnerComponent().equals(who);
@@ -7176,7 +7229,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private boolean isDeviceOwnerPackage(String packageName, int userId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
return mOwners.hasDeviceOwner()
&& mOwners.getDeviceOwnerUserId() == userId
&& mOwners.getDeviceOwnerPackageName().equals(packageName);
@@ -7184,7 +7237,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private boolean isProfileOwnerPackage(String packageName, int userId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
return mOwners.hasProfileOwner(userId)
&& mOwners.getProfileOwnerPackage(userId).equals(packageName);
}
@@ -7203,7 +7256,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!callingUserOnly) {
enforceManageUsers();
}
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!mOwners.hasDeviceOwner()) {
return null;
}
@@ -7221,7 +7274,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return UserHandle.USER_NULL;
}
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
return mOwners.hasDeviceOwner() ? mOwners.getDeviceOwnerUserId() : UserHandle.USER_NULL;
}
}
@@ -7236,7 +7289,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!mOwners.hasDeviceOwner()) {
return null;
}
@@ -7250,6 +7303,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
/** Returns the active device owner or {@code null} if there is no device owner. */
@VisibleForTesting
ActiveAdmin getDeviceOwnerAdminLocked() {
+ ensureLocked();
ComponentName component = mOwners.getDeviceOwnerComponent();
if (component == null) {
return null;
@@ -7280,7 +7334,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
} catch (NameNotFoundException e) {
throw new SecurityException(e);
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final ComponentName deviceOwnerComponent = mOwners.getDeviceOwnerComponent();
final int deviceOwnerUserId = mOwners.getDeviceOwnerUserId();
if (!mOwners.hasDeviceOwner()
@@ -7368,7 +7422,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final boolean hasIncompatibleAccountsOrNonAdb =
hasIncompatibleAccountsOrNonAdbNoLock(userHandle, who);
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceCanSetProfileOwnerLocked(who, userHandle, hasIncompatibleAccountsOrNonAdb);
final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
@@ -7414,7 +7468,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final int userId = mInjector.userHandleGetCallingUserId();
enforceNotManagedProfile(userId, "clear profile owner");
enforceUserUnlocked(userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
// Check if this is the profile owner who is calling
final ActiveAdmin admin =
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -7457,7 +7511,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
long token = mInjector.binderClearCallingIdentity();
try {
@@ -7546,7 +7600,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
+ "device or profile owner is set.");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
boolean transitionCheckNeeded = true;
// Calling identity/permission checks.
@@ -7616,7 +7670,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
// Check if this is the profile owner who is calling
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final int userId = UserHandle.getCallingUserId();
@@ -7664,7 +7718,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
return mOwners.getProfileOwnerComponent(userHandle);
}
}
@@ -7881,7 +7935,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void enforceDeviceOwnerOrManageUsers() {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (getActiveAdminWithPolicyForUidLocked(null, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
mInjector.binderGetCallingUid()) != null) {
return;
@@ -7891,7 +7945,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void enforceProfileOwnerOrSystemUser() {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (getActiveAdminWithPolicyForUidLocked(null,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, mInjector.binderGetCallingUid())
!= null) {
@@ -7904,7 +7958,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private void enforceProfileOwnerOrFullCrossUsersPermission(int userId) {
if (userId == mInjector.userHandleGetCallingUserId()) {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (getActiveAdminWithPolicyForUidLocked(null,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, mInjector.binderGetCallingUid())
!= null) {
@@ -8017,7 +8071,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
- synchronized (this) {
+ synchronized (getLockObject()) {
pw.println("Current Device Policy Manager state:");
mOwners.dump(" ", pw);
@@ -8048,6 +8102,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
pw.println();
mConstants.dump(" ", pw);
pw.println();
+ mStatLogger.dump(pw, " ");
+ pw.println();
pw.println(" Encryption Status: " + getEncryptionStatusName(getEncryptionStatus()));
}
}
@@ -8076,7 +8132,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ComponentName activity) {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
long id = mInjector.binderClearCallingIdentity();
@@ -8095,7 +8151,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void clearPackagePersistentPreferredActivities(ComponentName who, String packageName) {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
long id = mInjector.binderClearCallingIdentity();
@@ -8113,7 +8169,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setDefaultSmsApplication(ComponentName admin, String packageName) {
Preconditions.checkNotNull(admin, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
mInjector.binderWithCleanCallingIdentity(() ->
@@ -8167,7 +8223,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(admin, "admin is null");
Preconditions.checkNotNull(agent, "agent is null");
final int userHandle = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(admin,
DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES, parent);
ap.trustAgentInfos.put(agent.flattenToString(), new TrustAgentInfo(args));
@@ -8184,7 +8240,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(agent, "agent null");
enforceFullCrossUsersPermission(userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
final String componentName = agent.flattenToString();
if (admin != null) {
final ActiveAdmin ap = getActiveAdminUncheckedLocked(admin, userHandle, parent);
@@ -8234,7 +8290,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setRestrictionsProvider(ComponentName who, ComponentName permissionProvider) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
int userHandle = UserHandle.getCallingUserId();
@@ -8246,7 +8302,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public ComponentName getRestrictionsProvider(int userHandle) {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!isCallerWithSystemUid()) {
throw new SecurityException("Only the system can query the permission provider");
}
@@ -8259,7 +8315,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void addCrossProfileIntentFilter(ComponentName who, IntentFilter filter, int flags) {
Preconditions.checkNotNull(who, "ComponentName is null");
int callingUserId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
long id = mInjector.binderClearCallingIdentity();
@@ -8290,7 +8346,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void clearCrossProfileIntentFilters(ComponentName who) {
Preconditions.checkNotNull(who, "ComponentName is null");
int callingUserId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
long id = mInjector.binderClearCallingIdentity();
try {
@@ -8397,7 +8453,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
admin.permittedAccessiblityServices = packageList;
@@ -8413,7 +8469,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.permittedAccessiblityServices;
@@ -8425,7 +8481,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return null;
}
- synchronized (this) {
+ enforceManageUsers();
+ synchronized (getLockObject()) {
List<String> result = null;
// If we have multiple profiles we return the intersection of the
// permitted lists. This can happen in cases where we have a device
@@ -8492,7 +8549,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
throw new SecurityException(
"Only the system can query if an accessibility service is disabled by admin");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
if (admin == null) {
return false;
@@ -8570,7 +8627,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
admin.permittedInputMethods = packageList;
@@ -8586,7 +8643,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.permittedInputMethods;
@@ -8606,7 +8663,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
int userId = currentUser.id;
- synchronized (this) {
+ synchronized (getLockObject()) {
List<String> result = null;
// If we have multiple profiles we return the intersection of the
// permitted lists. This can happen in cases where we have a device
@@ -8666,7 +8723,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
throw new SecurityException(
"Only the system can query if an input method is disabled by admin");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
if (admin == null) {
return false;
@@ -8692,7 +8749,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
admin.permittedNotificationListeners = packageList;
@@ -8708,7 +8765,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(
who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.permittedNotificationListeners;
@@ -8726,7 +8783,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
throw new SecurityException(
"Only the system can query if a notification listener service is permitted");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userId);
if (profileOwner == null || profileOwner.permittedNotificationListeners == null) {
return true;
@@ -8782,7 +8839,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// Create user.
UserHandle user = null;
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
final int callingUid = mInjector.binderGetCallingUid();
@@ -8871,7 +8928,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final String ownerName = getProfileOwnerName(Process.myUserHandle().getIdentifier());
setProfileOwner(profileOwner, ownerName, userHandle);
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policyData = getUserData(userHandle);
policyData.mInitBundle = adminExtras;
policyData.mAdminBroadcastPending = true;
@@ -8902,7 +8959,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkNotNull(userHandle, "UserHandle is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -8941,7 +8998,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean switchUser(ComponentName who, UserHandle userHandle) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
long id = mInjector.binderClearCallingIdentity();
@@ -8965,7 +9022,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkNotNull(userHandle, "UserHandle is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -9000,7 +9057,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkNotNull(userHandle, "UserHandle is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -9018,7 +9075,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int callingUserId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (!isUserAffiliatedWithDeviceLocked(callingUserId)) {
throw new SecurityException("Admin " + who +
@@ -9070,7 +9127,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public List<UserHandle> getSecondaryUsers(ComponentName who) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -9094,7 +9151,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isEphemeralUser(ComponentName who) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
}
@@ -9129,7 +9186,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public String[] setPackagesSuspended(ComponentName who, String callerPackage,
String[] packageNames, boolean suspended) {
int callingUserId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a package access delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_PACKAGE_ACCESS);
@@ -9137,7 +9194,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
long id = mInjector.binderClearCallingIdentity();
try {
return mIPackageManager.setPackagesSuspendedAsUser(
- packageNames, suspended, null, null, "android", callingUserId);
+ packageNames, suspended, null, null, null, "android", callingUserId);
} catch (RemoteException re) {
// Shouldn't happen.
Slog.e(LOG_TAG, "Failed talking to the package manager", re);
@@ -9151,7 +9208,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) {
int callingUserId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a package access delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_PACKAGE_ACCESS);
@@ -9177,7 +9234,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin activeAdmin =
getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -9216,7 +9273,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void pushUserRestrictions(int userId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
final boolean isDeviceOwner = mOwners.isDeviceOwnerUserId(userId);
final Bundle userRestrictions;
// Whether device owner enforces camera restriction.
@@ -9263,7 +9320,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return activeAdmin.userRestrictions;
@@ -9274,7 +9331,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean setApplicationHidden(ComponentName who, String callerPackage, String packageName,
boolean hidden) {
int callingUserId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a package access delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_PACKAGE_ACCESS);
@@ -9297,7 +9354,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean isApplicationHidden(ComponentName who, String callerPackage,
String packageName) {
int callingUserId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a package access delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_PACKAGE_ACCESS);
@@ -9318,7 +9375,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void enableSystemApp(ComponentName who, String callerPackage, String packageName) {
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or an enable system app delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_ENABLE_SYSTEM_APP);
@@ -9359,7 +9416,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public int enableSystemAppWithIntent(ComponentName who, String callerPackage, Intent intent) {
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or an enable system app delegate.
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_ENABLE_SYSTEM_APP);
@@ -9421,7 +9478,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean installExistingPackage(ComponentName who, String callerPackage,
String packageName) {
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a PO or an install existing package delegate
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_INSTALL_EXISTING_PACKAGE);
@@ -9458,7 +9515,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin ap = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (disabled) {
@@ -9481,7 +9538,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return null;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(userId);
final int N = policy.mAdminList.size();
ArraySet<String> resultSet = new ArraySet<>();
@@ -9497,7 +9554,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void setUninstallBlocked(ComponentName who, String callerPackage, String packageName,
boolean uninstallBlocked) {
final int userId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a block uninstall delegate
enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_BLOCK_UNINSTALL);
@@ -9521,7 +9578,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// when the package is a system app, or when it is an active device admin.
final int userId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
if (who != null) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
}
@@ -9545,7 +9602,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (admin.disableCallerId != disabled) {
@@ -9561,7 +9618,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.disableCallerId;
@@ -9571,7 +9628,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean getCrossProfileCallerIdDisabledForUser(int userId) {
enforceCrossUsersPermission(userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
return (admin != null) ? admin.disableCallerId : false;
}
@@ -9583,7 +9640,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (admin.disableContactsSearch != disabled) {
@@ -9599,7 +9656,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.disableContactsSearch;
@@ -9609,7 +9666,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean getCrossProfileContactsSearchDisabledForUser(int userId) {
enforceCrossUsersPermission(userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
return (admin != null) ? admin.disableContactsSearch : false;
}
@@ -9624,7 +9681,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final long ident = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
final int managedUserId = getManagedUserId(callingUserId);
if (managedUserId < 0) {
return;
@@ -9682,7 +9739,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (admin.disableBluetoothContactSharing != disabled) {
@@ -9698,7 +9755,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.disableBluetoothContactSharing;
@@ -9710,7 +9767,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// TODO: Should there be a check to make sure this relationship is
// within a profile group?
// enforceSystemProcess("getCrossProfileCallerIdDisabled can only be called by system");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
return (admin != null) ? admin.disableBluetoothContactSharing : false;
}
@@ -9722,7 +9779,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkNotNull(packages, "packages is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceCanCallLockTaskLocked(who);
final int userHandle = mInjector.userHandleGetCallingUserId();
setLockTaskPackagesLocked(userHandle, new ArrayList<>(Arrays.asList(packages)));
@@ -9743,7 +9800,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.binderGetCallingUserHandle().getIdentifier();
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceCanCallLockTaskLocked(who);
final List<String> packages = getUserData(userHandle).mLockTaskPackages;
return packages.toArray(new String[packages.size()]);
@@ -9753,7 +9810,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isLockTaskPermitted(String pkg) {
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
return getUserData(userHandle).mLockTaskPackages.contains(pkg);
}
}
@@ -9772,7 +9829,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
"Cannot use LOCK_TASK_FEATURE_NOTIFICATIONS without LOCK_TASK_FEATURE_HOME");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceCanCallLockTaskLocked(who);
setLockTaskFeaturesLocked(userHandle, flags);
}
@@ -9789,7 +9846,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public int getLockTaskFeatures(ComponentName who) {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceCanCallLockTaskLocked(who);
return getUserData(userHandle).mLockTaskFeatures;
}
@@ -9828,7 +9885,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!isCallerWithSystemUid()) {
throw new SecurityException("notifyLockTaskModeChanged can only be called by system");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final DevicePolicyData policy = getUserData(userHandle);
if (policy.mStatusBarDisabled) {
@@ -9858,7 +9915,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void setGlobalSetting(ComponentName who, String setting, String value) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
// Some settings are no supported any more. However we do not want to throw a
@@ -9897,7 +9954,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
Preconditions.checkStringNotEmpty(setting, "String setting is null or empty");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (!SYSTEM_SETTINGS_WHITELIST.contains(setting)) {
@@ -9942,7 +9999,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
int callingUserId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (isDeviceOwner(who, callingUserId)) {
@@ -10002,7 +10059,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setMasterVolumeMuted(ComponentName who, boolean on) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
setUserRestriction(who, UserManager.DISALLOW_UNMUTE_DEVICE, on);
}
@@ -10011,7 +10068,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isMasterVolumeMuted(ComponentName who) {
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
AudioManager audioManager =
@@ -10022,7 +10079,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setUserIcon(ComponentName who, Bitmap icon) {
- synchronized (this) {
+ synchronized (getLockObject()) {
Preconditions.checkNotNull(who, "ComponentName is null");
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -10040,7 +10097,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean setKeyguardDisabled(ComponentName who, boolean disabled) {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (!isUserAffiliatedWithDeviceLocked(userId)) {
throw new SecurityException("Admin " + who +
@@ -10070,7 +10127,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean setStatusBarDisabled(ComponentName who, boolean disabled) {
int userId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (!isUserAffiliatedWithDeviceLocked(userId)) {
throw new SecurityException("Admin " + who +
@@ -10139,7 +10196,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
DevicePolicyData policy = getUserData(userHandle);
if (!policy.mUserSetupComplete) {
policy.mUserSetupComplete = true;
- synchronized (this) {
+ synchronized (getLockObject()) {
saveSettingsLocked(userHandle);
}
}
@@ -10149,7 +10206,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
DevicePolicyData policy = getUserData(userHandle);
if (!policy.mPaired) {
policy.mPaired = true;
- synchronized (this) {
+ synchronized (getLockObject()) {
saveSettingsLocked(userHandle);
}
}
@@ -10166,7 +10223,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private final Uri mDefaultImeChanged = Settings.Secure.getUriFor(
Settings.Secure.DEFAULT_INPUT_METHOD);
- @GuardedBy("DevicePolicyManagerService.this")
+ @GuardedBy("getLockObject()")
private Set<Integer> mUserIdsWithPendingChangesByOwner = new ArraySet<>();
public SetupContentObserver(Handler handler) {
@@ -10182,7 +10239,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mInjector.registerContentObserver(mDefaultImeChanged, false, this, UserHandle.USER_ALL);
}
- @GuardedBy("DevicePolicyManagerService.this")
+ @GuardedBy("getLockObject()")
private void addPendingChangeByOwnerLocked(int userId) {
mUserIdsWithPendingChangesByOwner.add(userId);
}
@@ -10192,13 +10249,13 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (mUserSetupComplete.equals(uri) || (mIsWatch && mPaired.equals(uri))) {
updateUserSetupCompleteAndPaired();
} else if (mDeviceProvisioned.equals(uri)) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
// Set PROPERTY_DEVICE_OWNER_PRESENT, for the SUW case where setting the property
// is delayed until device is marked as provisioned.
setDeviceOwnerSystemPropertyLocked();
}
} else if (mDefaultImeChanged.equals(uri)) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
if (mUserIdsWithPendingChangesByOwner.contains(userId)) {
// This change notification was triggered by the owner changing the current
// IME. Ignore it.
@@ -10220,7 +10277,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public List<String> getCrossProfileWidgetProviders(int profileId) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
if (mOwners == null) {
return Collections.emptyList();
}
@@ -10244,7 +10301,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void addOnCrossProfileWidgetProvidersChangeListener(
OnCrossProfileWidgetProvidersChangeListener listener) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
if (mWidgetProviderListeners == null) {
mWidgetProviderListeners = new ArrayList<>();
}
@@ -10256,14 +10313,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isActiveAdminWithPolicy(int uid, int reqPolicy) {
- synchronized(DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
return getActiveAdminWithPolicyForUidLocked(null, reqPolicy, uid) != null;
}
}
private void notifyCrossProfileProvidersChanged(int userId, List<String> packages) {
final List<OnCrossProfileWidgetProvidersChangeListener> listeners;
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
listeners = new ArrayList<>(mWidgetProviderListeners);
}
final int listenerCount = listeners.size();
@@ -10352,7 +10409,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void reportSeparateProfileChallengeChanged(@UserIdInt int userId) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
updateMaximumTimeToLockLocked(userId);
}
}
@@ -10364,7 +10421,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public CharSequence getPrintingDisabledReasonForUser(@UserIdInt int userId) {
- synchronized (DevicePolicyManagerService.this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(userId);
if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_PRINTING,
UserHandle.of(userId))) {
@@ -10426,7 +10483,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (DevicePolicyManager.POLICY_DISABLE_CAMERA.equals(restriction) ||
DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE.equals(restriction) ||
DevicePolicyManager.POLICY_MANDATORY_BACKUPS.equals(restriction)) {
- synchronized(this) {
+ synchronized (getLockObject()) {
final DevicePolicyData policy = getUserData(userId);
final int N = policy.mAdminList.size();
for (int i = 0; i < N; i++) {
@@ -10485,7 +10542,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
policy.validateAgainstPreviousFreezePeriod(record.first, record.second,
LocalDate.now());
}
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (policy == null) {
mOwners.clearSystemUpdatePolicy();
@@ -10502,7 +10559,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public SystemUpdatePolicy getSystemUpdatePolicy() {
- synchronized (this) {
+ synchronized (getLockObject()) {
SystemUpdatePolicy policy = mOwners.getSystemUpdatePolicy();
if (policy != null && !policy.isValid()) {
Slog.w(LOG_TAG, "Stored system update policy is invalid, return null instead.");
@@ -10535,7 +10592,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
*/
private void updateSystemUpdateFreezePeriodsRecord(boolean saveIfChanged) {
Slog.d(LOG_TAG, "updateSystemUpdateFreezePeriodsRecord");
- synchronized (this) {
+ synchronized (getLockObject()) {
final SystemUpdatePolicy policy = mOwners.getSystemUpdatePolicy();
if (policy == null) {
return;
@@ -10579,7 +10636,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void clearSystemUpdatePolicyFreezePeriodRecord() {
enforceShell("clearSystemUpdatePolicyFreezePeriodRecord");
- synchronized (this) {
+ synchronized (getLockObject()) {
// Print out current record to help diagnosed CTS failures
Slog.i(LOG_TAG, "Clear freeze period record: "
+ mOwners.getSystemUpdateFreezePeriodRecordAsString());
@@ -10597,7 +10654,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
*/
@VisibleForTesting
boolean isCallerDeviceOwner(int callerUid) {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!mOwners.hasDeviceOwner()) {
return false;
}
@@ -10643,7 +10700,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final long ident = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
// Broadcast to device owner first if there is one.
if (mOwners.hasDeviceOwner()) {
final UserHandle deviceOwnerUser =
@@ -10663,7 +10720,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
// Send broadcasts to corresponding profile owners if any.
for (final int userId : runningUserIds) {
- synchronized (this) {
+ synchronized (getLockObject()) {
final ComponentName profileOwnerPackage =
mOwners.getProfileOwnerComponent(userId);
if (profileOwnerPackage != null) {
@@ -10689,7 +10746,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void setPermissionPolicy(ComponentName admin, String callerPackage, int policy)
throws RemoteException {
int userId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a permission grant state delegate.
enforceCanManageScope(admin, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_PERMISSION_GRANT);
@@ -10704,7 +10761,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public int getPermissionPolicy(ComponentName admin) throws RemoteException {
int userId = UserHandle.getCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData userPolicy = getUserData(userId);
return userPolicy.mPermissionPolicy;
}
@@ -10714,7 +10771,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean setPermissionGrantState(ComponentName admin, String callerPackage,
String packageName, String permission, int grantState) throws RemoteException {
UserHandle user = mInjector.binderGetCallingUserHandle();
- synchronized (this) {
+ synchronized (getLockObject()) {
// Ensure the caller is a DO/PO or a permission grant state delegate.
enforceCanManageScope(admin, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
DELEGATION_PERMISSION_GRANT);
@@ -10772,7 +10829,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceCanManageScope(admin, callerPackage,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, DELEGATION_PERMISSION_GRANT);
}
- synchronized (this) {
+ synchronized (getLockObject()) {
long ident = mInjector.binderClearCallingIdentity();
try {
int granted = mIPackageManager.checkPermission(permission,
@@ -10910,7 +10967,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private int checkDeviceOwnerProvisioningPreCondition(int deviceOwnerUserId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
// hasIncompatibleAccountsOrNonAdb doesn't matter since the caller is not adb.
return checkDeviceOwnerProvisioningPreConditionLocked(/* owner unknown */ null,
deviceOwnerUserId, /* isAdb= */ false,
@@ -10977,7 +11034,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
* Return device owner or profile owner set on a given user.
*/
private @Nullable ComponentName getOwnerComponent(int userId) {
- synchronized (this) {
+ synchronized (getLockObject()) {
if (mOwners.getDeviceOwnerUserId() == userId) {
return mOwners.getDeviceOwnerComponent();
}
@@ -11028,7 +11085,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public String getWifiMacAddress(ComponentName admin) {
// Make sure caller has DO.
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -11068,7 +11125,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isSystemOnlyUser(ComponentName admin) {
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -11079,7 +11136,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public void reboot(ComponentName admin) {
Preconditions.checkNotNull(admin);
// Make sure caller has DO.
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
long ident = mInjector.binderClearCallingIdentity();
@@ -11101,7 +11158,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForUidLocked(who,
mInjector.binderGetCallingUid());
if (!TextUtils.equals(admin.shortSupportMessage, message)) {
@@ -11117,7 +11174,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForUidLocked(who,
mInjector.binderGetCallingUid());
return admin.shortSupportMessage;
@@ -11131,7 +11188,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForUidLocked(who,
mInjector.binderGetCallingUid());
if (!TextUtils.equals(admin.longSupportMessage, message)) {
@@ -11147,7 +11204,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
Preconditions.checkNotNull(who, "ComponentName is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForUidLocked(who,
mInjector.binderGetCallingUid());
return admin.longSupportMessage;
@@ -11163,7 +11220,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!isCallerWithSystemUid()) {
throw new SecurityException("Only the system can query support message for user");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
if (admin != null) {
return admin.shortSupportMessage;
@@ -11181,7 +11238,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!isCallerWithSystemUid()) {
throw new SecurityException("Only the system can query support message for user");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
if (admin != null) {
return admin.longSupportMessage;
@@ -11198,7 +11255,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
enforceManagedProfile(userHandle, "set organization color");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
admin.organizationColor = color;
@@ -11214,7 +11271,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceFullCrossUsersPermission(userId);
enforceManageUsers();
enforceManagedProfile(userId, "set organization color");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getProfileOwnerAdminLocked(userId);
admin.organizationColor = color;
saveSettingsLocked(userId);
@@ -11228,7 +11285,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
enforceManagedProfile(mInjector.userHandleGetCallingUserId(), "get organization color");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.organizationColor;
@@ -11242,7 +11299,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
enforceFullCrossUsersPermission(userHandle);
enforceManagedProfile(userHandle, "get organization color");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userHandle);
return (profileOwner != null)
? profileOwner.organizationColor
@@ -11258,7 +11315,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
if (!TextUtils.equals(admin.organizationName, text)) {
@@ -11276,7 +11333,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null");
enforceManagedProfile(mInjector.userHandleGetCallingUserId(), "get organization name");
- synchronized(this) {
+ synchronized (getLockObject()) {
ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.organizationName;
@@ -11289,7 +11346,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return null;
}
enforceDeviceOwnerOrManageUsers();
- synchronized(this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwnerAdmin = getDeviceOwnerAdminLocked();
return deviceOwnerAdmin == null ? null : deviceOwnerAdmin.organizationName;
}
@@ -11302,7 +11359,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
enforceFullCrossUsersPermission(userHandle);
enforceManagedProfile(userHandle, "get organization name");
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin profileOwner = getProfileOwnerAdminLocked(userHandle);
return (profileOwner != null)
? profileOwner.organizationName
@@ -11318,7 +11375,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return packageNames;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -11367,7 +11424,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return new ArrayList<>();
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return admin.meteredDisabledPackages == null
@@ -11387,7 +11444,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
throw new SecurityException(
"Only the system can query restricted pkgs for a specific user");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userId);
if (admin != null && admin.meteredDisabledPackages != null) {
return admin.meteredDisabledPackages.contains(packageName);
@@ -11429,7 +11486,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final Set<String> affiliationIds = new ArraySet<>(ids);
final int callingUserId = mInjector.userHandleGetCallingUserId();
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
getUserData(callingUserId).mAffiliationIds = affiliationIds;
saveSettingsLocked(callingUserId);
@@ -11456,7 +11513,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
return new ArrayList<String>(
getUserData(mInjector.userHandleGetCallingUserId()).mAffiliationIds);
@@ -11469,7 +11526,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
return isUserAffiliatedWithDeviceLocked(mInjector.userHandleGetCallingUserId());
}
}
@@ -11530,7 +11587,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (enabled == mInjector.securityLogGetLoggingEnabledProperty()) {
return;
@@ -11551,7 +11608,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!isCallerWithSystemUid()) {
Preconditions.checkNotNull(admin);
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -11560,12 +11617,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
- private synchronized void recordSecurityLogRetrievalTime() {
- final long currentTime = System.currentTimeMillis();
- DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
- if (currentTime > policyData.mLastSecurityLogRetrievalTime) {
- policyData.mLastSecurityLogRetrievalTime = currentTime;
- saveSettingsLocked(UserHandle.USER_SYSTEM);
+ private void recordSecurityLogRetrievalTime() {
+ synchronized (getLockObject()) {
+ final long currentTime = System.currentTimeMillis();
+ DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
+ if (currentTime > policyData.mLastSecurityLogRetrievalTime) {
+ policyData.mLastSecurityLogRetrievalTime = currentTime;
+ saveSettingsLocked(UserHandle.USER_SYSTEM);
+ }
}
}
@@ -11646,7 +11705,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceCanManageDeviceAdmin();
final int userId = mInjector.userHandleGetCallingUserId();
Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
return mPackagesToRemove.contains(packageUserPair);
}
}
@@ -11672,7 +11731,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
mPackagesToRemove.add(packageUserPair);
}
@@ -11707,7 +11766,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isDeviceProvisioned() {
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
return getUserDataUnchecked(UserHandle.USER_SYSTEM).mUserSetupComplete;
}
}
@@ -11734,7 +11793,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private void startUninstallIntent(final String packageName, final int userId) {
final Pair<String, Integer> packageUserPair = new Pair<>(packageName, userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
if (!mPackagesToRemove.contains(packageUserPair)) {
// Do nothing if uninstall was not requested or was already started.
return;
@@ -11769,7 +11828,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
* @param userHandle The user for which this admin has to be removed.
*/
private void removeAdminArtifacts(final ComponentName adminReceiver, final int userHandle) {
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
if (admin == null) {
return;
@@ -11799,7 +11858,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public void setDeviceProvisioningConfigApplied() {
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
policy.mDeviceProvisioningConfigApplied = true;
saveSettingsLocked(UserHandle.USER_SYSTEM);
@@ -11809,7 +11868,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isDeviceProvisioningConfigApplied() {
enforceManageUsers();
- synchronized (this) {
+ synchronized (getLockObject()) {
final DevicePolicyData policy = getUserData(UserHandle.USER_SYSTEM);
return policy.mDeviceProvisioningConfigApplied;
}
@@ -11835,7 +11894,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Settings.Secure.USER_SETUP_COMPLETE, 0, userId) != 0;
DevicePolicyData policy = getUserData(userId);
policy.mUserSetupComplete = isUserCompleted;
- synchronized (this) {
+ synchronized (getLockObject()) {
saveSettingsLocked(userId);
}
}
@@ -11850,7 +11909,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(
admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
if (!enabled) {
@@ -11879,7 +11938,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return true;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
try {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
IBackupManager ibm = mInjector.getIBackupManager();
@@ -11898,7 +11957,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -11944,18 +12003,20 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return success.get();
}
- synchronized private void saveMandatoryBackupTransport(
+ private void saveMandatoryBackupTransport(
ComponentName admin, int callingUid, ComponentName backupTransportComponent) {
- ActiveAdmin activeAdmin =
- getActiveAdminWithPolicyForUidLocked(
- admin,
- DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
- callingUid);
- if (!Objects.equals(backupTransportComponent,
- activeAdmin.mandatoryBackupTransport)) {
- activeAdmin.mandatoryBackupTransport =
- backupTransportComponent;
- saveSettingsLocked(UserHandle.USER_SYSTEM);
+ synchronized (getLockObject()) {
+ ActiveAdmin activeAdmin =
+ getActiveAdminWithPolicyForUidLocked(
+ admin,
+ DeviceAdminInfo.USES_POLICY_DEVICE_OWNER,
+ callingUid);
+ if (!Objects.equals(backupTransportComponent,
+ activeAdmin.mandatoryBackupTransport)) {
+ activeAdmin.mandatoryBackupTransport =
+ backupTransportComponent;
+ saveSettingsLocked(UserHandle.USER_SYSTEM);
+ }
}
}
@@ -11964,7 +12025,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return null;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin activeAdmin = getDeviceOwnerAdminLocked();
return activeAdmin == null ? null : activeAdmin.mandatoryBackupTransport;
}
@@ -11995,7 +12056,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
final String targetPackage;
- synchronized (this) {
+ synchronized (getLockObject()) {
targetPackage = getOwnerPackageNameForUserLocked(targetUserId);
}
@@ -12032,7 +12093,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final int callingUserId = mInjector.userHandleGetCallingUserId();
@@ -12106,7 +12167,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (accounts.length == 0) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
if (owner == null || !isAdminTestOnlyLocked(owner, userId)) {
Log.w(LOG_TAG,
"Non test-only owner can't be installed with existing accounts.");
@@ -12158,50 +12219,54 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
@Override
- public synchronized void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
+ public void setNetworkLoggingEnabled(ComponentName admin, boolean enabled) {
if (!mHasFeature) {
return;
}
- Preconditions.checkNotNull(admin);
- getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ synchronized (getLockObject()) {
+ Preconditions.checkNotNull(admin);
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
- if (enabled == isNetworkLoggingEnabledInternalLocked()) {
- // already in the requested state
- return;
- }
- ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
- deviceOwner.isNetworkLoggingEnabled = enabled;
- if (!enabled) {
- deviceOwner.numNetworkLoggingNotifications = 0;
- deviceOwner.lastNetworkLoggingNotificationTimeMs = 0;
- }
- saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+ if (enabled == isNetworkLoggingEnabledInternalLocked()) {
+ // already in the requested state
+ return;
+ }
+ ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+ deviceOwner.isNetworkLoggingEnabled = enabled;
+ if (!enabled) {
+ deviceOwner.numNetworkLoggingNotifications = 0;
+ deviceOwner.lastNetworkLoggingNotificationTimeMs = 0;
+ }
+ saveSettingsLocked(mInjector.userHandleGetCallingUserId());
- setNetworkLoggingActiveInternal(enabled);
+ setNetworkLoggingActiveInternal(enabled);
+ }
}
- private synchronized void setNetworkLoggingActiveInternal(boolean active) {
- final long callingIdentity = mInjector.binderClearCallingIdentity();
- try {
- if (active) {
- mNetworkLogger = new NetworkLogger(this, mInjector.getPackageManagerInternal());
- if (!mNetworkLogger.startNetworkLogging()) {
+ private void setNetworkLoggingActiveInternal(boolean active) {
+ synchronized (getLockObject()) {
+ final long callingIdentity = mInjector.binderClearCallingIdentity();
+ try {
+ if (active) {
+ mNetworkLogger = new NetworkLogger(this, mInjector.getPackageManagerInternal());
+ if (!mNetworkLogger.startNetworkLogging()) {
+ mNetworkLogger = null;
+ Slog.wtf(LOG_TAG, "Network logging could not be started due to the logging"
+ + " service not being available yet.");
+ }
+ maybePauseDeviceWideLoggingLocked();
+ sendNetworkLoggingNotificationLocked();
+ } else {
+ if (mNetworkLogger != null && !mNetworkLogger.stopNetworkLogging()) {
+ Slog.wtf(LOG_TAG, "Network logging could not be stopped due to the logging"
+ + " service not being available yet.");
+ }
mNetworkLogger = null;
- Slog.wtf(LOG_TAG, "Network logging could not be started due to the logging"
- + " service not being available yet.");
+ mInjector.getNotificationManager().cancel(SystemMessage.NOTE_NETWORK_LOGGING);
}
- maybePauseDeviceWideLoggingLocked();
- sendNetworkLoggingNotificationLocked();
- } else {
- if (mNetworkLogger != null && !mNetworkLogger.stopNetworkLogging()) {
- Slog.wtf(LOG_TAG, "Network logging could not be stopped due to the logging"
- + " service not being available yet.");
- }
- mNetworkLogger = null;
- mInjector.getNotificationManager().cancel(SystemMessage.NOTE_NETWORK_LOGGING);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(callingIdentity);
}
- } finally {
- mInjector.binderRestoreCallingIdentity(callingIdentity);
}
}
@@ -12248,7 +12313,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
enforceDeviceOwnerOrManageUsers();
return isNetworkLoggingEnabledInternalLocked();
}
@@ -12274,7 +12339,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(admin);
ensureDeviceOwnerAndAllUsersAffiliated(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
if (mNetworkLogger == null
|| !isNetworkLoggingEnabledInternalLocked()) {
return null;
@@ -12399,7 +12464,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (token == null || token.length < 32) {
throw new IllegalArgumentException("token must be at least 32-byte long");
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final int userHandle = mInjector.userHandleGetCallingUserId();
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -12424,7 +12489,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
final int userHandle = mInjector.userHandleGetCallingUserId();
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -12447,7 +12512,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean isResetPasswordTokenActive(ComponentName admin) {
- synchronized (this) {
+ synchronized (getLockObject()) {
final int userHandle = mInjector.userHandleGetCallingUserId();
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -12469,7 +12534,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public boolean resetPasswordWithToken(ComponentName admin, String passwordOrNull, byte[] token,
int flags) {
Preconditions.checkNotNull(token);
- synchronized (this) {
+ synchronized (getLockObject()) {
final int userHandle = mInjector.userHandleGetCallingUserId();
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -12495,7 +12560,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
public StringParceledListSlice getOwnerInstalledCaCerts(@NonNull UserHandle user) {
final int userId = user.getIdentifier();
enforceProfileOwnerOrFullCrossUsersPermission(userId);
- synchronized (this) {
+ synchronized (getLockObject()) {
return new StringParceledListSlice(
new ArrayList<>(getUserData(userId).mOwnerInstalledCaCerts));
}
@@ -12507,7 +12572,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(admin, "ComponentName is null");
Preconditions.checkNotNull(packageName, "packageName is null");
Preconditions.checkNotNull(callback, "callback is null");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
}
final int userId = UserHandle.getCallingUserId();
@@ -12535,13 +12600,13 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
@Override
- public synchronized void setLogoutEnabled(ComponentName admin, boolean enabled) {
+ public void setLogoutEnabled(ComponentName admin, boolean enabled) {
if (!mHasFeature) {
return;
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -12559,7 +12624,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (!mHasFeature) {
return false;
}
- synchronized (this) {
+ synchronized (getLockObject()) {
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
return (deviceOwner != null) && deviceOwner.isLogoutEnabled;
}
@@ -12607,7 +12672,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final long id = mInjector.binderClearCallingIdentity();
try {
- synchronized (this) {
+ synchronized (getLockObject()) {
/*
* We must ensure the whole process is atomic to prevent the device from ending up
* in an invalid state (e.g. no active admin). This could happen if the device
@@ -12713,7 +12778,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final String startUserSessionMessageString =
startUserSessionMessage != null ? startUserSessionMessage.toString() : null;
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -12738,7 +12803,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final String endUserSessionMessageString =
endUserSessionMessage != null ? endUserSessionMessage.toString() : null;
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -12760,7 +12825,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
return deviceOwner.startUserSessionMessage;
@@ -12774,7 +12839,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(admin);
- synchronized (this) {
+ synchronized (getLockObject()) {
final ActiveAdmin deviceOwner =
getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
return deviceOwner.endUserSessionMessage;
@@ -12788,7 +12853,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
@Nullable
public PersistableBundle getTransferOwnershipBundle() {
- synchronized (this) {
+ synchronized (getLockObject()) {
final int callingUserId = mInjector.userHandleGetCallingUserId();
getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
final File bundleFile = new File(
@@ -12817,7 +12882,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null in addOverrideApn");
Preconditions.checkNotNull(apnSetting, "ApnSetting is null in addOverrideApn");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -12848,7 +12913,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(who, "ComponentName is null in updateOverrideApn");
Preconditions.checkNotNull(apnSetting, "ApnSetting is null in updateOverrideApn");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -12871,7 +12936,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null in removeOverrideApn");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -12899,7 +12964,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return Collections.emptyList();
}
Preconditions.checkNotNull(who, "ComponentName is null in getOverrideApns");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -12937,7 +13002,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(who, "ComponentName is null in setOverrideApnEnabled");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -12962,7 +13027,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
Preconditions.checkNotNull(who, "ComponentName is null in isOverrideApnEnabled");
- synchronized (this) {
+ synchronized (getLockObject()) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
diff --git a/com/android/server/display/AutomaticBrightnessController.java b/com/android/server/display/AutomaticBrightnessController.java
index 5e1afeb5..f403953f 100644
--- a/com/android/server/display/AutomaticBrightnessController.java
+++ b/com/android/server/display/AutomaticBrightnessController.java
@@ -169,16 +169,6 @@ class AutomaticBrightnessController {
// Use -1 if there is no current auto-brightness value available.
private int mScreenAutoBrightness = -1;
- // The screen auto-brightness adjustment factor in the range -1 (dimmer) to 1 (brighter)
- private float mScreenAutoBrightnessAdjustment = 0.0f;
-
- // The maximum range of gamma adjustment possible using the screen
- // auto-brightness adjustment setting.
- private float mScreenAutoBrightnessAdjustmentMaxGamma;
-
- // The last screen auto-brightness gamma. (For printing in dump() only.)
- private float mLastScreenAutoBrightnessGamma = 1.0f;
-
// The current display policy. This is useful, for example, for knowing when we're dozing,
// where the light sensor may not be available.
private int mDisplayPolicy = DisplayPowerRequest.POLICY_OFF;
@@ -186,10 +176,8 @@ class AutomaticBrightnessController {
// True if we are collecting a brightness adjustment sample, along with some data
// for the initial state of the sample.
private boolean mBrightnessAdjustmentSamplePending;
- private float mBrightnessAdjustmentSampleOldAdjustment;
private float mBrightnessAdjustmentSampleOldLux;
private int mBrightnessAdjustmentSampleOldBrightness;
- private float mBrightnessAdjustmentSampleOldGamma;
// When the short term model is invalidated, we don't necessarily reset it (i.e. clear the
// user's adjustment) immediately, but wait for a drastic enough change in the ambient light.
@@ -200,12 +188,11 @@ class AutomaticBrightnessController {
private float SHORT_TERM_MODEL_THRESHOLD_RATIO = 0.6f;
public AutomaticBrightnessController(Callbacks callbacks, Looper looper,
- SensorManager sensorManager, BrightnessMappingStrategy mapper, int lightSensorWarmUpTime,
- int brightnessMin, int brightnessMax, float dozeScaleFactor,
+ SensorManager sensorManager, BrightnessMappingStrategy mapper,
+ int lightSensorWarmUpTime, int brightnessMin, int brightnessMax, float dozeScaleFactor,
int lightSensorRate, int initialLightSensorRate, long brighteningLightDebounceConfig,
long darkeningLightDebounceConfig, boolean resetAmbientLuxAfterWarmUpConfig,
- int ambientLightHorizon, float autoBrightnessAdjustmentMaxGamma,
- HysteresisLevels dynamicHysteresis) {
+ int ambientLightHorizon, HysteresisLevels dynamicHysteresis) {
mCallbacks = callbacks;
mSensorManager = sensorManager;
mBrightnessMapper = mapper;
@@ -221,7 +208,6 @@ class AutomaticBrightnessController {
mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
mAmbientLightHorizon = ambientLightHorizon;
mWeightingIntercept = ambientLightHorizon;
- mScreenAutoBrightnessAdjustmentMaxGamma = autoBrightnessAdjustmentMaxGamma;
mDynamicHysteresis = dynamicHysteresis;
mShortTermModelValid = true;
mShortTermModelAnchor = -1;
@@ -236,6 +222,9 @@ class AutomaticBrightnessController {
}
public int getAutomaticScreenBrightness() {
+ if (!mAmbientLuxValid) {
+ return -1;
+ }
if (mDisplayPolicy == DisplayPowerRequest.POLICY_DOZE) {
return (int) (mScreenAutoBrightness * mDozeScaleFactor);
}
@@ -243,7 +232,7 @@ class AutomaticBrightnessController {
}
public float getAutomaticScreenBrightnessAdjustment() {
- return mScreenAutoBrightnessAdjustment;
+ return mBrightnessMapper.getAutoBrightnessAdjustment();
}
public void configure(boolean enable, @Nullable BrightnessConfiguration configuration,
@@ -257,7 +246,9 @@ class AutomaticBrightnessController {
boolean dozing = (displayPolicy == DisplayPowerRequest.POLICY_DOZE);
boolean changed = setBrightnessConfiguration(configuration);
changed |= setDisplayPolicy(displayPolicy);
- changed |= setScreenAutoBrightnessAdjustment(adjustment);
+ if (userChangedAutoBrightnessAdjustment) {
+ changed |= setAutoBrightnessAdjustment(adjustment);
+ }
if (userChangedBrightness && enable) {
// Update the brightness curve with the new user control point. It's critical this
// happens after we update the autobrightness adjustment since it may reset it.
@@ -322,9 +313,6 @@ class AutomaticBrightnessController {
if (DEBUG) {
Slog.d(TAG, "ShortTermModel: anchor=" + mShortTermModelAnchor);
}
- // Reset the brightness adjustment so that the next time we're queried for brightness we
- // return the value the user set.
- mScreenAutoBrightnessAdjustment = 0.0f;
return true;
}
@@ -361,6 +349,7 @@ class AutomaticBrightnessController {
pw.println(" mLightSensorEnabled=" + mLightSensorEnabled);
pw.println(" mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
pw.println(" mAmbientLux=" + mAmbientLux);
+ pw.println(" mAmbientLuxValid=" + mAmbientLuxValid);
pw.println(" mAmbientLightHorizon=" + mAmbientLightHorizon);
pw.println(" mBrighteningLuxThreshold=" + mBrighteningLuxThreshold);
pw.println(" mDarkeningLuxThreshold=" + mDarkeningLuxThreshold);
@@ -369,10 +358,6 @@ class AutomaticBrightnessController {
pw.println(" mRecentLightSamples=" + mRecentLightSamples);
pw.println(" mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
pw.println(" mScreenAutoBrightness=" + mScreenAutoBrightness);
- pw.println(" mScreenAutoBrightnessAdjustment=" + mScreenAutoBrightnessAdjustment);
- pw.println(" mScreenAutoBrightnessAdjustmentMaxGamma="
- + mScreenAutoBrightnessAdjustmentMaxGamma);
- pw.println(" mLastScreenAutoBrightnessGamma=" + mLastScreenAutoBrightnessGamma);
pw.println(" mDisplayPolicy=" + mDisplayPolicy);
pw.println(" mShortTermModelAnchor=" + mShortTermModelAnchor);
@@ -429,8 +414,8 @@ class AutomaticBrightnessController {
if (lightSensorRate != mCurrentLightSensorRate) {
if (DEBUG) {
Slog.d(TAG, "adjustLightSensorRate: " +
- "previousRate=" + mCurrentLightSensorRate + ", " +
- "currentRate=" + lightSensorRate);
+ "previousRate=" + mCurrentLightSensorRate + ", " +
+ "currentRate=" + lightSensorRate);
}
mCurrentLightSensorRate = lightSensorRate;
mSensorManager.unregisterListener(mLightSensorListener);
@@ -439,12 +424,8 @@ class AutomaticBrightnessController {
}
}
- private boolean setScreenAutoBrightnessAdjustment(float adjustment) {
- if (adjustment != mScreenAutoBrightnessAdjustment) {
- mScreenAutoBrightnessAdjustment = adjustment;
- return true;
- }
- return false;
+ private boolean setAutoBrightnessAdjustment(float adjustment) {
+ return mBrightnessMapper.setAutoBrightnessAdjustment(adjustment);
}
private void setAmbientLux(float lux) {
@@ -466,12 +447,14 @@ class AutomaticBrightnessController {
final float maxAmbientLux =
mShortTermModelAnchor + mShortTermModelAnchor * SHORT_TERM_MODEL_THRESHOLD_RATIO;
if (minAmbientLux < mAmbientLux && mAmbientLux < maxAmbientLux) {
- Slog.d(TAG, "ShortTermModel: re-validate user data, ambient lux is " +
- minAmbientLux + " < " + mAmbientLux + " < " + maxAmbientLux);
+ if (DEBUG) {
+ Slog.d(TAG, "ShortTermModel: re-validate user data, ambient lux is " +
+ minAmbientLux + " < " + mAmbientLux + " < " + maxAmbientLux);
+ }
mShortTermModelValid = true;
} else {
Slog.d(TAG, "ShortTermModel: reset data, ambient lux is " + mAmbientLux +
- "(" + minAmbientLux + ", " + maxAmbientLux + ")");
+ "(" + minAmbientLux + ", " + maxAmbientLux + ")");
resetShortTermModel();
}
}
@@ -498,9 +481,9 @@ class AutomaticBrightnessController {
}
}
if (DEBUG) {
- Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=("
- + mAmbientLightRingBuffer.getTime(endIndex) + ", "
- + mAmbientLightRingBuffer.getLux(endIndex) + ")");
+ Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
+ mAmbientLightRingBuffer.getTime(endIndex) + ", " +
+ mAmbientLightRingBuffer.getLux(endIndex) + ")");
}
float sum = 0;
float totalWeight = 0;
@@ -517,8 +500,8 @@ class AutomaticBrightnessController {
float lux = mAmbientLightRingBuffer.getLux(i);
if (DEBUG) {
Slog.d(TAG, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
- "lux=" + lux + ", " +
- "weight=" + weight);
+ "lux=" + lux + ", " +
+ "weight=" + weight);
}
totalWeight += weight;
sum += lux * weight;
@@ -526,8 +509,8 @@ class AutomaticBrightnessController {
}
if (DEBUG) {
Slog.d(TAG, "calculateAmbientLux: " +
- "totalWeight=" + totalWeight + ", " +
- "newAmbientLux=" + (sum / totalWeight));
+ "totalWeight=" + totalWeight + ", " +
+ "newAmbientLux=" + (sum / totalWeight));
}
return sum / totalWeight;
}
@@ -581,8 +564,8 @@ class AutomaticBrightnessController {
if (time < timeWhenSensorWarmedUp) {
if (DEBUG) {
Slog.d(TAG, "updateAmbientLux: Sensor not ready yet: " +
- "time=" + time + ", " +
- "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
+ "time=" + time + ", " +
+ "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
}
mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
timeWhenSensorWarmedUp);
@@ -621,10 +604,10 @@ class AutomaticBrightnessController {
setAmbientLux(fastAmbientLux);
if (DEBUG) {
Slog.d(TAG, "updateAmbientLux: " +
- ((fastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " +
- "mBrighteningLuxThreshold=" + mBrighteningLuxThreshold + ", " +
- "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
- "mAmbientLux=" + mAmbientLux);
+ ((fastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " +
+ "mBrighteningLuxThreshold=" + mBrighteningLuxThreshold + ", " +
+ "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
+ "mAmbientLux=" + mAmbientLux);
}
updateAutoBrightness(true);
nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
@@ -641,7 +624,7 @@ class AutomaticBrightnessController {
nextTransitionTime > time ? nextTransitionTime : time + mNormalLightSensorRate;
if (DEBUG) {
Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " +
- nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
+ nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
}
mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
}
@@ -652,40 +635,17 @@ class AutomaticBrightnessController {
}
float value = mBrightnessMapper.getBrightness(mAmbientLux);
- float gamma = 1.0f;
-
- if (USE_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT
- && mScreenAutoBrightnessAdjustment != 0.0f) {
- final float adjGamma = MathUtils.pow(mScreenAutoBrightnessAdjustmentMaxGamma,
- Math.min(1.0f, Math.max(-1.0f, -mScreenAutoBrightnessAdjustment)));
- gamma *= adjGamma;
- if (DEBUG) {
- Slog.d(TAG, "updateAutoBrightness: adjGamma=" + adjGamma);
- }
- }
-
- if (gamma != 1.0f) {
- final float in = value;
- value = MathUtils.pow(value, gamma);
- if (DEBUG) {
- Slog.d(TAG, "updateAutoBrightness: " +
- "gamma=" + gamma + ", " +
- "in=" + in + ", " +
- "out=" + value);
- }
- }
int newScreenAutoBrightness =
clampScreenBrightness(Math.round(value * PowerManager.BRIGHTNESS_ON));
if (mScreenAutoBrightness != newScreenAutoBrightness) {
if (DEBUG) {
Slog.d(TAG, "updateAutoBrightness: " +
- "mScreenAutoBrightness=" + mScreenAutoBrightness + ", " +
- "newScreenAutoBrightness=" + newScreenAutoBrightness);
+ "mScreenAutoBrightness=" + mScreenAutoBrightness + ", " +
+ "newScreenAutoBrightness=" + newScreenAutoBrightness);
}
mScreenAutoBrightness = newScreenAutoBrightness;
- mLastScreenAutoBrightnessGamma = gamma;
if (sendUpdate) {
mCallbacks.updateBrightness();
}
@@ -700,10 +660,8 @@ class AutomaticBrightnessController {
private void prepareBrightnessAdjustmentSample() {
if (!mBrightnessAdjustmentSamplePending) {
mBrightnessAdjustmentSamplePending = true;
- mBrightnessAdjustmentSampleOldAdjustment = mScreenAutoBrightnessAdjustment;
mBrightnessAdjustmentSampleOldLux = mAmbientLuxValid ? mAmbientLux : -1;
mBrightnessAdjustmentSampleOldBrightness = mScreenAutoBrightness;
- mBrightnessAdjustmentSampleOldGamma = mLastScreenAutoBrightnessGamma;
} else {
mHandler.removeMessages(MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE);
}
@@ -725,22 +683,16 @@ class AutomaticBrightnessController {
if (mAmbientLuxValid && mScreenAutoBrightness >= 0) {
if (DEBUG) {
Slog.d(TAG, "Auto-brightness adjustment changed by user: " +
- "adj=" + mScreenAutoBrightnessAdjustment + ", " +
- "lux=" + mAmbientLux + ", " +
- "brightness=" + mScreenAutoBrightness + ", " +
- "gamma=" + mLastScreenAutoBrightnessGamma + ", " +
- "ring=" + mAmbientLightRingBuffer);
+ "lux=" + mAmbientLux + ", " +
+ "brightness=" + mScreenAutoBrightness + ", " +
+ "ring=" + mAmbientLightRingBuffer);
}
EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ,
- mBrightnessAdjustmentSampleOldAdjustment,
mBrightnessAdjustmentSampleOldLux,
mBrightnessAdjustmentSampleOldBrightness,
- mBrightnessAdjustmentSampleOldGamma,
- mScreenAutoBrightnessAdjustment,
mAmbientLux,
- mScreenAutoBrightness,
- mLastScreenAutoBrightnessGamma);
+ mScreenAutoBrightness);
}
}
}
diff --git a/com/android/server/display/BrightnessMappingStrategy.java b/com/android/server/display/BrightnessMappingStrategy.java
index 4313d172..f7439b9a 100644
--- a/com/android/server/display/BrightnessMappingStrategy.java
+++ b/com/android/server/display/BrightnessMappingStrategy.java
@@ -28,6 +28,7 @@ import android.util.Spline;
import com.android.internal.util.Preconditions;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.utils.Plog;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -41,11 +42,13 @@ import java.util.Arrays;
*/
public abstract class BrightnessMappingStrategy {
private static final String TAG = "BrightnessMappingStrategy";
- private static final boolean DEBUG = false;
+ private static final boolean DEBUG = true;
private static final float LUX_GRAD_SMOOTHING = 0.25f;
private static final float MAX_GRAD = 1.0f;
+ private static final Plog PLOG = Plog.createSystemPlog(TAG);
+
@Nullable
public static BrightnessMappingStrategy create(Resources resources) {
float[] luxLevels = getLuxLevels(resources.getIntArray(
@@ -54,6 +57,9 @@ public abstract class BrightnessMappingStrategy {
com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
float[] brightnessLevelsNits = getFloatArray(resources.obtainTypedArray(
com.android.internal.R.array.config_autoBrightnessDisplayValuesNits));
+ float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
+ com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
+ 1, 1);
float[] nitsRange = getFloatArray(resources.obtainTypedArray(
com.android.internal.R.array.config_screenBrightnessNits));
@@ -68,14 +74,16 @@ public abstract class BrightnessMappingStrategy {
com.android.internal.R.integer.config_screenBrightnessSettingMaximum);
if (backlightRange[0] > minimumBacklight
|| backlightRange[backlightRange.length - 1] < maximumBacklight) {
- Slog.w(TAG, "Screen brightness mapping does not cover whole range of available"
- + " backlight values, autobrightness functionality may be impaired.");
+ Slog.w(TAG, "Screen brightness mapping does not cover whole range of available " +
+ "backlight values, autobrightness functionality may be impaired.");
}
BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
builder.setCurve(luxLevels, brightnessLevelsNits);
- return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange);
+ return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange,
+ autoBrightnessAdjustmentMaxGamma);
} else if (isValidMapping(luxLevels, brightnessLevelsBacklight)) {
- return new SimpleMappingStrategy(luxLevels, brightnessLevelsBacklight);
+ return new SimpleMappingStrategy(luxLevels, brightnessLevelsBacklight,
+ autoBrightnessAdjustmentMaxGamma);
} else {
return null;
}
@@ -173,6 +181,26 @@ public abstract class BrightnessMappingStrategy {
public abstract float getBrightness(float lux);
/**
+ * Returns the current auto-brightness adjustment.
+ *
+ * The returned adjustment is a value in the range [-1.0, 1.0] such that
+ * {@code config_autoBrightnessAdjustmentMaxGamma<sup>-adjustment</sup>} is used to gamma
+ * correct the brightness curve.
+ */
+ public abstract float getAutoBrightnessAdjustment();
+
+ /**
+ * Sets the auto-brightness adjustment.
+ *
+ * @param adjustment The desired auto-brightness adjustment.
+ * @return Whether the auto-brightness adjustment has changed.
+ *
+ * @Deprecated The auto-brightness adjustment should not be set directly, but rather inferred
+ * from user data points.
+ */
+ public abstract boolean setAutoBrightnessAdjustment(float adjustment);
+
+ /**
* Converts the provided backlight value to nits if possible.
*
* Returns -1.0f if there's no available mapping for the backlight to nits.
@@ -200,12 +228,13 @@ public abstract class BrightnessMappingStrategy {
*/
public abstract void clearUserDataPoints();
- /** @return true if there are any short term adjustments applied to the curve */
+ /** @return True if there are any short term adjustments applied to the curve. */
public abstract boolean hasUserDataPoints();
- /** @return true if the current brightness config is the default one */
+ /** @return True if the current brightness configuration is the default one. */
public abstract boolean isDefaultConfig();
+ /** @return The default brightness configuration. */
public abstract BrightnessConfiguration getDefaultConfig();
public abstract void dump(PrintWriter pw);
@@ -216,22 +245,8 @@ public abstract class BrightnessMappingStrategy {
return (float) brightness / PowerManager.BRIGHTNESS_ON;
}
- private static Spline createSpline(float[] x, float[] y) {
- Spline spline = Spline.createSpline(x, y);
- if (DEBUG) {
- Slog.d(TAG, "Spline: " + spline);
- for (float v = 1f; v < x[x.length - 1] * 1.25f; v *= 1.25f) {
- Slog.d(TAG, String.format(" %7.1f: %7.1f", v, spline.interpolate(v)));
- }
- }
- return spline;
- }
-
private static Pair<float[], float[]> insertControlPoint(
float[] luxLevels, float[] brightnessLevels, float lux, float brightness) {
- if (DEBUG) {
- Slog.d(TAG, "Inserting new control point at (" + lux + ", " + brightness + ")");
- }
final int idx = findInsertionPoint(luxLevels, lux);
final float[] newLuxLevels;
final float[] newBrightnessLevels;
@@ -274,9 +289,7 @@ public abstract class BrightnessMappingStrategy {
private static void smoothCurve(float[] lux, float[] brightness, int idx) {
if (DEBUG) {
- Slog.d(TAG, "smoothCurve(lux=" + Arrays.toString(lux)
- + ", brightness=" + Arrays.toString(brightness)
- + ", idx=" + idx + ")");
+ PLOG.logCurve("unsmoothed curve", lux, brightness);
}
float prevLux = lux[idx];
float prevBrightness = brightness[idx];
@@ -294,7 +307,6 @@ public abstract class BrightnessMappingStrategy {
prevBrightness = newBrightness;
brightness[i] = newBrightness;
}
-
// Smooth curve for data points below the newly introduced point
prevLux = lux[idx];
prevBrightness = brightness[idx];
@@ -312,8 +324,7 @@ public abstract class BrightnessMappingStrategy {
brightness[i] = newBrightness;
}
if (DEBUG) {
- Slog.d(TAG, "Smoothed Curve: lux=" + Arrays.toString(lux)
- + ", brightness=" + Arrays.toString(brightness));
+ PLOG.logCurve("smoothed curve", lux, brightness);
}
}
@@ -323,6 +334,72 @@ public abstract class BrightnessMappingStrategy {
- MathUtils.log(prevLux + LUX_GRAD_SMOOTHING)));
}
+ private static float inferAutoBrightnessAdjustment(float maxGamma,
+ float desiredBrightness, float currentBrightness) {
+ float adjustment = 0;
+ float gamma = Float.NaN;
+ // Extreme edge cases: use a simpler heuristic, as proper gamma correction around the edges
+ // affects the curve rather drastically.
+ if (currentBrightness <= 0.1f || currentBrightness >= 0.9f) {
+ adjustment = (desiredBrightness - currentBrightness) * 2;
+ // Edge case: darkest adjustment possible.
+ } else if (desiredBrightness == 0) {
+ adjustment = -1;
+ // Edge case: brightest adjustment possible.
+ } else if (desiredBrightness == 1) {
+ adjustment = +1;
+ } else {
+ // current^gamma = desired => gamma = log[current](desired)
+ gamma = MathUtils.log(desiredBrightness) / MathUtils.log(currentBrightness);
+ // max^-adjustment = gamma => adjustment = -log[max](gamma)
+ adjustment = -MathUtils.log(gamma) / MathUtils.log(maxGamma);
+ }
+ adjustment = MathUtils.constrain(adjustment, -1, +1);
+ if (DEBUG) {
+ Slog.d(TAG, "inferAutoBrightnessAdjustment: " + maxGamma + "^" + -adjustment + "=" +
+ MathUtils.pow(maxGamma, -adjustment) + " == " + gamma);
+ Slog.d(TAG, "inferAutoBrightnessAdjustment: " + currentBrightness + "^" + gamma + "=" +
+ MathUtils.pow(currentBrightness, gamma) + " == " + desiredBrightness);
+ }
+ return adjustment;
+ }
+
+ private static Pair<float[], float[]> getAdjustedCurve(float[] lux, float[] brightness,
+ float userLux, float userBrightness, float adjustment, float maxGamma) {
+ float[] newLux = lux;
+ float[] newBrightness = Arrays.copyOf(brightness, brightness.length);
+ if (DEBUG) {
+ PLOG.logCurve("unadjusted curve", newLux, newBrightness);
+ }
+ adjustment = MathUtils.constrain(adjustment, -1, 1);
+ float gamma = MathUtils.pow(maxGamma, -adjustment);
+ if (DEBUG) {
+ Slog.d(TAG, "getAdjustedCurve: " + maxGamma + "^" + -adjustment + "=" +
+ MathUtils.pow(maxGamma, -adjustment) + " == " + gamma);
+ }
+ if (gamma != 1) {
+ for (int i = 0; i < newBrightness.length; i++) {
+ newBrightness[i] = MathUtils.pow(newBrightness[i], gamma);
+ }
+ }
+ if (DEBUG) {
+ PLOG.logCurve("gamma adjusted curve", newLux, newBrightness);
+ }
+ if (userLux != -1) {
+ Pair<float[], float[]> curve = insertControlPoint(newLux, newBrightness, userLux,
+ userBrightness);
+ newLux = curve.first;
+ newBrightness = curve.second;
+ if (DEBUG) {
+ PLOG.logCurve("gamma and user adjusted curve", newLux, newBrightness);
+ // This is done for comparison.
+ curve = insertControlPoint(lux, brightness, userLux, userBrightness);
+ PLOG.logCurve("user adjusted curve", curve.first ,curve.second);
+ }
+ }
+ return Pair.create(newLux, newBrightness);
+ }
+
/**
* A {@link BrightnessMappingStrategy} that maps from ambient room brightness directly to the
* backlight of the display.
@@ -337,10 +414,12 @@ public abstract class BrightnessMappingStrategy {
private final float[] mBrightness;
private Spline mSpline;
+ private float mMaxGamma;
+ private float mAutoBrightnessAdjustment;
private float mUserLux;
private float mUserBrightness;
- public SimpleMappingStrategy(float[] lux, int[] brightness) {
+ public SimpleMappingStrategy(float[] lux, int[] brightness, float maxGamma) {
Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
"Lux and brightness arrays must not be empty!");
Preconditions.checkArgument(lux.length == brightness.length,
@@ -357,9 +436,14 @@ public abstract class BrightnessMappingStrategy {
mBrightness[i] = normalizeAbsoluteBrightness(brightness[i]);
}
- mSpline = createSpline(mLux, mBrightness);
+ mMaxGamma = maxGamma;
+ mAutoBrightnessAdjustment = 0;
mUserLux = -1;
mUserBrightness = -1;
+ if (DEBUG) {
+ PLOG.start("simple mapping strategy");
+ }
+ computeSpline();
}
@Override
@@ -373,27 +457,65 @@ public abstract class BrightnessMappingStrategy {
}
@Override
+ public float getAutoBrightnessAdjustment() {
+ return mAutoBrightnessAdjustment;
+ }
+
+ @Override
+ public boolean setAutoBrightnessAdjustment(float adjustment) {
+ adjustment = MathUtils.constrain(adjustment, -1, 1);
+ if (adjustment == mAutoBrightnessAdjustment) {
+ return false;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "setAutoBrightnessAdjustment: " + mAutoBrightnessAdjustment + " => " +
+ adjustment);
+ PLOG.start("auto-brightness adjustment");
+ }
+ mAutoBrightnessAdjustment = adjustment;
+ computeSpline();
+ return true;
+ }
+
+ @Override
public float convertToNits(int backlight) {
return -1.0f;
}
@Override
public void addUserDataPoint(float lux, float brightness) {
+ float unadjustedBrightness = getUnadjustedBrightness(lux);
if (DEBUG){
- Slog.d(TAG, "addUserDataPoint(lux=" + lux + ", brightness=" + brightness + ")");
+ Slog.d(TAG, "addUserDataPoint: (" + lux + "," + brightness + ")");
+ PLOG.start("add user data point")
+ .logPoint("user data point", lux, brightness)
+ .logPoint("current brightness", lux, unadjustedBrightness);
+ }
+ float adjustment = inferAutoBrightnessAdjustment(mMaxGamma,
+ brightness /* desiredBrightness */,
+ unadjustedBrightness /* currentBrightness */);
+ if (DEBUG) {
+ Slog.d(TAG, "addUserDataPoint: " + mAutoBrightnessAdjustment + " => " +
+ adjustment);
}
- Pair<float[], float[]> curve = insertControlPoint(mLux, mBrightness, lux, brightness);
- mSpline = createSpline(curve.first, curve.second);
+ mAutoBrightnessAdjustment = adjustment;
mUserLux = lux;
mUserBrightness = brightness;
+ computeSpline();
}
@Override
public void clearUserDataPoints() {
if (mUserLux != -1) {
- mSpline = createSpline(mLux, mBrightness);
+ if (DEBUG) {
+ Slog.d(TAG, "clearUserDataPoints: " + mAutoBrightnessAdjustment + " => 0");
+ PLOG.start("clear user data points")
+ .logPoint("user data point", mUserLux, mUserBrightness);
+ }
+ mAutoBrightnessAdjustment = 0;
mUserLux = -1;
mUserBrightness = -1;
+ computeSpline();
}
}
@@ -408,15 +530,30 @@ public abstract class BrightnessMappingStrategy {
}
@Override
- public BrightnessConfiguration getDefaultConfig() { return null; }
+ public BrightnessConfiguration getDefaultConfig() {
+ return null;
+ }
@Override
public void dump(PrintWriter pw) {
pw.println("SimpleMappingStrategy");
pw.println(" mSpline=" + mSpline);
+ pw.println(" mMaxGamma=" + mMaxGamma);
+ pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
pw.println(" mUserLux=" + mUserLux);
pw.println(" mUserBrightness=" + mUserBrightness);
}
+
+ private void computeSpline() {
+ Pair<float[], float[]> curve = getAdjustedCurve(mLux, mBrightness, mUserLux,
+ mUserBrightness, mAutoBrightnessAdjustment, mMaxGamma);
+ mSpline = Spline.createSpline(curve.first, curve.second);
+ }
+
+ private float getUnadjustedBrightness(float lux) {
+ Spline spline = Spline.createSpline(mLux, mBrightness);
+ return spline.interpolate(lux);
+ }
}
/** A {@link BrightnessMappingStrategy} that maps from ambient room brightness to the physical
@@ -445,11 +582,13 @@ public abstract class BrightnessMappingStrategy {
// a brightness in nits.
private Spline mBacklightToNitsSpline;
+ private float mMaxGamma;
+ private float mAutoBrightnessAdjustment;
private float mUserLux;
private float mUserBrightness;
- public PhysicalMappingStrategy(BrightnessConfiguration config,
- float[] nits, int[] backlight) {
+ public PhysicalMappingStrategy(BrightnessConfiguration config, float[] nits,
+ int[] backlight, float maxGamma) {
Preconditions.checkArgument(nits.length != 0 && backlight.length != 0,
"Nits and backlight arrays must not be empty!");
Preconditions.checkArgument(nits.length == backlight.length,
@@ -459,6 +598,8 @@ public abstract class BrightnessMappingStrategy {
Preconditions.checkArrayElementsInRange(backlight,
PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON, "backlight");
+ mMaxGamma = maxGamma;
+ mAutoBrightnessAdjustment = 0;
mUserLux = -1;
mUserBrightness = -1;
@@ -469,11 +610,15 @@ public abstract class BrightnessMappingStrategy {
normalizedBacklight[i] = normalizeAbsoluteBrightness(backlight[i]);
}
- mNitsToBacklightSpline = createSpline(nits, normalizedBacklight);
- mBacklightToNitsSpline = createSpline(normalizedBacklight, nits);
+ mNitsToBacklightSpline = Spline.createSpline(nits, normalizedBacklight);
+ mBacklightToNitsSpline = Spline.createSpline(normalizedBacklight, nits);
mDefaultConfig = config;
- setBrightnessConfiguration(config);
+ if (DEBUG) {
+ PLOG.start("physical mapping strategy");
+ }
+ mConfig = config;
+ computeSpline();
}
@Override
@@ -484,10 +629,11 @@ public abstract class BrightnessMappingStrategy {
if (config.equals(mConfig)) {
return false;
}
-
- Pair<float[], float[]> curve = config.getCurve();
- mBrightnessSpline = createSpline(curve.first /*lux*/, curve.second /*nits*/);
+ if (DEBUG) {
+ PLOG.start("brightness configuration");
+ }
mConfig = config;
+ computeSpline();
return true;
}
@@ -499,31 +645,65 @@ public abstract class BrightnessMappingStrategy {
}
@Override
+ public float getAutoBrightnessAdjustment() {
+ return mAutoBrightnessAdjustment;
+ }
+
+ @Override
+ public boolean setAutoBrightnessAdjustment(float adjustment) {
+ adjustment = MathUtils.constrain(adjustment, -1, 1);
+ if (adjustment == mAutoBrightnessAdjustment) {
+ return false;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "setAutoBrightnessAdjustment: " + mAutoBrightnessAdjustment + " => " +
+ adjustment);
+ PLOG.start("auto-brightness adjustment");
+ }
+ mAutoBrightnessAdjustment = adjustment;
+ computeSpline();
+ return true;
+ }
+
+ @Override
public float convertToNits(int backlight) {
return mBacklightToNitsSpline.interpolate(normalizeAbsoluteBrightness(backlight));
}
@Override
- public void addUserDataPoint(float lux, float backlight) {
+ public void addUserDataPoint(float lux, float brightness) {
+ float unadjustedBrightness = getUnadjustedBrightness(lux);
if (DEBUG){
- Slog.d(TAG, "addUserDataPoint(lux=" + lux + ", backlight=" + backlight + ")");
+ Slog.d(TAG, "addUserDataPoint: (" + lux + "," + brightness + ")");
+ PLOG.start("add user data point")
+ .logPoint("user data point", lux, brightness)
+ .logPoint("current brightness", lux, unadjustedBrightness);
}
- float brightness = mBacklightToNitsSpline.interpolate(backlight);
- Pair<float[], float[]> defaultCurve = mConfig.getCurve();
- Pair<float[], float[]> newCurve =
- insertControlPoint(defaultCurve.first, defaultCurve.second, lux, brightness);
- mBrightnessSpline = createSpline(newCurve.first, newCurve.second);
+ float adjustment = inferAutoBrightnessAdjustment(mMaxGamma,
+ brightness /* desiredBrightness */,
+ unadjustedBrightness /* currentBrightness */);
+ if (DEBUG) {
+ Slog.d(TAG, "addUserDataPoint: " + mAutoBrightnessAdjustment + " => " +
+ adjustment);
+ }
+ mAutoBrightnessAdjustment = adjustment;
mUserLux = lux;
mUserBrightness = brightness;
+ computeSpline();
}
@Override
public void clearUserDataPoints() {
if (mUserLux != -1) {
- Pair<float[], float[]> defaultCurve = mConfig.getCurve();
- mBrightnessSpline = createSpline(defaultCurve.first, defaultCurve.second);
+ if (DEBUG) {
+ Slog.d(TAG, "clearUserDataPoints: " + mAutoBrightnessAdjustment + " => 0");
+ PLOG.start("clear user data points")
+ .logPoint("user data point", mUserLux, mUserBrightness);
+ }
+ mAutoBrightnessAdjustment = 0;
mUserLux = -1;
mUserBrightness = -1;
+ computeSpline();
}
}
@@ -538,7 +718,9 @@ public abstract class BrightnessMappingStrategy {
}
@Override
- public BrightnessConfiguration getDefaultConfig() { return mDefaultConfig; }
+ public BrightnessConfiguration getDefaultConfig() {
+ return mDefaultConfig;
+ }
@Override
public void dump(PrintWriter pw) {
@@ -546,8 +728,35 @@ public abstract class BrightnessMappingStrategy {
pw.println(" mConfig=" + mConfig);
pw.println(" mBrightnessSpline=" + mBrightnessSpline);
pw.println(" mNitsToBacklightSpline=" + mNitsToBacklightSpline);
+ pw.println(" mMaxGamma=" + mMaxGamma);
+ pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
pw.println(" mUserLux=" + mUserLux);
pw.println(" mUserBrightness=" + mUserBrightness);
}
+
+ private void computeSpline() {
+ Pair<float[], float[]> defaultCurve = mConfig.getCurve();
+ float[] defaultLux = defaultCurve.first;
+ float[] defaultNits = defaultCurve.second;
+ float[] defaultBacklight = new float[defaultNits.length];
+ for (int i = 0; i < defaultBacklight.length; i++) {
+ defaultBacklight[i] = mNitsToBacklightSpline.interpolate(defaultNits[i]);
+ }
+ Pair<float[], float[]> curve = getAdjustedCurve(defaultLux, defaultBacklight, mUserLux,
+ mUserBrightness, mAutoBrightnessAdjustment, mMaxGamma);
+ float[] lux = curve.first;
+ float[] backlight = curve.second;
+ float[] nits = new float[backlight.length];
+ for (int i = 0; i < nits.length; i++) {
+ nits[i] = mBacklightToNitsSpline.interpolate(backlight[i]);
+ }
+ mBrightnessSpline = Spline.createSpline(lux, nits);
+ }
+
+ private float getUnadjustedBrightness(float lux) {
+ Pair<float[], float[]> curve = mConfig.getCurve();
+ Spline spline = Spline.createSpline(curve.first, curve.second);
+ return mNitsToBacklightSpline.interpolate(spline.interpolate(lux));
+ }
}
}
diff --git a/com/android/server/display/BrightnessTracker.java b/com/android/server/display/BrightnessTracker.java
index 88df195e..905c7e37 100644
--- a/com/android/server/display/BrightnessTracker.java
+++ b/com/android/server/display/BrightnessTracker.java
@@ -686,10 +686,15 @@ public class BrightnessTracker {
}
public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(int userId) {
- ArrayList<AmbientBrightnessDayStats> stats = mAmbientBrightnessStatsTracker.getUserStats(
- userId);
- return (stats != null) ? new ParceledListSlice<>(stats) : new ParceledListSlice<>(
- Collections.EMPTY_LIST);
+ if (mAmbientBrightnessStatsTracker != null) {
+ ArrayList<AmbientBrightnessDayStats> stats =
+ mAmbientBrightnessStatsTracker.getUserStats(
+ userId);
+ if (stats != null) {
+ return new ParceledListSlice<>(stats);
+ }
+ }
+ return ParceledListSlice.emptyList();
}
// Not allowed to keep the SensorEvent so used to copy the data we care about.
diff --git a/com/android/server/display/ColorDisplayService.java b/com/android/server/display/ColorDisplayService.java
index b3d309dd..37035c3a 100644
--- a/com/android/server/display/ColorDisplayService.java
+++ b/com/android/server/display/ColorDisplayService.java
@@ -300,6 +300,11 @@ public final class ColorDisplayService extends SystemService
dtm.setColorMode(mode, mIsActivated ? mMatrixNight : MATRIX_IDENTITY);
}
+ @Override
+ public void onAccessibilityTransformChanged(boolean state) {
+ onDisplayColorModeChanged(mController.getColorMode());
+ }
+
/**
* Set coefficients based on native mode. Use DisplayTransformManager#isNativeModeEnabled while
* setting is stable; when setting is changing, pass native mode selection directly.
diff --git a/com/android/server/display/DisplayManagerService.java b/com/android/server/display/DisplayManagerService.java
index c7ae1f4f..0f531a80 100644
--- a/com/android/server/display/DisplayManagerService.java
+++ b/com/android/server/display/DisplayManagerService.java
@@ -36,11 +36,13 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.graphics.Point;
import android.hardware.SensorManager;
import android.hardware.display.AmbientBrightnessDayStats;
import android.hardware.display.BrightnessChangeEvent;
import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.Curve;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.DisplayViewport;
@@ -61,16 +63,21 @@ import android.os.Message;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.ServiceManager;
+import android.os.ShellCallback;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.IntArray;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.Spline;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
@@ -273,6 +280,11 @@ public final class DisplayManagerService extends SystemService {
private final Injector mInjector;
+ // The minimum brightness curve, which guarantess that any brightness curve that dips below it
+ // is rejected by the system.
+ private final Curve mMinimumBrightnessCurve;
+ private final Spline mMinimumBrightnessSpline;
+
public DisplayManagerService(Context context) {
this(context, new Injector());
}
@@ -286,8 +298,15 @@ public final class DisplayManagerService extends SystemService {
mUiHandler = UiThread.getHandler();
mDisplayAdapterListener = new DisplayAdapterListener();
mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
+ Resources resources = mContext.getResources();
mDefaultDisplayDefaultColorMode = mContext.getResources().getInteger(
com.android.internal.R.integer.config_defaultDisplayDefaultColorMode);
+ float[] lux = getFloatArray(resources.obtainTypedArray(
+ com.android.internal.R.array.config_minimumBrightnessCurveLux));
+ float[] nits = getFloatArray(resources.obtainTypedArray(
+ com.android.internal.R.array.config_minimumBrightnessCurveNits));
+ mMinimumBrightnessCurve = new Curve(lux, nits);
+ mMinimumBrightnessSpline = Spline.createSpline(lux, nits);
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
@@ -1029,9 +1048,15 @@ public final class DisplayManagerService extends SystemService {
}
}
+ @VisibleForTesting
+ Curve getMinimumBrightnessCurveInternal() {
+ return mMinimumBrightnessCurve;
+ }
+
private void setBrightnessConfigurationForUserInternal(
- @NonNull BrightnessConfiguration c, @UserIdInt int userId,
+ @Nullable BrightnessConfiguration c, @UserIdInt int userId,
@Nullable String packageName) {
+ validateBrightnessConfiguration(c);
final int userSerial = getUserManager().getUserSerialNumber(userId);
synchronized (mSyncRoot) {
try {
@@ -1046,6 +1071,28 @@ public final class DisplayManagerService extends SystemService {
}
}
+ @VisibleForTesting
+ void validateBrightnessConfiguration(BrightnessConfiguration config) {
+ if (config == null) {
+ return;
+ }
+ if (isBrightnessConfigurationTooDark(config)) {
+ throw new IllegalArgumentException("brightness curve is too dark");
+ }
+ }
+
+ private boolean isBrightnessConfigurationTooDark(BrightnessConfiguration config) {
+ Pair<float[], float[]> curve = config.getCurve();
+ float[] lux = curve.first;
+ float[] nits = curve.second;
+ for (int i = 0; i < lux.length; i++) {
+ if (nits[i] < mMinimumBrightnessSpline.interpolate(lux[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void loadBrightnessConfiguration() {
synchronized (mSyncRoot) {
final int userSerial = getUserManager().getUserSerialNumber(mCurrentUserId);
@@ -1366,6 +1413,16 @@ public final class DisplayManagerService extends SystemService {
}
}
+ private static float[] getFloatArray(TypedArray array) {
+ int length = array.length();
+ float[] floatArray = new float[length];
+ for (int i = 0; i < length; i++) {
+ floatArray[i] = array.getFloat(i, Float.NaN);
+ }
+ array.recycle();
+ return floatArray;
+ }
+
/**
* This is the object that everything in the display manager locks on.
* We make it an inner class within the {@link DisplayManagerService} to so that it is
@@ -1983,6 +2040,39 @@ public final class DisplayManagerService extends SystemService {
}
}
+ @Override // Binder call
+ public void onShellCommand(FileDescriptor in, FileDescriptor out,
+ FileDescriptor err, String[] args, ShellCallback callback,
+ ResultReceiver resultReceiver) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ DisplayManagerShellCommand command = new DisplayManagerShellCommand(this);
+ command.exec(this, in, out, err, args, callback, resultReceiver);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override // Binder call
+ public Curve getMinimumBrightnessCurve() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return getMinimumBrightnessCurveInternal();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ void setBrightness(int brightness) {
+ Settings.System.putIntForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS, brightness, UserHandle.USER_CURRENT);
+ }
+
+ void resetBrightnessConfiguration() {
+ setBrightnessConfigurationForUserInternal(null, mContext.getUserId(),
+ mContext.getPackageName());
+ }
+
private boolean validatePackageName(int uid, String packageName) {
if (packageName != null) {
String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
diff --git a/com/android/server/display/DisplayManagerShellCommand.java b/com/android/server/display/DisplayManagerShellCommand.java
new file mode 100644
index 00000000..27cad1ee
--- /dev/null
+++ b/com/android/server/display/DisplayManagerShellCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.server.display;
+
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.lang.NumberFormatException;
+
+class DisplayManagerShellCommand extends ShellCommand {
+ private static final String TAG = "DisplayManagerShellCommand";
+
+ private final DisplayManagerService.BinderService mService;
+
+ DisplayManagerShellCommand(DisplayManagerService.BinderService service) {
+ mService = service;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ final PrintWriter pw = getOutPrintWriter();
+ switch(cmd) {
+ case "set-brightness":
+ return setBrightness();
+ case "reset-brightness-configuration":
+ return resetBrightnessConfiguration();
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+ pw.println("Display manager commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println();
+ pw.println(" set-brightness BRIGHTNESS");
+ pw.println(" Sets the current brightness to BRIGHTNESS (a number between 0 and 1).");
+ pw.println(" reset-brightness-configuration");
+ pw.println(" Reset the brightness to its default configuration.");
+ pw.println();
+ Intent.printIntentArgsHelp(pw , "");
+ }
+
+ private int setBrightness() {
+ String brightnessText = getNextArg();
+ if (brightnessText == null) {
+ getErrPrintWriter().println("Error: no brightness specified");
+ return 1;
+ }
+ float brightness = -1;
+ try {
+ brightness = Float.parseFloat(brightnessText);
+ } catch (NumberFormatException e) {
+ }
+ if (brightness < 0 || brightness > 1) {
+ getErrPrintWriter().println("Error: brightness should be a number between 0 and 1");
+ return 1;
+ }
+ mService.setBrightness((int) brightness * 255);
+ return 0;
+ }
+
+ private int resetBrightnessConfiguration() {
+ mService.resetBrightnessConfiguration();
+ return 0;
+ }
+}
diff --git a/com/android/server/display/DisplayPowerController.java b/com/android/server/display/DisplayPowerController.java
index 1784ef14..5f4c8efd 100644
--- a/com/android/server/display/DisplayPowerController.java
+++ b/com/android/server/display/DisplayPowerController.java
@@ -424,9 +424,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
int ambientLightHorizon = resources.getInteger(
com.android.internal.R.integer.config_autoBrightnessAmbientLightHorizon);
- float autoBrightnessAdjustmentMaxGamma = resources.getFraction(
- com.android.internal.R.fraction.config_autoBrightnessAdjustmentMaxGamma,
- 1, 1);
int lightSensorWarmUpTimeConfig = resources.getInteger(
com.android.internal.R.integer.config_lightSensorWarmupTime);
@@ -450,7 +447,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mScreenBrightnessRangeMaximum, dozeScaleFactor, lightSensorRate,
initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
autoBrightnessResetAmbientLuxAfterWarmUp, ambientLightHorizon,
- autoBrightnessAdjustmentMaxGamma, dynamicHysteresis);
+ dynamicHysteresis);
} else {
mUseSoftwareAutoBrightnessConfig = false;
}
diff --git a/com/android/server/display/LocalDisplayAdapter.java b/com/android/server/display/LocalDisplayAdapter.java
index 5ca9abc8..b9a279ad 100644
--- a/com/android/server/display/LocalDisplayAdapter.java
+++ b/com/android/server/display/LocalDisplayAdapter.java
@@ -436,6 +436,10 @@ final class LocalDisplayAdapter extends DisplayAdapter {
com.android.internal.R.bool.config_localDisplaysMirrorContent)) {
mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY;
}
+
+ if (res.getBoolean(com.android.internal.R.bool.config_localDisplaysPrivate)) {
+ mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE;
+ }
}
}
return mInfo;
diff --git a/com/android/server/display/utils/Plog.java b/com/android/server/display/utils/Plog.java
new file mode 100644
index 00000000..3a18387c
--- /dev/null
+++ b/com/android/server/display/utils/Plog.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.server.display.utils;
+
+
+import java.lang.StringBuilder;
+import java.lang.System;
+
+import android.util.Slog;
+
+/**
+ * A utility to log multiple points and curves in a structured way so they can be easily consumed
+ * by external tooling
+ *
+ * To start a plot, call {@link Plog.start} with the plot's title; to add a point to it, call
+ * {@link Plog.logPoint} with the point name (that will appear in the legend) and coordinates; and
+ * to log a curve, call {@link Plog.logCurve} with its name and points.
+ */
+public abstract class Plog {
+ // A unique identifier used to group points and curves that belong on the same plot.
+ private long mId;
+
+ /**
+ * Returns a Plog instance that emits messages to the system log.
+ *
+ * @param tag The tag of the emitted messages in the system log.
+ * @return A plog instance that emits messages to the system log.
+ */
+ public static Plog createSystemPlog(String tag) {
+ return new SystemPlog(tag);
+ }
+
+ /**
+ * Start a new plot.
+ *
+ * @param title The plot title.
+ * @return The Plog instance (for chaining).
+ */
+ public Plog start(String title) {
+ mId = System.currentTimeMillis();
+ write(formatTitle(title));
+ return this;
+ }
+
+ /**
+ * Adds a point to the current plot.
+ *
+ * @param name The point name (that will appear in the legend).
+ * @param x The point x coordinate.
+ * @param y The point y coordinate.
+ * @return The Plog instance (for chaining).
+ */
+ public Plog logPoint(String name, float x, float y) {
+ write(formatPoint(name, x, y));
+ return this;
+ }
+
+ /**
+ * Adds a curve to the current plot.
+ *
+ * @param name The curve name (that will appear in the legend).
+ * @param xs The curve x coordinates.
+ * @param ys The curve y coordinates.
+ * @return The Plog instance (for chaining).
+ */
+ public Plog logCurve(String name, float[] xs, float[] ys) {
+ write(formatCurve(name, xs, ys));
+ return this;
+ }
+
+ private String formatTitle(String title) {
+ return "title: " + title;
+ }
+
+ private String formatPoint(String name, float x, float y) {
+ return "point: " + name + ": (" + x + "," + y + ")";
+ }
+
+ private String formatCurve(String name, float[] xs, float[] ys) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("curve: " + name + ": [");
+ int n = xs.length <= ys.length ? xs.length : ys.length;
+ for (int i = 0; i < n; i++) {
+ sb.append("(" + xs[i] + "," + ys[i] + "),");
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ private void write(String message) {
+ emit("[PLOG " + mId + "] " + message);
+ }
+
+ /**
+ * Emits a message (depending on the concrete Plog implementation).
+ *
+ * @param message The message.
+ */
+ protected abstract void emit(String message);
+
+ /**
+ * A Plog that emits messages to the system log.
+ */
+ public static class SystemPlog extends Plog {
+ // The tag of the emitted messages in the system log.
+ private final String mTag;
+
+ /**
+ * Returns a Plog instance that emits messages to the system log.
+ *
+ * @param tag The tag of the emitted messages in the system log.
+ * @return A Plog instance that emits messages to the system log.
+ */
+ public SystemPlog(String tag) {
+ mTag = tag;
+ }
+
+ /**
+ * Emits a message to the system log.
+ *
+ * @param message The message.
+ */
+ protected void emit(String message) {
+ Slog.d(mTag, message);
+ }
+ }
+}
diff --git a/com/android/server/fingerprint/AuthenticationClient.java b/com/android/server/fingerprint/AuthenticationClient.java
index 8be2c9ee..afd1a94b 100644
--- a/com/android/server/fingerprint/AuthenticationClient.java
+++ b/com/android/server/fingerprint/AuthenticationClient.java
@@ -18,8 +18,8 @@ package com.android.server.fingerprint;
import android.content.Context;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
-import android.hardware.biometrics.BiometricDialog;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.IFingerprintServiceReceiver;
@@ -46,8 +46,8 @@ public abstract class AuthenticationClient extends ClientMonitor {
public static final int LOCKOUT_PERMANENT = 2;
// Callback mechanism received from the client
- // (BiometricDialog -> FingerprintManager -> FingerprintService -> AuthenticationClient)
- private IBiometricDialogReceiver mDialogReceiverFromClient;
+ // (BiometricPrompt -> FingerprintManager -> FingerprintService -> AuthenticationClient)
+ private IBiometricPromptReceiver mDialogReceiverFromClient;
private Bundle mBundle;
private IStatusBarService mStatusBarService;
private boolean mInLockout;
@@ -55,13 +55,13 @@ public abstract class AuthenticationClient extends ClientMonitor {
protected boolean mDialogDismissed;
// Receives events from SystemUI and handles them before forwarding them to FingerprintDialog
- protected IBiometricDialogReceiver mDialogReceiver = new IBiometricDialogReceiver.Stub() {
+ protected IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
@Override // binder call
public void onDialogDismissed(int reason) {
if (mBundle != null && mDialogReceiverFromClient != null) {
try {
mDialogReceiverFromClient.onDialogDismissed(reason);
- if (reason == BiometricDialog.DISMISSED_REASON_USER_CANCEL) {
+ if (reason == BiometricPrompt.DISMISSED_REASON_USER_CANCEL) {
onError(FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED,
0 /* vendorCode */);
}
@@ -88,7 +88,7 @@ public abstract class AuthenticationClient extends ClientMonitor {
public AuthenticationClient(Context context, long halDeviceId, IBinder token,
IFingerprintServiceReceiver receiver, int targetUserId, int groupId, long opId,
boolean restricted, String owner, Bundle bundle,
- IBiometricDialogReceiver dialogReceiver, IStatusBarService statusBarService) {
+ IBiometricPromptReceiver dialogReceiver, IStatusBarService statusBarService) {
super(context, halDeviceId, token, receiver, targetUserId, groupId, restricted, owner);
mOpId = opId;
mBundle = bundle;
@@ -299,7 +299,7 @@ public abstract class AuthenticationClient extends ClientMonitor {
// If the user already cancelled authentication (via some interaction with the
// dialog, we do not need to hide it since it's already hidden.
// If the device is in lockout, don't hide the dialog - it will automatically hide
- // after BiometricDialog.HIDE_DIALOG_DELAY
+ // after BiometricPrompt.HIDE_DIALOG_DELAY
if (mBundle != null && !mDialogDismissed && !mInLockout) {
try {
mStatusBarService.hideFingerprintDialog();
diff --git a/com/android/server/fingerprint/FingerprintService.java b/com/android/server/fingerprint/FingerprintService.java
index 92d3772e..4a1beb19 100644
--- a/com/android/server/fingerprint/FingerprintService.java
+++ b/com/android/server/fingerprint/FingerprintService.java
@@ -38,7 +38,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.IBiometricPromptReceiver;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprintClientCallback;
import android.hardware.fingerprint.Fingerprint;
@@ -230,10 +230,11 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
}
List<ActivityManager.RunningTaskInfo> runningTasks = mActivityManager.getTasks(1);
if (!runningTasks.isEmpty()) {
- if (runningTasks.get(0).topActivity.getPackageName()
- != mCurrentClient.getOwnerString()) {
+ final String topPackage = runningTasks.get(0).topActivity.getPackageName();
+ if (!topPackage.contentEquals(mCurrentClient.getOwnerString())) {
mCurrentClient.stop(false /* initiatedByClient */);
- Slog.e(TAG, "Stopping background authentication");
+ Slog.e(TAG, "Stopping background authentication, top: " + topPackage
+ + " currentClient: " + mCurrentClient.getOwnerString());
}
}
} catch (RemoteException e) {
@@ -849,7 +850,7 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
private void startAuthentication(IBinder token, long opId, int callingUserId, int groupId,
IFingerprintServiceReceiver receiver, int flags, boolean restricted,
- String opPackageName, Bundle bundle, IBiometricDialogReceiver dialogReceiver) {
+ String opPackageName, Bundle bundle, IBiometricPromptReceiver dialogReceiver) {
updateActiveGroup(groupId, opPackageName);
if (DEBUG) Slog.v(TAG, "startAuthentication(" + opPackageName + ")");
@@ -1160,7 +1161,7 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
public void authenticate(final IBinder token, final long opId, final int groupId,
final IFingerprintServiceReceiver receiver, final int flags,
final String opPackageName, final Bundle bundle,
- final IBiometricDialogReceiver dialogReceiver) {
+ final IBiometricPromptReceiver dialogReceiver) {
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final int callingUserId = UserHandle.getCallingUserId();
diff --git a/com/android/server/input/InputWindowHandle.java b/com/android/server/input/InputWindowHandle.java
index 3d6f7ad1..720eaaa5 100644
--- a/com/android/server/input/InputWindowHandle.java
+++ b/com/android/server/input/InputWindowHandle.java
@@ -92,7 +92,7 @@ public final class InputWindowHandle {
public int inputFeatures;
// Display this input is on.
- public final int displayId;
+ public int displayId;
private native void nativeDispose();
diff --git a/com/android/server/job/JobSchedulerService.java b/com/android/server/job/JobSchedulerService.java
index 66817fad..736aa463 100644
--- a/com/android/server/job/JobSchedulerService.java
+++ b/com/android/server/job/JobSchedulerService.java
@@ -127,7 +127,7 @@ import java.util.function.Predicate;
* Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}.
* @hide
*/
-public final class JobSchedulerService extends com.android.server.SystemService
+public class JobSchedulerService extends com.android.server.SystemService
implements StateChangedListener, JobCompletedListener {
public static final String TAG = "JobScheduler";
public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -781,6 +781,10 @@ public final class JobSchedulerService extends com.android.server.SystemService
}
};
+ public Context getTestableContext() {
+ return getContext();
+ }
+
public Object getLock() {
return mLock;
}
diff --git a/com/android/server/job/controllers/ConnectivityController.java b/com/android/server/job/controllers/ConnectivityController.java
index 8365fd2e..0c66c5b2 100644
--- a/com/android/server/job/controllers/ConnectivityController.java
+++ b/com/android/server/job/controllers/ConnectivityController.java
@@ -30,12 +30,12 @@ import android.net.NetworkInfo;
import android.net.NetworkPolicyManager;
import android.net.NetworkRequest;
import android.net.TrafficStats;
-import android.os.Process;
import android.os.UserHandle;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
@@ -46,6 +46,7 @@ import com.android.server.job.JobSchedulerService.Constants;
import com.android.server.job.JobServiceContext;
import com.android.server.job.StateControllerProto;
+import java.util.Objects;
import java.util.function.Predicate;
/**
@@ -63,7 +64,6 @@ public final class ConnectivityController extends StateController implements
private final ConnectivityManager mConnManager;
private final NetworkPolicyManager mNetPolicyManager;
- private boolean mConnected;
@GuardedBy("mLock")
private final ArraySet<JobStatus> mTrackedJobs = new ArraySet<>();
@@ -74,9 +74,11 @@ public final class ConnectivityController extends StateController implements
mConnManager = mContext.getSystemService(ConnectivityManager.class);
mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
- mConnected = false;
+ // We're interested in all network changes; internally we match these
+ // network changes against the active network for each UID with jobs.
+ final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+ mConnManager.registerNetworkCallback(request, mNetworkCallback);
- mConnManager.registerDefaultNetworkCallback(mNetworkCallback);
mNetPolicyManager.registerListener(mNetPolicyListener);
}
@@ -198,14 +200,18 @@ public final class ConnectivityController extends StateController implements
}
private boolean updateConstraintsSatisfied(JobStatus jobStatus) {
+ final Network network = mConnManager.getActiveNetworkForUid(jobStatus.getSourceUid());
+ final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
+ return updateConstraintsSatisfied(jobStatus, network, capabilities);
+ }
+
+ private boolean updateConstraintsSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
// TODO: consider matching against non-active networks
- final int jobUid = jobStatus.getSourceUid();
final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
-
- final Network network = mConnManager.getActiveNetworkForUid(jobUid, ignoreBlocked);
- final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, jobUid, ignoreBlocked);
- final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
+ final NetworkInfo info = mConnManager.getNetworkInfoForUid(network,
+ jobStatus.getSourceUid(), ignoreBlocked);
final boolean connected = (info != null) && info.isConnected();
final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants);
@@ -218,12 +224,6 @@ public final class ConnectivityController extends StateController implements
// using non-default routes.
jobStatus.network = network;
- // Track system-uid connected/validated as a general reportable proxy for the
- // overall state of connectivity constraint satisfiability.
- if (jobUid == Process.SYSTEM_UID) {
- mConnected = connected;
- }
-
if (DEBUG) {
Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged")
+ " for " + jobStatus + ": connected=" + connected
@@ -233,18 +233,48 @@ public final class ConnectivityController extends StateController implements
}
/**
- * Update all jobs tracked by this controller.
+ * Update any jobs tracked by this controller that match given filters.
*
- * @param uid only update jobs belonging to this UID, or {@code -1} to
+ * @param filterUid only update jobs belonging to this UID, or {@code -1} to
* update all tracked jobs.
+ * @param filterNetwork only update jobs that would use this
+ * {@link Network}, or {@code null} to update all tracked jobs.
*/
- private void updateTrackedJobs(int uid) {
+ private void updateTrackedJobs(int filterUid, Network filterNetwork) {
synchronized (mLock) {
+ // Since this is a really hot codepath, temporarily cache any
+ // answers that we get from ConnectivityManager.
+ final SparseArray<Network> uidToNetwork = new SparseArray<>();
+ final SparseArray<NetworkCapabilities> networkToCapabilities = new SparseArray<>();
+
boolean changed = false;
- for (int i = mTrackedJobs.size()-1; i >= 0; i--) {
+ for (int i = mTrackedJobs.size() - 1; i >= 0; i--) {
final JobStatus js = mTrackedJobs.valueAt(i);
- if (uid == -1 || uid == js.getSourceUid()) {
- changed |= updateConstraintsSatisfied(js);
+ final int uid = js.getSourceUid();
+
+ final boolean uidMatch = (filterUid == -1 || filterUid == uid);
+ if (uidMatch) {
+ Network network = uidToNetwork.get(uid);
+ if (network == null) {
+ network = mConnManager.getActiveNetworkForUid(uid);
+ uidToNetwork.put(uid, network);
+ }
+
+ // Update either when we have a network match, or when the
+ // job hasn't yet been evaluated against the currently
+ // active network; typically when we just lost a network.
+ final boolean networkMatch = (filterNetwork == null
+ || Objects.equals(filterNetwork, network));
+ final boolean forceUpdate = !Objects.equals(js.network, network);
+ if (networkMatch || forceUpdate) {
+ final int netId = network != null ? network.netId : -1;
+ NetworkCapabilities capabilities = networkToCapabilities.get(netId);
+ if (capabilities == null) {
+ capabilities = mConnManager.getNetworkCapabilities(network);
+ networkToCapabilities.put(netId, capabilities);
+ }
+ changed |= updateConstraintsSatisfied(js, network, capabilities);
+ }
}
}
if (changed) {
@@ -273,19 +303,19 @@ public final class ConnectivityController extends StateController implements
private final NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
- public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
if (DEBUG) {
- Slog.v(TAG, "onCapabilitiesChanged() : " + networkCapabilities);
+ Slog.v(TAG, "onCapabilitiesChanged: " + network);
}
- updateTrackedJobs(-1);
+ updateTrackedJobs(-1, network);
}
@Override
public void onLost(Network network) {
if (DEBUG) {
- Slog.v(TAG, "Network lost");
+ Slog.v(TAG, "onLost: " + network);
}
- updateTrackedJobs(-1);
+ updateTrackedJobs(-1, network);
}
};
@@ -293,25 +323,9 @@ public final class ConnectivityController extends StateController implements
@Override
public void onUidRulesChanged(int uid, int uidRules) {
if (DEBUG) {
- Slog.v(TAG, "Uid rules changed for " + uid);
+ Slog.v(TAG, "onUidRulesChanged: " + uid);
}
- updateTrackedJobs(uid);
- }
-
- @Override
- public void onRestrictBackgroundChanged(boolean restrictBackground) {
- if (DEBUG) {
- Slog.v(TAG, "Background restriction change to " + restrictBackground);
- }
- updateTrackedJobs(-1);
- }
-
- @Override
- public void onUidPoliciesChanged(int uid, int uidPolicies) {
- if (DEBUG) {
- Slog.v(TAG, "Uid policy changed for " + uid);
- }
- updateTrackedJobs(uid);
+ updateTrackedJobs(uid, null);
}
};
@@ -319,9 +333,6 @@ public final class ConnectivityController extends StateController implements
@Override
public void dumpControllerStateLocked(IndentingPrintWriter pw,
Predicate<JobStatus> predicate) {
- pw.println("System connected: " + mConnected);
- pw.println();
-
for (int i = 0; i < mTrackedJobs.size(); i++) {
final JobStatus js = mTrackedJobs.valueAt(i);
if (predicate.test(js)) {
@@ -343,8 +354,6 @@ public final class ConnectivityController extends StateController implements
final long token = proto.start(fieldId);
final long mToken = proto.start(StateControllerProto.CONNECTIVITY);
- proto.write(StateControllerProto.ConnectivityController.IS_CONNECTED, mConnected);
-
for (int i = 0; i < mTrackedJobs.size(); i++) {
final JobStatus js = mTrackedJobs.valueAt(i);
if (!predicate.test(js)) {
diff --git a/com/android/server/job/controllers/StateController.java b/com/android/server/job/controllers/StateController.java
index 495109d8..c2be2833 100644
--- a/com/android/server/job/controllers/StateController.java
+++ b/com/android/server/job/controllers/StateController.java
@@ -41,7 +41,7 @@ public abstract class StateController {
StateController(JobSchedulerService service) {
mService = service;
mStateChangedListener = service;
- mContext = service.getContext();
+ mContext = service.getTestableContext();
mLock = service.getLock();
mConstants = service.getConstants();
}
diff --git a/com/android/server/location/ExponentialBackOff.java b/com/android/server/location/ExponentialBackOff.java
new file mode 100644
index 00000000..8c77b217
--- /dev/null
+++ b/com/android/server/location/ExponentialBackOff.java
@@ -0,0 +1,32 @@
+package com.android.server.location;
+
+/**
+ * A simple implementation of exponential backoff.
+ */
+class ExponentialBackOff {
+ private static final int MULTIPLIER = 2;
+ private final long mInitIntervalMillis;
+ private final long mMaxIntervalMillis;
+ private long mCurrentIntervalMillis;
+
+ ExponentialBackOff(long initIntervalMillis, long maxIntervalMillis) {
+ mInitIntervalMillis = initIntervalMillis;
+ mMaxIntervalMillis = maxIntervalMillis;
+
+ mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
+ }
+
+ long nextBackoffMillis() {
+ if (mCurrentIntervalMillis > mMaxIntervalMillis) {
+ return mMaxIntervalMillis;
+ }
+
+ mCurrentIntervalMillis *= MULTIPLIER;
+ return mCurrentIntervalMillis;
+ }
+
+ void reset() {
+ mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
+ }
+}
+
diff --git a/com/android/server/location/GnssLocationProvider.java b/com/android/server/location/GnssLocationProvider.java
index 5955c9c3..58bca196 100644
--- a/com/android/server/location/GnssLocationProvider.java
+++ b/com/android/server/location/GnssLocationProvider.java
@@ -76,14 +76,18 @@ import android.telephony.TelephonyManager;
import android.telephony.gsm.GsmCellLocation;
import android.text.TextUtils;
import android.util.Log;
-import android.util.NtpTrustedTime;
-import com.android.internal.app.IAppOpsService;
+
import com.android.internal.app.IBatteryStats;
import com.android.internal.location.GpsNetInitiatedHandler;
import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
import com.android.internal.location.ProviderProperties;
import com.android.internal.location.ProviderRequest;
import com.android.internal.location.gnssmetrics.GnssMetrics;
+import com.android.server.location.GnssSatelliteBlacklistHelper.GnssSatelliteBlacklistCallback;
+import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
+
+import libcore.io.IoUtils;
+
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
@@ -93,21 +97,19 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
-import libcore.io.IoUtils;
-
/**
* A GNSS implementation of LocationProvider used by LocationManager.
*
* {@hide}
*/
-public class GnssLocationProvider implements LocationProviderInterface {
+public class GnssLocationProvider implements LocationProviderInterface, InjectNtpTimeCallback,
+ GnssSatelliteBlacklistCallback {
private static final String TAG = "GnssLocationProvider";
@@ -208,7 +210,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
private static final int UPDATE_LOCATION = 7; // Handle external location from network listener
private static final int ADD_LISTENER = 8;
private static final int REMOVE_LISTENER = 9;
- private static final int INJECT_NTP_TIME_FINISHED = 10;
private static final int DOWNLOAD_XTRA_DATA_FINISHED = 11;
private static final int SUBSCRIPTION_OR_SIM_CHANGED = 12;
private static final int INITIALIZE_HANDLER = 13;
@@ -310,7 +311,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
}
- private Object mLock = new Object();
+ private final Object mLock = new Object();
// current status
private int mStatus = LocationProvider.TEMPORARILY_UNAVAILABLE;
@@ -329,9 +330,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
// Typical hot TTTF is ~5 seconds, so 10 seconds seems sane.
private static final int GPS_POLLING_THRESHOLD_INTERVAL = 10 * 1000;
- // how often to request NTP time, in milliseconds
- // current setting 24 hours
- private static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
// how long to wait if we have a network error in NTP or XTRA downloading
// the initial value of the exponential backoff
// current setting - 5 minutes
@@ -344,8 +342,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
// Timeout when holding wakelocks for downloading XTRA data.
private static final long DOWNLOAD_XTRA_DATA_TIMEOUT_MS = 60 * 1000;
- private BackOff mNtpBackOff = new BackOff(RETRY_INTERVAL, MAX_RETRY_INTERVAL);
- private BackOff mXtraBackOff = new BackOff(RETRY_INTERVAL, MAX_RETRY_INTERVAL);
+ private final ExponentialBackOff mXtraBackOff = new ExponentialBackOff(RETRY_INTERVAL,
+ MAX_RETRY_INTERVAL);
// true if we are enabled, protected by this
private boolean mEnabled;
@@ -357,12 +355,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
// flags to trigger NTP or XTRA data download when network becomes available
// initialized to true so we do NTP and XTRA when the network comes up after booting
- private int mInjectNtpTimePending = STATE_PENDING_NETWORK;
private int mDownloadXtraDataPending = STATE_PENDING_NETWORK;
- // set to true if the GPS engine requested on-demand NTP time requests
- private boolean mOnDemandTimeInjection;
-
// true if GPS is navigating
private boolean mNavigating;
@@ -417,14 +411,15 @@ public class GnssLocationProvider implements LocationProviderInterface {
private boolean mSuplEsEnabled = false;
private final Context mContext;
- private final NtpTrustedTime mNtpTime;
private final ILocationManager mILocationManager;
private final LocationExtras mLocationExtras = new LocationExtras();
private final GnssStatusListenerHelper mListenerHelper;
+ private final GnssSatelliteBlacklistHelper mGnssSatelliteBlacklistHelper;
private final GnssMeasurementsProvider mGnssMeasurementsProvider;
private final GnssNavigationMessageProvider mGnssNavigationMessageProvider;
private final LocationChangeListener mNetworkLocationListener = new NetworkLocationListener();
private final LocationChangeListener mFusedLocationListener = new FusedLocationListener();
+ private final NtpTimeHelper mNtpTimeHelper;
// Handler for processing events
private Handler mHandler;
@@ -517,9 +512,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
- if (mInjectNtpTimePending == STATE_PENDING_NETWORK) {
- requestUtcTime();
- }
+ mNtpTimeHelper.onNetworkAvailable();
if (mDownloadXtraDataPending == STATE_PENDING_NETWORK) {
if (mSupportsXtra) {
// Download only if supported, (prevents an unneccesary on-boot
@@ -588,6 +581,16 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
};
+ /**
+ * Implements {@link GnssSatelliteBlacklistCallback#onUpdateSatelliteBlacklist}.
+ */
+ @Override
+ public void onUpdateSatelliteBlacklist(int[] constellations, int[] svids) {
+ mHandler.post(()->{
+ native_set_satellite_blacklist(constellations, svids);
+ });
+ }
+
private void subscriptionOrSimChanged(Context context) {
if (DEBUG) Log.d(TAG, "received SIM related action: ");
TelephonyManager phone = (TelephonyManager)
@@ -762,7 +765,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
public GnssLocationProvider(Context context, ILocationManager ilocationManager,
Looper looper) {
mContext = context;
- mNtpTime = NtpTrustedTime.getInstance(context);
mILocationManager = ilocationManager;
// Create a wake lock
@@ -880,6 +882,11 @@ public class GnssLocationProvider implements LocationProviderInterface {
}
};
mGnssMetrics = new GnssMetrics(mBatteryStats);
+
+ mNtpTimeHelper = new NtpTimeHelper(mContext, looper, this);
+ mGnssSatelliteBlacklistHelper = new GnssSatelliteBlacklistHelper(mContext,
+ looper, this);
+ mHandler.post(mGnssSatelliteBlacklistHelper::updateSatelliteBlacklist);
}
/**
@@ -895,6 +902,15 @@ public class GnssLocationProvider implements LocationProviderInterface {
return PROPERTIES;
}
+
+ /**
+ * Implements {@link InjectNtpTimeCallback#injectTime}
+ */
+ @Override
+ public void injectTime(long time, long timeReference, int uncertainty) {
+ native_inject_time(time, timeReference, uncertainty);
+ }
+
private void handleUpdateNetworkState(Network network) {
// retrieve NetworkInfo for this UID
NetworkInfo info = mConnMgr.getNetworkInfo(network);
@@ -1014,79 +1030,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
Log.e(TAG, "Invalid status to release SUPL connection: " + agpsDataConnStatus);
}
}
-
- private void handleInjectNtpTime() {
- if (mInjectNtpTimePending == STATE_DOWNLOADING) {
- // already downloading data
- return;
- }
- if (!isDataNetworkConnected()) {
- // try again when network is up
- mInjectNtpTimePending = STATE_PENDING_NETWORK;
- return;
- }
- mInjectNtpTimePending = STATE_DOWNLOADING;
-
- // hold wake lock while task runs
- mWakeLock.acquire();
- Log.i(TAG, "WakeLock acquired by handleInjectNtpTime()");
- AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
- @Override
- public void run() {
- long delay;
-
- // force refresh NTP cache when outdated
- boolean refreshSuccess = true;
- if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
- refreshSuccess = mNtpTime.forceRefresh();
- }
-
- // only update when NTP time is fresh
- if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
- long time = mNtpTime.getCachedNtpTime();
- long timeReference = mNtpTime.getCachedNtpTimeReference();
- long certainty = mNtpTime.getCacheCertainty();
-
- if (DEBUG) {
- long now = System.currentTimeMillis();
- Log.d(TAG, "NTP server returned: "
- + time + " (" + new Date(time)
- + ") reference: " + timeReference
- + " certainty: " + certainty
- + " system time offset: " + (time - now));
- }
-
- native_inject_time(time, timeReference, (int) certainty);
- delay = NTP_INTERVAL;
- mNtpBackOff.reset();
- } else {
- Log.e(TAG, "requestTime failed");
- delay = mNtpBackOff.nextBackoffMillis();
- }
-
- sendMessage(INJECT_NTP_TIME_FINISHED, 0, null);
-
- if (DEBUG) {
- String message = String.format(
- "onDemandTimeInjection=%s, refreshSuccess=%s, delay=%s",
- mOnDemandTimeInjection,
- refreshSuccess,
- delay);
- Log.d(TAG, message);
- }
- if (mOnDemandTimeInjection || !refreshSuccess) {
- // send delayed message for next NTP injection
- // since this is delayed and not urgent we do not hold a wake lock here
- mHandler.sendEmptyMessageDelayed(INJECT_NTP_TIME, delay);
- }
-
- // release wake lock held by task
- mWakeLock.release();
- Log.i(TAG, "WakeLock released by handleInjectNtpTime()");
- }
- });
- }
-
+
private void handleRequestLocation(boolean independentFromGnss) {
if (isRequestLocationRateLimited()) {
if (DEBUG) {
@@ -2006,7 +1950,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
mEngineCapabilities = capabilities;
if (hasCapability(GPS_CAPABILITY_ON_DEMAND_TIME)) {
- mOnDemandTimeInjection = true;
+ mNtpTimeHelper.enablePeriodicTimeInjection();
requestUtcTime();
}
@@ -2467,7 +2411,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
handleReleaseSuplConnection(msg.arg1);
break;
case INJECT_NTP_TIME:
- handleInjectNtpTime();
+ mNtpTimeHelper.retrieveAndInjectNtpTime();
break;
case REQUEST_LOCATION:
handleRequestLocation((boolean) msg.obj);
@@ -2475,9 +2419,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
case DOWNLOAD_XTRA_DATA:
handleDownloadXtraData();
break;
- case INJECT_NTP_TIME_FINISHED:
- mInjectNtpTimePending = STATE_IDLE;
- break;
case DOWNLOAD_XTRA_DATA_FINISHED:
mDownloadXtraDataPending = STATE_IDLE;
break;
@@ -2808,8 +2749,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
return "REQUEST_LOCATION";
case DOWNLOAD_XTRA_DATA:
return "DOWNLOAD_XTRA_DATA";
- case INJECT_NTP_TIME_FINISHED:
- return "INJECT_NTP_TIME_FINISHED";
case DOWNLOAD_XTRA_DATA_FINISHED:
return "DOWNLOAD_XTRA_DATA_FINISHED";
case UPDATE_LOCATION:
@@ -2856,36 +2795,6 @@ public class GnssLocationProvider implements LocationProviderInterface {
pw.append(s);
}
- /**
- * A simple implementation of exponential backoff.
- */
- private static final class BackOff {
- private static final int MULTIPLIER = 2;
- private final long mInitIntervalMillis;
- private final long mMaxIntervalMillis;
- private long mCurrentIntervalMillis;
-
- public BackOff(long initIntervalMillis, long maxIntervalMillis) {
- mInitIntervalMillis = initIntervalMillis;
- mMaxIntervalMillis = maxIntervalMillis;
-
- mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
- }
-
- public long nextBackoffMillis() {
- if (mCurrentIntervalMillis > mMaxIntervalMillis) {
- return mMaxIntervalMillis;
- }
-
- mCurrentIntervalMillis *= MULTIPLIER;
- return mCurrentIntervalMillis;
- }
-
- public void reset() {
- mCurrentIntervalMillis = mInitIntervalMillis / MULTIPLIER;
- }
- }
-
// preallocated to avoid memory allocation in reportNmea()
private byte[] mNmeaBuffer = new byte[120];
@@ -3008,6 +2917,8 @@ public class GnssLocationProvider implements LocationProviderInterface {
private static native boolean native_set_emergency_supl_pdn(int emergencySuplPdn);
+ private static native boolean native_set_satellite_blacklist(int[] constellations, int[] svIds);
+
// GNSS Batching
private static native int native_get_batch_size();
@@ -3022,3 +2933,4 @@ public class GnssLocationProvider implements LocationProviderInterface {
private static native void native_cleanup_batching();
}
+
diff --git a/com/android/server/location/GnssSatelliteBlacklistHelper.java b/com/android/server/location/GnssSatelliteBlacklistHelper.java
new file mode 100644
index 00000000..77951aa7
--- /dev/null
+++ b/com/android/server/location/GnssSatelliteBlacklistHelper.java
@@ -0,0 +1,102 @@
+package com.android.server.location;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Detects blacklist change and updates the blacklist.
+ */
+class GnssSatelliteBlacklistHelper {
+
+ private static final String TAG = "GnssBlacklistHelper";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final String BLACKLIST_DELIMITER = ",";
+
+ private final Context mContext;
+ private final GnssSatelliteBlacklistCallback mCallback;
+
+ interface GnssSatelliteBlacklistCallback {
+ void onUpdateSatelliteBlacklist(int[] constellations, int[] svids);
+ }
+
+ GnssSatelliteBlacklistHelper(Context context, Looper looper,
+ GnssSatelliteBlacklistCallback callback) {
+ mContext = context;
+ mCallback = callback;
+ ContentObserver contentObserver = new ContentObserver(new Handler(looper)) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updateSatelliteBlacklist();
+ }
+ };
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(
+ Settings.Global.GNSS_SATELLITE_BLACKLIST),
+ true,
+ contentObserver, UserHandle.USER_ALL);
+ }
+
+ void updateSatelliteBlacklist() {
+ ContentResolver resolver = mContext.getContentResolver();
+ String blacklist = Settings.Global.getString(
+ resolver,
+ Settings.Global.GNSS_SATELLITE_BLACKLIST);
+ if (blacklist == null) {
+ blacklist = "";
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("Update GNSS satellite blacklist: %s", blacklist));
+ }
+
+ List<Integer> blacklistValues;
+ try {
+ blacklistValues = parseSatelliteBlacklist(blacklist);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Exception thrown when parsing blacklist string.", e);
+ return;
+ }
+
+ if (blacklistValues.size() % 2 != 0) {
+ Log.e(TAG, "blacklist string has odd number of values."
+ + "Aborting updateSatelliteBlacklist");
+ return;
+ }
+
+ int length = blacklistValues.size() / 2;
+ int[] constellations = new int[length];
+ int[] svids = new int[length];
+ for (int i = 0; i < length; i++) {
+ constellations[i] = blacklistValues.get(i * 2);
+ svids[i] = blacklistValues.get(i * 2 + 1);
+ }
+ mCallback.onUpdateSatelliteBlacklist(constellations, svids);
+ }
+
+ @VisibleForTesting
+ static List<Integer> parseSatelliteBlacklist(String blacklist) throws NumberFormatException {
+ String[] strings = blacklist.split(BLACKLIST_DELIMITER);
+ List<Integer> parsed = new ArrayList<>(strings.length);
+ for (String string : strings) {
+ string = string.trim();
+ if (!"".equals(string)) {
+ int value = Integer.parseInt(string);
+ if (value < 0) {
+ throw new NumberFormatException("Negative value is invalid.");
+ }
+ parsed.add(value);
+ }
+ }
+ return parsed;
+ }
+}
diff --git a/com/android/server/location/GnssSatelliteBlacklistHelperTest.java b/com/android/server/location/GnssSatelliteBlacklistHelperTest.java
new file mode 100644
index 00000000..d6f54460
--- /dev/null
+++ b/com/android/server/location/GnssSatelliteBlacklistHelperTest.java
@@ -0,0 +1,130 @@
+package com.android.server.location;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Unit tests for {@link GnssSatelliteBlacklistHelper}.
+ */
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+ manifest = Config.NONE,
+ shadows = {
+ },
+ sdk = 27
+)
+@SystemLoaderPackages({"com.android.server.location"})
+@Presubmit
+public class GnssSatelliteBlacklistHelperTest {
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ @Mock
+ private GnssSatelliteBlacklistHelper.GnssSatelliteBlacklistCallback mCallback;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mContentResolver = mContext.getContentResolver();
+ new GnssSatelliteBlacklistHelper(mContext, Looper.myLooper(), mCallback);
+ }
+
+ @Test
+ public void blacklistOf2Satellites_callbackIsCalled() {
+ String blacklist = "3,0,5,24";
+ updateBlacklistAndVerifyCallbackIsCalled(blacklist);
+ }
+
+ @Test
+ public void blacklistWithSpaces_callbackIsCalled() {
+ String blacklist = "3, 11";
+ updateBlacklistAndVerifyCallbackIsCalled(blacklist);
+ }
+
+ @Test
+ public void emptyBlacklist_callbackIsCalled() {
+ String blacklist = "";
+ updateBlacklistAndVerifyCallbackIsCalled(blacklist);
+ }
+
+ @Test
+ public void blacklistWithOddNumberOfValues_callbackIsNotCalled() {
+ String blacklist = "3,0,5";
+ updateBlacklistAndNotifyContentObserver(blacklist);
+ verify(mCallback, never()).onUpdateSatelliteBlacklist(any(int[].class), any(int[].class));
+ }
+
+ @Test
+ public void blacklistWithNegativeValue_callbackIsNotCalled() {
+ String blacklist = "3,-11";
+ updateBlacklistAndNotifyContentObserver(blacklist);
+ verify(mCallback, never()).onUpdateSatelliteBlacklist(any(int[].class), any(int[].class));
+ }
+
+ @Test
+ public void blacklistWithNonDigitCharacter_callbackIsNotCalled() {
+ String blacklist = "3,1a,5,11";
+ updateBlacklistAndNotifyContentObserver(blacklist);
+ verify(mCallback, never()).onUpdateSatelliteBlacklist(any(int[].class), any(int[].class));
+ }
+
+ private void updateBlacklistAndNotifyContentObserver(String blacklist) {
+ Settings.Global.putString(mContentResolver,
+ Settings.Global.GNSS_SATELLITE_BLACKLIST, blacklist);
+ notifyContentObserverFor(Settings.Global.GNSS_SATELLITE_BLACKLIST);
+ }
+
+ private void updateBlacklistAndVerifyCallbackIsCalled(String blacklist) {
+ updateBlacklistAndNotifyContentObserver(blacklist);
+
+ ArgumentCaptor<int[]> constellationsCaptor = ArgumentCaptor.forClass(int[].class);
+ ArgumentCaptor<int[]> svIdsCaptor = ArgumentCaptor.forClass(int[].class);
+ verify(mCallback).onUpdateSatelliteBlacklist(constellationsCaptor.capture(),
+ svIdsCaptor.capture());
+
+ int[] constellations = constellationsCaptor.getValue();
+ int[] svIds = svIdsCaptor.getValue();
+ List<Integer> values = GnssSatelliteBlacklistHelper.parseSatelliteBlacklist(blacklist);
+ assertThat(values.size()).isEqualTo(constellations.length * 2);
+ assertThat(svIds.length).isEqualTo(constellations.length);
+ for (int i = 0; i < constellations.length; i++) {
+ assertThat(constellations[i]).isEqualTo(values.get(i * 2));
+ assertThat(svIds[i]).isEqualTo(values.get(i * 2 + 1));
+ }
+ }
+
+ private static void notifyContentObserverFor(String globalSetting) {
+ Collection<ContentObserver> contentObservers =
+ Shadows.shadowOf(RuntimeEnvironment.application.getContentResolver())
+ .getContentObservers(Settings.Global.getUriFor(globalSetting));
+ assertThat(contentObservers).isNotEmpty();
+ contentObservers.iterator().next().onChange(false /* selfChange */);
+ }
+}
diff --git a/com/android/server/location/NtpTimeHelper.java b/com/android/server/location/NtpTimeHelper.java
new file mode 100644
index 00000000..296b500e
--- /dev/null
+++ b/com/android/server/location/NtpTimeHelper.java
@@ -0,0 +1,191 @@
+package com.android.server.location;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.NtpTrustedTime;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Date;
+
+/**
+ * Handles inject NTP time to GNSS.
+ *
+ * <p>The client is responsible to call {@link #onNetworkAvailable()} when network is available
+ * for retrieving NTP Time.
+ */
+class NtpTimeHelper {
+
+ private static final String TAG = "NtpTimeHelper";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // states for injecting ntp
+ private static final int STATE_PENDING_NETWORK = 0;
+ private static final int STATE_RETRIEVING_AND_INJECTING = 1;
+ private static final int STATE_IDLE = 2;
+
+ // how often to request NTP time, in milliseconds
+ // current setting 24 hours
+ @VisibleForTesting
+ static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
+
+ // how long to wait if we have a network error in NTP
+ // the initial value of the exponential backoff
+ // current setting - 5 minutes
+ @VisibleForTesting
+ static final long RETRY_INTERVAL = 5 * 60 * 1000;
+ // how long to wait if we have a network error in NTP
+ // the max value of the exponential backoff
+ // current setting - 4 hours
+ private static final long MAX_RETRY_INTERVAL = 4 * 60 * 60 * 1000;
+
+ private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
+ private static final String WAKELOCK_KEY = "NtpTimeHelper";
+
+ private final ExponentialBackOff mNtpBackOff = new ExponentialBackOff(RETRY_INTERVAL,
+ MAX_RETRY_INTERVAL);
+
+ private final ConnectivityManager mConnMgr;
+ private final NtpTrustedTime mNtpTime;
+ private final WakeLock mWakeLock;
+ private final Handler mHandler;
+
+ @GuardedBy("this")
+ private final InjectNtpTimeCallback mCallback;
+
+ // flags to trigger NTP when network becomes available
+ // initialized to STATE_PENDING_NETWORK so we do NTP when the network comes up after booting
+ @GuardedBy("this")
+ private int mInjectNtpTimeState = STATE_PENDING_NETWORK;
+
+ // set to true if the GPS engine requested on-demand NTP time requests
+ @GuardedBy("this")
+ private boolean mOnDemandTimeInjection;
+
+ interface InjectNtpTimeCallback {
+ void injectTime(long time, long timeReference, int uncertainty);
+ }
+
+ @VisibleForTesting
+ NtpTimeHelper(Context context, Looper looper, InjectNtpTimeCallback callback,
+ NtpTrustedTime ntpTime) {
+ mConnMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCallback = callback;
+ mNtpTime = ntpTime;
+ mHandler = new Handler(looper);
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
+ }
+
+ NtpTimeHelper(Context context, Looper looper, InjectNtpTimeCallback callback) {
+ this(context, looper, callback, NtpTrustedTime.getInstance(context));
+ }
+
+ synchronized void enablePeriodicTimeInjection() {
+ mOnDemandTimeInjection = true;
+ }
+
+ synchronized void onNetworkAvailable() {
+ if (mInjectNtpTimeState == STATE_PENDING_NETWORK) {
+ retrieveAndInjectNtpTime();
+ }
+ }
+
+ /**
+ * @return {@code true} if there is a network available for outgoing connections,
+ * {@code false} otherwise.
+ */
+ private boolean isNetworkConnected() {
+ NetworkInfo activeNetworkInfo = mConnMgr.getActiveNetworkInfo();
+ return activeNetworkInfo != null && activeNetworkInfo.isConnected();
+ }
+
+ synchronized void retrieveAndInjectNtpTime() {
+ if (mInjectNtpTimeState == STATE_RETRIEVING_AND_INJECTING) {
+ // already downloading data
+ return;
+ }
+ if (!isNetworkConnected()) {
+ // try again when network is up
+ mInjectNtpTimeState = STATE_PENDING_NETWORK;
+ return;
+ }
+ mInjectNtpTimeState = STATE_RETRIEVING_AND_INJECTING;
+
+ // hold wake lock while task runs
+ mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
+ new Thread(this::blockingGetNtpTimeAndInject).start();
+ }
+
+ /** {@link NtpTrustedTime#forceRefresh} is a blocking network operation. */
+ private void blockingGetNtpTimeAndInject() {
+ long delay;
+
+ // force refresh NTP cache when outdated
+ boolean refreshSuccess = true;
+ if (mNtpTime.getCacheAge() >= NTP_INTERVAL) {
+ // Blocking network operation.
+ refreshSuccess = mNtpTime.forceRefresh();
+ }
+
+ synchronized (this) {
+ mInjectNtpTimeState = STATE_IDLE;
+
+ // only update when NTP time is fresh
+ // If refreshSuccess is false, cacheAge does not drop down.
+ if (mNtpTime.getCacheAge() < NTP_INTERVAL) {
+ long time = mNtpTime.getCachedNtpTime();
+ long timeReference = mNtpTime.getCachedNtpTimeReference();
+ long certainty = mNtpTime.getCacheCertainty();
+
+ if (DEBUG) {
+ long now = System.currentTimeMillis();
+ Log.d(TAG, "NTP server returned: "
+ + time + " (" + new Date(time)
+ + ") reference: " + timeReference
+ + " certainty: " + certainty
+ + " system time offset: " + (time - now));
+ }
+
+ // Ok to cast to int, as can't rollover in practice
+ mHandler.post(() -> mCallback.injectTime(time, timeReference, (int) certainty));
+
+ delay = NTP_INTERVAL;
+ mNtpBackOff.reset();
+ } else {
+ Log.e(TAG, "requestTime failed");
+ delay = mNtpBackOff.nextBackoffMillis();
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "onDemandTimeInjection=%s, refreshSuccess=%s, delay=%s",
+ mOnDemandTimeInjection,
+ refreshSuccess,
+ delay));
+ }
+ // TODO(b/73893222): reconcile Capabilities bit 'on demand' name vs. de facto periodic
+ // injection.
+ if (mOnDemandTimeInjection || !refreshSuccess) {
+ /* Schedule next NTP injection.
+ * Since this is delayed, the wake lock is released right away, and will be held
+ * again when the delayed task runs.
+ */
+ mHandler.postDelayed(this::retrieveAndInjectNtpTime, delay);
+ }
+ }
+ try {
+ // release wake lock held by task
+ mWakeLock.release();
+ } catch (Exception e) {
+ // This happens when the WakeLock is already released.
+ }
+ }
+}
diff --git a/com/android/server/location/NtpTimeHelperTest.java b/com/android/server/location/NtpTimeHelperTest.java
new file mode 100644
index 00000000..a68b5795
--- /dev/null
+++ b/com/android/server/location/NtpTimeHelperTest.java
@@ -0,0 +1,100 @@
+package com.android.server.location;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.util.NtpTrustedTime;
+
+import com.android.server.location.NtpTimeHelper.InjectNtpTimeCallback;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowSystemClock;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit tests for {@link NtpTimeHelper}.
+ */
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+ manifest = Config.NONE,
+ sdk = 27
+)
+@SystemLoaderPackages({"com.android.server.location"})
+@Presubmit
+public class NtpTimeHelperTest {
+
+ private static final long MOCK_NTP_TIME = 1519930775453L;
+ @Mock
+ private NtpTrustedTime mMockNtpTrustedTime;
+ private NtpTimeHelper mNtpTimeHelper;
+ private CountDownLatch mCountDownLatch;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mCountDownLatch = new CountDownLatch(1);
+ InjectNtpTimeCallback callback =
+ (time, timeReference, uncertainty) -> {
+ assertThat(time).isEqualTo(MOCK_NTP_TIME);
+ mCountDownLatch.countDown();
+ };
+ mNtpTimeHelper = new NtpTimeHelper(RuntimeEnvironment.application,
+ Looper.myLooper(),
+ callback, mMockNtpTrustedTime);
+ }
+
+ @Test
+ public void handleInjectNtpTime_cachedAgeLow_injectTime() throws InterruptedException {
+ doReturn(NtpTimeHelper.NTP_INTERVAL - 1).when(mMockNtpTrustedTime).getCacheAge();
+ doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime();
+
+ mNtpTimeHelper.retrieveAndInjectNtpTime();
+
+ waitForTasksToBePostedOnHandlerAndRunThem();
+ assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ }
+
+ @Test
+ public void handleInjectNtpTime_injectTimeFailed_injectTimeDelayed()
+ throws InterruptedException {
+ doReturn(NtpTimeHelper.NTP_INTERVAL + 1).when(mMockNtpTrustedTime).getCacheAge();
+ doReturn(false).when(mMockNtpTrustedTime).forceRefresh();
+
+ mNtpTimeHelper.retrieveAndInjectNtpTime();
+ waitForTasksToBePostedOnHandlerAndRunThem();
+ assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isFalse();
+
+ doReturn(true).when(mMockNtpTrustedTime).forceRefresh();
+ doReturn(1L).when(mMockNtpTrustedTime).getCacheAge();
+ doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime();
+ ShadowSystemClock.sleep(NtpTimeHelper.RETRY_INTERVAL);
+
+ waitForTasksToBePostedOnHandlerAndRunThem();
+ assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ }
+
+ /**
+ * Since a thread is created in {@link NtpTimeHelper#retrieveAndInjectNtpTime} and the task to
+ * be verified is posted in the thread, we have to wait for the task to be posted and then it
+ * can be run.
+ */
+ private void waitForTasksToBePostedOnHandlerAndRunThem() throws InterruptedException {
+ mCountDownLatch.await(1, TimeUnit.SECONDS);
+ ShadowLooper.runUiThreadTasks();
+ }
+}
+
diff --git a/com/android/server/locksettings/SyntheticPasswordManager.java b/com/android/server/locksettings/SyntheticPasswordManager.java
index 88b2a368..0700ab35 100644
--- a/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -534,17 +534,33 @@ public class SyntheticPasswordManager {
private void destroyWeaverSlot(long handle, int userId) {
int slot = loadWeaverSlot(handle, userId);
+ destroyState(WEAVER_SLOT_NAME, handle, userId);
if (slot != INVALID_WEAVER_SLOT) {
- try {
- weaverEnroll(slot, null, null);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed to destroy slot", e);
+ Set<Integer> usedSlots = getUsedWeaverSlots();
+ if (!usedSlots.contains(slot)) {
+ Log.i(TAG, "Destroy weaver slot " + slot + " for user " + userId);
+ try {
+ weaverEnroll(slot, null, null);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to destroy slot", e);
+ }
+ } else {
+ Log.w(TAG, "Skip destroying reused weaver slot " + slot + " for user " + userId);
}
}
- destroyState(WEAVER_SLOT_NAME, handle, userId);
}
- private int getNextAvailableWeaverSlot() {
+ /**
+ * Return the set of weaver slots that are currently in use by all users on the device.
+ * <p>
+ * <em>Note:</em> Users who are in the process of being deleted are not tracked here
+ * (due to them being marked as partial in UserManager so not visible from
+ * {@link UserManager#getUsers}). As a result their weaver slots will not be considered
+ * taken and can be reused by new users. Care should be taken when cleaning up the
+ * deleted user in {@link #removeUser}, to prevent a reused slot from being erased
+ * unintentionally.
+ */
+ private Set<Integer> getUsedWeaverSlots() {
Map<Integer, List<Long>> slotHandles = mStorage.listSyntheticPasswordHandlesForAllUsers(
WEAVER_SLOT_NAME);
HashSet<Integer> slots = new HashSet<>();
@@ -554,8 +570,13 @@ public class SyntheticPasswordManager {
slots.add(slot);
}
}
+ return slots;
+ }
+
+ private int getNextAvailableWeaverSlot() {
+ Set<Integer> usedSlots = getUsedWeaverSlots();
for (int i = 0; i < mWeaverConfig.slots; i++) {
- if (!slots.contains(i)) {
+ if (!usedSlots.contains(i)) {
return i;
}
}
@@ -592,6 +613,7 @@ public class SyntheticPasswordManager {
if (isWeaverAvailable()) {
// Weaver based user password
int weaverSlot = getNextAvailableWeaverSlot();
+ Log.i(TAG, "Weaver enroll password to slot " + weaverSlot + " for user " + userId);
byte[] weaverSecret = weaverEnroll(weaverSlot, passwordTokenToWeaverKey(pwdToken), null);
if (weaverSecret == null) {
Log.e(TAG, "Fail to enroll user password under weaver " + userId);
@@ -749,6 +771,7 @@ public class SyntheticPasswordManager {
if (isWeaverAvailable()) {
int slot = getNextAvailableWeaverSlot();
try {
+ Log.i(TAG, "Weaver enroll token to slot " + slot + " for user " + userId);
weaverEnroll(slot, null, tokenData.weaverSecret);
} catch (RemoteException e) {
Log.e(TAG, "Failed to enroll weaver secret when activating token", e);
diff --git a/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
index 4c4176a4..567eaaa5 100644
--- a/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
+++ b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -20,6 +20,7 @@ import static android.security.keystore.recovery.KeyChainProtectionParams.TYPE_L
import android.annotation.Nullable;
import android.content.Context;
+import android.security.Scrypt;
import android.security.keystore.recovery.KeyChainProtectionParams;
import android.security.keystore.recovery.KeyChainSnapshot;
import android.security.keystore.recovery.KeyDerivationParams;
@@ -69,6 +70,17 @@ public class KeySyncTask implements Runnable {
private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10;
+ // TODO: Reduce the minimal length once all other components are updated
+ private static final int MIN_CREDENTIAL_LEN_TO_USE_SCRYPT = 24;
+ @VisibleForTesting
+ static final int SCRYPT_PARAM_N = 4096;
+ @VisibleForTesting
+ static final int SCRYPT_PARAM_R = 8;
+ @VisibleForTesting
+ static final int SCRYPT_PARAM_P = 1;
+ @VisibleForTesting
+ static final int SCRYPT_PARAM_OUTLEN_BYTES = 32;
+
private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
private final int mUserId;
private final int mCredentialType;
@@ -78,6 +90,7 @@ public class KeySyncTask implements Runnable {
private final RecoverySnapshotStorage mRecoverySnapshotStorage;
private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
+ private final Scrypt mScrypt;
public static KeySyncTask newInstance(
Context context,
@@ -98,7 +111,8 @@ public class KeySyncTask implements Runnable {
credential,
credentialUpdated,
PlatformKeyManager.getInstance(context, recoverableKeyStoreDb),
- new TestOnlyInsecureCertificateHelper());
+ new TestOnlyInsecureCertificateHelper(),
+ new Scrypt());
}
/**
@@ -110,7 +124,7 @@ public class KeySyncTask implements Runnable {
* @param credential The credential, encoded as a {@link String}.
* @param credentialUpdated signals weather credentials were updated.
* @param platformKeyManager platform key manager
- * @param TestOnlyInsecureCertificateHelper utility class used for end-to-end tests
+ * @param testOnlyInsecureCertificateHelper utility class used for end-to-end tests
*/
@VisibleForTesting
KeySyncTask(
@@ -122,7 +136,8 @@ public class KeySyncTask implements Runnable {
String credential,
boolean credentialUpdated,
PlatformKeyManager platformKeyManager,
- TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
+ TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
+ Scrypt scrypt) {
mSnapshotListenersStorage = recoverySnapshotListenersStorage;
mRecoverableKeyStoreDb = recoverableKeyStoreDb;
mUserId = userId;
@@ -131,7 +146,8 @@ public class KeySyncTask implements Runnable {
mCredentialUpdated = credentialUpdated;
mPlatformKeyManager = platformKeyManager;
mRecoverySnapshotStorage = snapshotStorage;
- mTestOnlyInsecureCertificateHelper = TestOnlyInsecureCertificateHelper;
+ mTestOnlyInsecureCertificateHelper = testOnlyInsecureCertificateHelper;
+ mScrypt = scrypt;
}
@Override
@@ -230,8 +246,14 @@ public class KeySyncTask implements Runnable {
}
}
+ boolean useScryptToHashCredential = shouldUseScryptToHashCredential(rootCertAlias);
byte[] salt = generateSalt();
- byte[] localLskfHash = hashCredentials(salt, mCredential);
+ byte[] localLskfHash;
+ if (useScryptToHashCredential) {
+ localLskfHash = hashCredentialsByScrypt(salt, mCredential);
+ } else {
+ localLskfHash = hashCredentialsBySaltedSha256(salt, mCredential);
+ }
Map<String, SecretKey> rawKeys;
try {
@@ -303,10 +325,17 @@ public class KeySyncTask implements Runnable {
Log.e(TAG,"Could not encrypt with recovery key", e);
return;
}
+ KeyDerivationParams keyDerivationParams;
+ if (useScryptToHashCredential) {
+ keyDerivationParams = KeyDerivationParams.createScryptParams(
+ salt, /*memoryDifficulty=*/ SCRYPT_PARAM_N);
+ } else {
+ keyDerivationParams = KeyDerivationParams.createSha256Params(salt);
+ }
KeyChainProtectionParams metadata = new KeyChainProtectionParams.Builder()
.setUserSecretType(TYPE_LOCKSCREEN)
.setLockScreenUiFormat(getUiFormat(mCredentialType, mCredential))
- .setKeyDerivationParams(KeyDerivationParams.createSha256Params(salt))
+ .setKeyDerivationParams(keyDerivationParams)
.setSecret(new byte[0])
.build();
@@ -443,7 +472,7 @@ public class KeySyncTask implements Runnable {
* @return The SHA-256 hash.
*/
@VisibleForTesting
- static byte[] hashCredentials(byte[] salt, String credentials) {
+ static byte[] hashCredentialsBySaltedSha256(byte[] salt, String credentials) {
byte[] credentialsBytes = credentials.getBytes(StandardCharsets.UTF_8);
ByteBuffer byteBuffer = ByteBuffer.allocate(
salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
@@ -462,6 +491,12 @@ public class KeySyncTask implements Runnable {
}
}
+ private byte[] hashCredentialsByScrypt(byte[] salt, String credentials) {
+ return mScrypt.scrypt(
+ credentials.getBytes(StandardCharsets.UTF_8), salt,
+ SCRYPT_PARAM_N, SCRYPT_PARAM_R, SCRYPT_PARAM_P, SCRYPT_PARAM_OUTLEN_BYTES);
+ }
+
private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
@@ -479,4 +514,11 @@ public class KeySyncTask implements Runnable {
}
return keyEntries;
}
+
+ private boolean shouldUseScryptToHashCredential(String rootCertAlias) {
+ return mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
+ && mCredential.length() >= MIN_CREDENTIAL_LEN_TO_USE_SCRYPT
+ // TODO: Remove the test cert check once all other components are updated
+ && mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias);
+ }
}
diff --git a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
index e5ff5b83..52394d2a 100644
--- a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
+++ b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
@@ -375,6 +375,8 @@ public class PlatformKeyManager {
throws NoSuchAlgorithmException, KeyStoreException {
String encryptAlias = getEncryptAlias(userId, generationId);
String decryptAlias = getDecryptAlias(userId, generationId);
+ // SecretKey implementation doesn't provide reliable way to destroy the secret
+ // so it may live in memory for some time.
SecretKey secretKey = generateAesKey();
// Store decryption key first since it is more likely to fail.
@@ -398,8 +400,6 @@ public class PlatformKeyManager {
.build());
setGenerationId(userId, generationId);
-
- // TODO: Use a reliable way to destroy the temporary secretKey in memory.
}
/**
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
index 7ebe8bf2..396862dc 100644
--- a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
@@ -29,7 +29,6 @@ import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
-// TODO: Rename RecoverableKeyGenerator to RecoverableKeyManager as it can import a key too now
/**
* Generates/imports keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
*
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index c09d7259..c484251c 100644
--- a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -18,6 +18,7 @@ package com.android.server.locksettings.recoverablekeystore;
import static android.security.keystore.recovery.RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT;
import static android.security.keystore.recovery.RecoveryController.ERROR_DECRYPTION_FAILED;
+import static android.security.keystore.recovery.RecoveryController.ERROR_DOWNGRADE_CERTIFICATE;
import static android.security.keystore.recovery.RecoveryController.ERROR_INSECURE_USER;
import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_KEY_FORMAT;
import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE;
@@ -46,12 +47,12 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.internal.util.Preconditions;
-import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
-import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
-import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException;
+import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
import com.android.server.locksettings.recoverablekeystore.certificate.CertValidationException;
import com.android.server.locksettings.recoverablekeystore.certificate.CertXml;
+import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
+import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
@@ -212,6 +213,8 @@ public class RecoverableKeyStoreManager {
Log.i(TAG, "The cert file serial number is the same, so skip updating.");
} else {
Log.e(TAG, "The cert file serial number is older than the one in database.");
+ throw new ServiceSpecificException(ERROR_DOWNGRADE_CERTIFICATE,
+ "The cert file serial number is older than the one in database.");
}
return;
}
@@ -295,20 +298,6 @@ public class RecoverableKeyStoreManager {
initRecoveryService(rootCertificateAlias, recoveryServiceCertFile);
}
- private PublicKey parseEcPublicKey(@NonNull byte[] bytes) throws ServiceSpecificException {
- try {
- KeyFactory kf = KeyFactory.getInstance("EC");
- X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(bytes);
- return kf.generatePublic(pkSpec);
- } catch (NoSuchAlgorithmException e) {
- Log.wtf(TAG, "EC algorithm not available. AOSP must support this.", e);
- throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
- } catch (InvalidKeySpecException e) {
- throw new ServiceSpecificException(
- ERROR_BAD_CERTIFICATE_FORMAT, "Not a valid X509 certificate.");
- }
- }
-
/**
* Gets all data necessary to recover application keys on new device.
*
@@ -747,8 +736,6 @@ public class RecoverableKeyStoreManager {
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
- // TODO: Refactor RecoverableKeyGenerator to wrap the PlatformKey logic
-
PlatformEncryptionKey encryptionKey;
try {
encryptionKey = mPlatformKeyManager.getEncryptKey(userId);
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
index 53c972fb..7c4360e5 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -291,7 +291,7 @@ public class RecoverableKeyStoreDb {
}
/**
- * Sets the {@code generationId} of the platform key for the account owned by {@code userId}.
+ * Sets the {@code generationId} of the platform key for user {@code userId}.
*
* @return The primary key ID of the relation.
*/
@@ -630,7 +630,6 @@ public class RecoverableKeyStoreDb {
* @hide
*/
public long setActiveRootOfTrust(int userId, int uid, @Nullable String rootAlias) {
- // TODO: Call getDefaultCertificateAliasIfEmpty() here too?
SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_ACTIVE_ROOT_OF_TRUST, rootAlias);
diff --git a/com/android/server/media/MediaSessionService.java b/com/android/server/media/MediaSessionService.java
index 6413ba98..a3c6c80c 100644
--- a/com/android/server/media/MediaSessionService.java
+++ b/com/android/server/media/MediaSessionService.java
@@ -1515,6 +1515,24 @@ public class MediaSessionService extends SystemService implements Monitor {
final int uid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
try {
+ int controllerUserId = UserHandle.getUserId(controllerUid);
+ int controllerUidFromPackageName;
+ try {
+ controllerUidFromPackageName = getContext().getPackageManager()
+ .getPackageUidAsUser(controllerPackageName, controllerUserId);
+ } catch (NameNotFoundException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Package " + controllerPackageName + " doesn't exist");
+ }
+ return false;
+ }
+ if (controllerUidFromPackageName != controllerUid) {
+ if (DEBUG) {
+ Log.d(TAG, "Package name " + controllerPackageName
+ + " doesn't match with the uid " + controllerUid);
+ }
+ return false;
+ }
return hasMediaControlPermission(UserHandle.getUserId(uid), controllerPackageName,
controllerPid, controllerUid);
} finally {
diff --git a/com/android/server/net/NetworkPolicyManagerService.java b/com/android/server/net/NetworkPolicyManagerService.java
index 29d2e557..a85960a1 100644
--- a/com/android/server/net/NetworkPolicyManagerService.java
+++ b/com/android/server/net/NetworkPolicyManagerService.java
@@ -39,6 +39,7 @@ import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkPolicy.LIMIT_DISABLED;
import static android.net.NetworkPolicy.SNOOZE_NEVER;
@@ -70,9 +71,18 @@ import static android.net.NetworkTemplate.MATCH_MOBILE;
import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.NetworkTemplate.buildTemplateMobileAll;
import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.provider.Settings.Global.NETPOLICY_OVERRIDE_ENABLED;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_ENABLED;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_FRAC_JOBS;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_FRAC_MULTIPATH;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_LIMITED;
+import static android.provider.Settings.Global.NETPOLICY_QUOTA_UNLIMITED;
import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
+import static android.telephony.CarrierConfigManager.KEY_DATA_LIMIT_NOTIFICATION_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_DATA_RAPID_NOTIFICATION_BOOL;
+import static android.telephony.CarrierConfigManager.KEY_DATA_WARNING_NOTIFICATION_BOOL;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
import static com.android.internal.util.ArrayUtils.appendInt;
@@ -115,6 +125,7 @@ import android.app.PendingIntent;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -188,6 +199,7 @@ import android.util.AtomicFile;
import android.util.DataUnit;
import android.util.Log;
import android.util.Pair;
+import android.util.Range;
import android.util.RecurrenceRule;
import android.util.Slog;
import android.util.SparseArray;
@@ -208,6 +220,7 @@ import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
+import com.android.internal.util.StatLogger;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
@@ -351,6 +364,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
*/
private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000;
+ private static final long QUOTA_UNLIMITED_DEFAULT = DataUnit.MEBIBYTES.toBytes(20);
+ private static final float QUOTA_LIMITED_DEFAULT = 0.1f;
+ private static final float QUOTA_FRAC_JOBS_DEFAULT = 0.5f;
+ private static final float QUOTA_FRAC_MULTIPATH_DEFAULT = 0.5f;
+
private static final int MSG_RULES_CHANGED = 1;
private static final int MSG_METERED_IFACES_CHANGED = 2;
private static final int MSG_LIMIT_REACHED = 5;
@@ -362,6 +380,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
private static final int MSG_RESET_FIREWALL_RULES_BY_UID = 15;
private static final int MSG_SUBSCRIPTION_OVERRIDE = 16;
private static final int MSG_METERED_RESTRICTED_PACKAGES_CHANGED = 17;
+ private static final int MSG_SET_NETWORK_TEMPLATE_ENABLED = 18;
private static final int UID_MSG_STATE_CHANGED = 100;
private static final int UID_MSG_GONE = 101;
@@ -490,6 +509,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
@GuardedBy("mNetworkPoliciesSecondLock")
private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray();
+ /** Map from network ID to last observed roaming state */
+ @GuardedBy("mNetworkPoliciesSecondLock")
+ private final SparseBooleanArray mNetworkRoaming = new SparseBooleanArray();
+
/** Map from netId to subId as of last update */
@GuardedBy("mNetworkPoliciesSecondLock")
private final SparseIntArray mNetIdToSubId = new SparseIntArray();
@@ -526,6 +549,19 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
// TODO: migrate notifications to SystemUI
+
+ interface Stats {
+ int UPDATE_NETWORK_ENABLED = 0;
+ int IS_UID_NETWORKING_BLOCKED = 1;
+
+ int COUNT = IS_UID_NETWORKING_BLOCKED + 1;
+ }
+
+ public final StatLogger mStatLogger = new StatLogger(new String[] {
+ "updateNetworkEnabledNL()",
+ "isUidNetworkingBlocked()",
+ });
+
public NetworkPolicyManagerService(Context context, IActivityManager activityManager,
INetworkManagementService networkManagement) {
this(context, activityManager, networkManagement, AppGlobals.getPackageManager(),
@@ -1005,6 +1041,16 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
};
+ private static boolean updateCapabilityChange(SparseBooleanArray lastValues, boolean newValue,
+ Network network) {
+ final boolean lastValue = lastValues.get(network.netId, false);
+ final boolean changed = (lastValue != newValue) || lastValues.indexOfKey(network.netId) < 0;
+ if (changed) {
+ lastValues.put(network.netId, newValue);
+ }
+ return changed;
+ }
+
private final NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
public void onCapabilitiesChanged(Network network,
@@ -1012,13 +1058,18 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
if (network == null || networkCapabilities == null) return;
synchronized (mNetworkPoliciesSecondLock) {
- final boolean oldMetered = mNetworkMetered.get(network.netId, false);
final boolean newMetered = !networkCapabilities
.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ final boolean meteredChanged = updateCapabilityChange(
+ mNetworkMetered, newMetered, network);
+
+ final boolean newRoaming = !networkCapabilities
+ .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+ final boolean roamingChanged = updateCapabilityChange(
+ mNetworkRoaming, newRoaming, network);
- if ((oldMetered != newMetered) || mNetworkMetered.indexOfKey(network.netId) < 0) {
+ if (meteredChanged || roamingChanged) {
mLogger.meterednessChanged(network.netId, newMetered);
- mNetworkMetered.put(network.netId, newMetered);
updateNetworkRulesNL();
}
}
@@ -1059,8 +1110,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final long now = mClock.millis();
for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
final NetworkPolicy policy = mNetworkPolicy.valueAt(i);
+ final int subId = findRelevantSubId(policy.template);
+
// ignore policies that aren't relevant to user
- if (!isTemplateRelevant(policy.template)) continue;
+ if (subId == INVALID_SUBSCRIPTION_ID) continue;
if (!policy.hasCycle()) continue;
final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager
@@ -1069,28 +1122,43 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final long cycleEnd = cycle.second.toInstant().toEpochMilli();
final long totalBytes = getTotalBytes(policy.template, cycleStart, cycleEnd);
- // Notify when data usage is over warning/limit
- if (policy.isOverLimit(totalBytes)) {
- final boolean snoozedThisCycle = policy.lastLimitSnooze >= cycleStart;
- if (snoozedThisCycle) {
- enqueueNotification(policy, TYPE_LIMIT_SNOOZED, totalBytes, null);
- } else {
- enqueueNotification(policy, TYPE_LIMIT, totalBytes, null);
- notifyOverLimitNL(policy.template);
+ // Carrier might want to manage notifications themselves
+ final PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId);
+ final boolean notifyWarning = getBooleanDefeatingNullable(config,
+ KEY_DATA_WARNING_NOTIFICATION_BOOL, true);
+ final boolean notifyLimit = getBooleanDefeatingNullable(config,
+ KEY_DATA_LIMIT_NOTIFICATION_BOOL, true);
+ final boolean notifyRapid = getBooleanDefeatingNullable(config,
+ KEY_DATA_RAPID_NOTIFICATION_BOOL, true);
+
+ // Notify when data usage is over warning
+ if (notifyWarning) {
+ if (policy.isOverWarning(totalBytes) && !policy.isOverLimit(totalBytes)) {
+ final boolean snoozedThisCycle = policy.lastWarningSnooze >= cycleStart;
+ if (!snoozedThisCycle) {
+ enqueueNotification(policy, TYPE_WARNING, totalBytes, null);
+ }
}
+ }
- } else {
- notifyUnderLimitNL(policy.template);
-
- final boolean snoozedThisCycle = policy.lastWarningSnooze >= cycleStart;
- if (policy.isOverWarning(totalBytes) && !snoozedThisCycle) {
- enqueueNotification(policy, TYPE_WARNING, totalBytes, null);
+ // Notify when data usage is over limit
+ if (notifyLimit) {
+ if (policy.isOverLimit(totalBytes)) {
+ final boolean snoozedThisCycle = policy.lastLimitSnooze >= cycleStart;
+ if (snoozedThisCycle) {
+ enqueueNotification(policy, TYPE_LIMIT_SNOOZED, totalBytes, null);
+ } else {
+ enqueueNotification(policy, TYPE_LIMIT, totalBytes, null);
+ notifyOverLimitNL(policy.template);
+ }
+ } else {
+ notifyUnderLimitNL(policy.template);
}
}
// Warn if average usage over last 4 days is on track to blow pretty
// far past the plan limits.
- if (policy.limitBytes != LIMIT_DISABLED) {
+ if (notifyRapid && policy.limitBytes != LIMIT_DISABLED) {
final long recentDuration = TimeUnit.DAYS.toMillis(4);
final long recentStart = now - recentDuration;
final long recentEnd = now;
@@ -1167,27 +1235,26 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
* current device state, such as when
* {@link TelephonyManager#getSubscriberId()} matches. This is regardless of
* data connection status.
+ *
+ * @return relevant subId, or {@link #INVALID_SUBSCRIPTION_ID} when no
+ * matching subId found.
*/
- private boolean isTemplateRelevant(NetworkTemplate template) {
- if (template.isMatchRuleMobile()) {
- final TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
- final SubscriptionManager sub = mContext.getSystemService(SubscriptionManager.class);
+ private int findRelevantSubId(NetworkTemplate template) {
+ final TelephonyManager tele = mContext.getSystemService(TelephonyManager.class);
+ final SubscriptionManager sub = mContext.getSystemService(SubscriptionManager.class);
- // Mobile template is relevant when any active subscriber matches
- final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
- for (int subId : subIds) {
- final String subscriberId = tele.getSubscriberId(subId);
- final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
- TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true,
- true);
- if (template.matches(probeIdent)) {
- return true;
- }
+ // Mobile template is relevant when any active subscriber matches
+ final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
+ for (int subId : subIds) {
+ final String subscriberId = tele.getSubscriberId(subId);
+ final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
+ TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true,
+ true);
+ if (template.matches(probeIdent)) {
+ return subId;
}
- return false;
- } else {
- return true;
}
+ return INVALID_SUBSCRIPTION_ID;
}
/**
@@ -1538,6 +1605,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
// TODO: reset any policy-disabled networks when any policy is removed
// completely, which is currently rare case.
+ final long startTime = mStatLogger.getTime();
+
for (int i = mNetworkPolicy.size()-1; i >= 0; i--) {
final NetworkPolicy policy = mNetworkPolicy.valueAt(i);
// shortcut when policy has no limit
@@ -1559,6 +1628,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
setNetworkTemplateEnabled(policy.template, networkEnabled);
}
+
+ mStatLogger.logDurationStat(Stats.UPDATE_NETWORK_ENABLED, startTime);
}
/**
@@ -1566,6 +1637,13 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
* {@link NetworkTemplate}.
*/
private void setNetworkTemplateEnabled(NetworkTemplate template, boolean enabled) {
+ // Don't call setNetworkTemplateEnabledInner() directly because we may have a lock
+ // held. Call it via the handler.
+ mHandler.obtainMessage(MSG_SET_NETWORK_TEMPLATE_ENABLED, enabled ? 1 : 0, 0, template)
+ .sendToTarget();
+ }
+
+ private void setNetworkTemplateEnabledInner(NetworkTemplate template, boolean enabled) {
// TODO: reach into ConnectivityManager to proactively disable bringing
// up this network, since we know that traffic will be blocked.
@@ -1731,10 +1809,18 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
mMeteredIfaces = newMeteredIfaces;
+ final ContentResolver cr = mContext.getContentResolver();
+ final boolean quotaEnabled = Settings.Global.getInt(cr,
+ NETPOLICY_QUOTA_ENABLED, 1) != 0;
+ final long quotaUnlimited = Settings.Global.getLong(cr,
+ NETPOLICY_QUOTA_UNLIMITED, QUOTA_UNLIMITED_DEFAULT);
+ final float quotaLimited = Settings.Global.getFloat(cr,
+ NETPOLICY_QUOTA_LIMITED, QUOTA_LIMITED_DEFAULT);
+
// Finally, calculate our opportunistic quotas
- // TODO: add experiments support to disable or tweak ratios
mSubscriptionOpportunisticQuota.clear();
for (NetworkState state : states) {
+ if (!quotaEnabled) continue;
if (state.network == null) continue;
final int subId = getSubIdLocked(state.network);
final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
@@ -1742,18 +1828,21 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final long quotaBytes;
final long limitBytes = plan.getDataLimitBytes();
- if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
+ if (!state.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
+ // Clamp to 0 when roaming
+ quotaBytes = 0;
+ } else if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
quotaBytes = OPPORTUNISTIC_QUOTA_UNKNOWN;
} else if (limitBytes == SubscriptionPlan.BYTES_UNLIMITED) {
// Unlimited data; let's use 20MiB/day (600MiB/month)
- quotaBytes = DataUnit.MEBIBYTES.toBytes(20);
+ quotaBytes = quotaUnlimited;
} else {
// Limited data; let's only use 10% of remaining budget
- final Pair<ZonedDateTime, ZonedDateTime> cycle = plan.cycleIterator().next();
- final long start = cycle.first.toInstant().toEpochMilli();
- final long end = cycle.second.toInstant().toEpochMilli();
+ final Range<ZonedDateTime> cycle = plan.cycleIterator().next();
+ final long start = cycle.getLower().toInstant().toEpochMilli();
+ final long end = cycle.getUpper().toInstant().toEpochMilli();
final Instant now = mClock.instant();
- final long startOfDay = ZonedDateTime.ofInstant(now, cycle.first.getZone())
+ final long startOfDay = ZonedDateTime.ofInstant(now, cycle.getLower().getZone())
.truncatedTo(ChronoUnit.DAYS)
.toInstant().toEpochMilli();
final long totalBytes = getTotalBytes(
@@ -1764,7 +1853,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final long remainingDays =
1 + ((end - now.toEpochMilli() - 1) / TimeUnit.DAYS.toMillis(1));
- quotaBytes = Math.max(0, (remainingBytes / remainingDays) / 10);
+ quotaBytes = Math.max(0, (long) ((remainingBytes / remainingDays) * quotaLimited));
}
mSubscriptionOpportunisticQuota.put(subId, quotaBytes);
@@ -3034,17 +3123,25 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
// We can only override when carrier told us about plans
synchronized (mNetworkPoliciesSecondLock) {
- if (ArrayUtils.isEmpty(mSubscriptionPlans.get(subId))) {
+ final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
+ if (plan == null
+ || plan.getDataLimitBehavior() == SubscriptionPlan.LIMIT_BEHAVIOR_UNKNOWN) {
throw new IllegalStateException(
- "Must provide SubscriptionPlan information before overriding");
+ "Must provide valid SubscriptionPlan to enable overriding");
}
}
- mHandler.sendMessage(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
- overrideMask, overrideValue, subId));
- if (timeoutMillis > 0) {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
- overrideMask, 0, subId), timeoutMillis);
+ // Only allow overrides when feature is enabled. However, we always
+ // allow disabling of overrides for safety reasons.
+ final boolean overrideEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+ NETPOLICY_OVERRIDE_ENABLED, 1) != 0;
+ if (overrideEnabled || overrideValue == 0) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+ overrideMask, overrideValue, subId));
+ if (timeoutMillis > 0) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+ overrideMask, 0, subId), timeoutMillis);
+ }
}
}
@@ -3222,6 +3319,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
fout.decreaseIndent();
+ fout.println();
+ mStatLogger.dump(fout);
+
mLogger.dumpLogs(fout);
}
}
@@ -4182,6 +4282,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
setMeteredRestrictedPackagesInternal(packageNames, userId);
return true;
}
+ case MSG_SET_NETWORK_TEMPLATE_ENABLED: {
+ final NetworkTemplate template = (NetworkTemplate) msg.obj;
+ final boolean enabled = msg.arg1 != 0;
+ setNetworkTemplateEnabledInner(template, enabled);
+ return true;
+ }
default: {
return false;
}
@@ -4575,8 +4681,14 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
@Override
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+ final long startTime = mStatLogger.getTime();
+
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- return isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+ final boolean ret = isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+
+ mStatLogger.logDurationStat(Stats.IS_UID_NETWORKING_BLOCKED, startTime);
+
+ return ret;
}
private boolean isUidNetworkingBlockedInternal(int uid, boolean isNetworkMetered) {
@@ -4651,11 +4763,17 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
*/
@Override
public boolean isUidNetworkingBlocked(int uid, String ifname) {
+ final long startTime = mStatLogger.getTime();
+
final boolean isNetworkMetered;
synchronized (mNetworkPoliciesSecondLock) {
isNetworkMetered = mMeteredIfaces.contains(ifname);
}
- return isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+ final boolean ret = isUidNetworkingBlockedInternal(uid, isNetworkMetered);
+
+ mStatLogger.logDurationStat(Stats.IS_UID_NETWORKING_BLOCKED, startTime);
+
+ return ret;
}
@Override
@@ -4681,11 +4799,24 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
@Override
public long getSubscriptionOpportunisticQuota(Network network, int quotaType) {
+ final long quotaBytes;
synchronized (mNetworkPoliciesSecondLock) {
- // TODO: handle splitting quota between use-cases
- return mSubscriptionOpportunisticQuota.get(getSubIdLocked(network),
+ quotaBytes = mSubscriptionOpportunisticQuota.get(getSubIdLocked(network),
OPPORTUNISTIC_QUOTA_UNKNOWN);
}
+ if (quotaBytes == OPPORTUNISTIC_QUOTA_UNKNOWN) {
+ return OPPORTUNISTIC_QUOTA_UNKNOWN;
+ }
+
+ if (quotaType == QUOTA_TYPE_JOBS) {
+ return (long) (quotaBytes * Settings.Global.getFloat(mContext.getContentResolver(),
+ NETPOLICY_QUOTA_FRAC_JOBS, QUOTA_FRAC_JOBS_DEFAULT));
+ } else if (quotaType == QUOTA_TYPE_MULTIPATH) {
+ return (long) (quotaBytes * Settings.Global.getFloat(mContext.getContentResolver(),
+ NETPOLICY_QUOTA_FRAC_MULTIPATH, QUOTA_FRAC_MULTIPATH_DEFAULT));
+ } else {
+ return OPPORTUNISTIC_QUOTA_UNKNOWN;
+ }
}
@Override
@@ -4754,7 +4885,21 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
@GuardedBy("mNetworkPoliciesSecondLock")
private SubscriptionPlan getPrimarySubscriptionPlanLocked(int subId) {
final SubscriptionPlan[] plans = mSubscriptionPlans.get(subId);
- return ArrayUtils.isEmpty(plans) ? null : plans[0];
+ if (!ArrayUtils.isEmpty(plans)) {
+ for (SubscriptionPlan plan : plans) {
+ if (plan.getCycleRule().isRecurring()) {
+ // Recurring plans will always have an active cycle
+ return plan;
+ } else {
+ // Non-recurring plans need manual test for active cycle
+ final Range<ZonedDateTime> cycle = plan.cycleIterator().next();
+ if (cycle.contains(ZonedDateTime.now(mClock))) {
+ return plan;
+ }
+ }
+ }
+ }
+ return null;
}
/**
@@ -4801,6 +4946,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
return (val != null) ? val : new NetworkState[0];
}
+ private static boolean getBooleanDefeatingNullable(@Nullable PersistableBundle bundle,
+ String key, boolean defaultValue) {
+ return (bundle != null) ? bundle.getBoolean(key, defaultValue) : defaultValue;
+ }
+
private class NotificationId {
private final String mTag;
private final int mId;
diff --git a/com/android/server/net/NetworkStatsCollection.java b/com/android/server/net/NetworkStatsCollection.java
index 2ef754e2..ab525237 100644
--- a/com/android/server/net/NetworkStatsCollection.java
+++ b/com/android/server/net/NetworkStatsCollection.java
@@ -47,7 +47,7 @@ import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.IntArray;
import android.util.MathUtils;
-import android.util.Pair;
+import android.util.Range;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
@@ -266,11 +266,11 @@ public class NetworkStatsCollection implements FileRotator.Reader {
long collectEnd = end;
if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) {
- final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = augmentPlan.cycleIterator();
+ final Iterator<Range<ZonedDateTime>> it = augmentPlan.cycleIterator();
while (it.hasNext()) {
- final Pair<ZonedDateTime, ZonedDateTime> cycle = it.next();
- final long cycleStart = cycle.first.toInstant().toEpochMilli();
- final long cycleEnd = cycle.second.toInstant().toEpochMilli();
+ final Range<ZonedDateTime> cycle = it.next();
+ final long cycleStart = cycle.getLower().toInstant().toEpochMilli();
+ final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli();
if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) {
augmentStart = cycleStart;
collectStart = Long.min(collectStart, augmentStart);
diff --git a/com/android/server/net/NetworkStatsObservers.java b/com/android/server/net/NetworkStatsObservers.java
index 741c2062..d8408730 100644
--- a/com/android/server/net/NetworkStatsObservers.java
+++ b/com/android/server/net/NetworkStatsObservers.java
@@ -16,7 +16,7 @@
package com.android.server.net;
-import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -52,8 +52,6 @@ class NetworkStatsObservers {
private static final String TAG = "NetworkStatsObservers";
private static final boolean LOGV = false;
- private static final long MIN_THRESHOLD_BYTES = 2 * MB_IN_BYTES;
-
private static final int MSG_REGISTER = 1;
private static final int MSG_UNREGISTER = 2;
private static final int MSG_UPDATE_STATS = 3;
diff --git a/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java b/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
index 17c5868a..766d8ca8 100644
--- a/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
+++ b/com/android/server/net/watchlist/NetworkWatchlistShellCommand.java
@@ -91,7 +91,7 @@ class NetworkWatchlistShellCommand extends ShellCommand {
final long ident = Binder.clearCallingIdentity();
try {
// Reset last report time
- if (!WatchlistConfig.getInstance().isConfigSecure()) {
+ if (WatchlistConfig.getInstance().isConfigSecure()) {
pw.println("Error: Cannot force generate report under production config");
return -1;
}
diff --git a/com/android/server/net/watchlist/ReportEncoder.java b/com/android/server/net/watchlist/ReportEncoder.java
index 2a8f4d5c..a482e058 100644
--- a/com/android/server/net/watchlist/ReportEncoder.java
+++ b/com/android/server/net/watchlist/ReportEncoder.java
@@ -49,6 +49,7 @@ class ReportEncoder {
* Apply DP on watchlist results, and generate a serialized watchlist report ready to store
* in DropBox.
*/
+ @Nullable
static byte[] encodeWatchlistReport(WatchlistConfig config, byte[] userSecret,
List<String> appDigestList, WatchlistReportDbHelper.AggregatedResult aggregatedResult) {
Map<String, Boolean> resultMap = PrivacyUtils.createDpEncodedReportMap(
diff --git a/com/android/server/net/watchlist/WatchlistConfig.java b/com/android/server/net/watchlist/WatchlistConfig.java
index d7938420..8352ca60 100644
--- a/com/android/server/net/watchlist/WatchlistConfig.java
+++ b/com/android/server/net/watchlist/WatchlistConfig.java
@@ -16,6 +16,7 @@
package com.android.server.net.watchlist;
+import android.annotation.Nullable;
import android.os.FileUtils;
import android.util.AtomicFile;
import android.util.Log;
@@ -55,9 +56,6 @@ class WatchlistConfig {
private static final String NETWORK_WATCHLIST_DB_FOR_TEST_PATH =
"/data/misc/network_watchlist/network_watchlist_for_test.xml";
- // Hash for null / unknown config, a 32 byte array filled with content 0x00
- private static final byte[] UNKNOWN_CONFIG_HASH = new byte[32];
-
private static class XmlTags {
private static final String WATCHLIST_CONFIG = "watchlist-config";
private static final String SHA256_DOMAIN = "sha256-domain";
@@ -228,16 +226,21 @@ class WatchlistConfig {
return mIsSecureConfig;
}
+ @Nullable
+ /**
+ * Get watchlist config SHA-256 digest.
+ * Return null if watchlist config does not exist.
+ */
public byte[] getWatchlistConfigHash() {
if (!mXmlFile.exists()) {
- return UNKNOWN_CONFIG_HASH;
+ return null;
}
try {
return DigestUtils.getSha256Hash(mXmlFile);
} catch (IOException | NoSuchAlgorithmException e) {
Log.e(TAG, "Unable to get watchlist config hash", e);
}
- return UNKNOWN_CONFIG_HASH;
+ return null;
}
/**
@@ -271,8 +274,10 @@ class WatchlistConfig {
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println("Watchlist config hash: " + HexDump.toHexString(getWatchlistConfigHash()));
+ final byte[] hash = getWatchlistConfigHash();
+ pw.println("Watchlist config hash: " + (hash != null ? HexDump.toHexString(hash) : null));
pw.println("Domain CRC32 digest list:");
+ // mDomainDigests won't go from non-null to null so it's safe
if (mDomainDigests != null) {
mDomainDigests.crc32Digests.dump(fd, pw, args);
}
@@ -281,6 +286,7 @@ class WatchlistConfig {
mDomainDigests.sha256Digests.dump(fd, pw, args);
}
pw.println("Ip CRC32 digest list:");
+ // mIpDigests won't go from non-null to null so it's safe
if (mIpDigests != null) {
mIpDigests.crc32Digests.dump(fd, pw, args);
}
diff --git a/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/com/android/server/net/watchlist/WatchlistLoggingHandler.java
index b331b9c0..864ce5dd 100644
--- a/com/android/server/net/watchlist/WatchlistLoggingHandler.java
+++ b/com/android/server/net/watchlist/WatchlistLoggingHandler.java
@@ -346,6 +346,7 @@ class WatchlistLoggingHandler extends Handler {
* @param ipAddresses Ip address that you want to search in watchlist.
* @return Ip address that exists in watchlist, null if it does not match anything.
*/
+ @Nullable
private String searchIpInWatchlist(String[] ipAddresses) {
for (String ipAddress : ipAddresses) {
if (isIpInWatchlist(ipAddress)) {
@@ -377,6 +378,7 @@ class WatchlistLoggingHandler extends Handler {
* @param host Host that we want to search.
* @return Domain that exists in watchlist, null if it does not match anything.
*/
+ @Nullable
private String searchAllSubDomainsInWatchlist(String host) {
if (host == null) {
return null;
@@ -392,6 +394,7 @@ class WatchlistLoggingHandler extends Handler {
/** Get all sub-domains in a host */
@VisibleForTesting
+ @Nullable
static String[] getAllSubDomains(String host) {
if (host == null) {
return null;
diff --git a/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index 632ab81b..c69934ac 100644
--- a/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -144,6 +144,7 @@ class WatchlistReportDbHelper extends SQLiteOpenHelper {
* Aggregate all records in database before input timestamp, and return a
* rappor encoded result.
*/
+ @Nullable
public AggregatedResult getAggregatedRecords(long untilTimestamp) {
final String selectStatement = WhiteListReportContract.TIMESTAMP + " < ?";
diff --git a/com/android/server/net/watchlist/WatchlistSettings.java b/com/android/server/net/watchlist/WatchlistSettings.java
index e20a510d..c2f3ba01 100644
--- a/com/android/server/net/watchlist/WatchlistSettings.java
+++ b/com/android/server/net/watchlist/WatchlistSettings.java
@@ -86,7 +86,7 @@ class WatchlistSettings {
}
}
- public void reloadSettings() {
+ private void reloadSettings() {
if (!mXmlFile.exists()) {
// No settings config
return;
diff --git a/com/android/server/notification/ManagedServices.java b/com/android/server/notification/ManagedServices.java
index d5a32aa3..c98f6a2b 100644
--- a/com/android/server/notification/ManagedServices.java
+++ b/com/android/server/notification/ManagedServices.java
@@ -110,7 +110,7 @@ abstract public class ManagedServices {
protected final Object mMutex;
private final UserProfiles mUserProfiles;
private final IPackageManager mPm;
- private final UserManager mUm;
+ protected final UserManager mUm;
private final Config mConfig;
private final Handler mHandler = new Handler(Looper.getMainLooper());
diff --git a/com/android/server/notification/NotificationDelegate.java b/com/android/server/notification/NotificationDelegate.java
index 36bc0962..b61a27ac 100644
--- a/com/android/server/notification/NotificationDelegate.java
+++ b/com/android/server/notification/NotificationDelegate.java
@@ -40,4 +40,6 @@ public interface NotificationDelegate {
void onNotificationExpansionChanged(String key, boolean userAction, boolean expanded);
void onNotificationDirectReplied(String key);
void onNotificationSettingsViewed(String key);
+ void onNotificationSmartRepliesAdded(String key, int replyCount);
+ void onNotificationSmartReplySent(String key, int replyIndex);
}
diff --git a/com/android/server/notification/NotificationManagerService.java b/com/android/server/notification/NotificationManagerService.java
index f31ca0a2..d59c9de2 100644
--- a/com/android/server/notification/NotificationManagerService.java
+++ b/com/android/server/notification/NotificationManagerService.java
@@ -114,12 +114,14 @@ import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
import android.media.IRingtonePlayer;
+import android.metrics.LogMaker;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
@@ -394,6 +396,8 @@ public class NotificationManagerService extends SystemService {
private GroupHelper mGroupHelper;
private boolean mIsTelevision;
+ private MetricsLogger mMetricsLogger;
+
private static class Archive {
final int mBufferSize;
final ArrayDeque<StatusBarNotification> mBuffer;
@@ -492,8 +496,8 @@ public class NotificationManagerService extends SystemService {
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
for (ComponentName cn : approvedAssistants) {
try {
- getBinderService().setNotificationAssistantAccessGrantedForUser(cn,
- userId, true);
+ getBinderService().setNotificationAssistantAccessGrantedForUser(
+ cn, userId, true);
} catch (RemoteException e) {
e.printStackTrace();
}
@@ -535,6 +539,8 @@ public class NotificationManagerService extends SystemService {
mConditionProviders.migrateToXml();
savePolicyFile();
}
+
+ mAssistants.ensureAssistant();
}
private void loadPolicyFile() {
@@ -798,6 +804,18 @@ public class NotificationManagerService extends SystemService {
// Report to usage stats that notification was made visible
if (DBG) Slog.d(TAG, "Marking notification as visible " + nv.key);
reportSeen(r);
+
+ // If the newly visible notification has smart replies
+ // then log that the user has seen them.
+ if (r.getNumSmartRepliesAdded() > 0
+ && !r.hasSeenSmartReplies()) {
+ r.setSeenSmartReplies(true);
+ LogMaker logMaker = r.getLogMaker()
+ .setCategory(MetricsEvent.SMART_REPLY_VISIBLE)
+ .addTaggedData(MetricsEvent.NOTIFICATION_SMART_REPLY_COUNT,
+ r.getNumSmartRepliesAdded());
+ mMetricsLogger.write(logMaker);
+ }
}
r.setVisibility(true, nv.rank);
nv.recycle();
@@ -852,6 +870,31 @@ public class NotificationManagerService extends SystemService {
}
@Override
+ public void onNotificationSmartRepliesAdded(String key, int replyCount) {
+ synchronized (mNotificationLock) {
+ NotificationRecord r = mNotificationsByKey.get(key);
+ if (r != null) {
+ r.setNumSmartRepliesAdded(replyCount);
+ }
+ }
+ }
+
+ @Override
+ public void onNotificationSmartReplySent(String key, int replyIndex) {
+ synchronized (mNotificationLock) {
+ NotificationRecord r = mNotificationsByKey.get(key);
+ if (r != null) {
+ LogMaker logMaker = r.getLogMaker()
+ .setCategory(MetricsEvent.SMART_REPLY_ACTION)
+ .setSubtype(replyIndex);
+ mMetricsLogger.write(logMaker);
+ // Treat clicking on a smart reply as a user interaction.
+ reportUserInteraction(r);
+ }
+ }
+ }
+
+ @Override
public void onNotificationSettingsViewed(String key) {
synchronized (mNotificationLock) {
NotificationRecord r = mNotificationsByKey.get(key);
@@ -1346,6 +1389,7 @@ public class NotificationManagerService extends SystemService {
extractorNames = new String[0];
}
mUsageStats = usageStats;
+ mMetricsLogger = new MetricsLogger();
mRankingHandler = new RankingHandlerWorker(mRankingThread.getLooper());
mConditionProviders = conditionProviders;
mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders);
@@ -1828,7 +1872,7 @@ public class NotificationManagerService extends SystemService {
};
int newSuppressedVisualEffects = incomingPolicy.suppressedVisualEffects;
- if (targetSdkVersion <= Build.VERSION_CODES.O_MR1) {
+ if (targetSdkVersion < Build.VERSION_CODES.P) {
// unset higher order bits introduced in P, maintain the user's higher order bits
for (int i = 0; i < effectsIntroducedInP.length ; i++) {
newSuppressedVisualEffects &= ~effectsIntroducedInP[i];
@@ -2063,7 +2107,7 @@ public class NotificationManagerService extends SystemService {
@Override
public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) {
- checkCallerIsSystem();
+ enforceSystemOrSystemUI("setNotificationsEnabledForPackage");
mRankingHelper.setEnabled(pkg, uid, enabled);
// Now, cancel any outstanding notifications that are part of a just-disabled app
@@ -2286,6 +2330,12 @@ public class NotificationManagerService extends SystemService {
}
@Override
+ public int getBlockedChannelCount(String pkg, int uid) {
+ enforceSystemOrSystemUI("getBlockedChannelCount");
+ return mRankingHelper.getBlockedChannelCount(pkg, uid);
+ }
+
+ @Override
public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroupsForPackage(
String pkg, int uid, boolean includeDeleted) {
checkCallerIsSystem();
@@ -3179,7 +3229,7 @@ public class NotificationManagerService extends SystemService {
0, UserHandle.getUserId(MY_UID));
Policy currPolicy = mZenModeHelper.getNotificationPolicy();
- if (applicationInfo.targetSdkVersion <= Build.VERSION_CODES.O_MR1) {
+ if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.P) {
int priorityCategories = policy.priorityCategories;
// ignore alarm and media values from new policy
priorityCategories &= ~Policy.PRIORITY_CATEGORY_ALARMS;
@@ -3947,9 +3997,14 @@ public class NotificationManagerService extends SystemService {
+ ", notificationUid=" + notificationUid
+ ", notification=" + notification;
Log.e(TAG, noChannelStr);
- doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
- "Failed to post notification on channel \"" + channelId + "\"\n" +
- "See log for more details");
+ boolean appNotificationsOff = mRankingHelper.getImportance(pkg, notificationUid)
+ == NotificationManager.IMPORTANCE_NONE;
+
+ if (!appNotificationsOff) {
+ doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
+ "Failed to post notification on channel \"" + channelId + "\"\n" +
+ "See log for more details");
+ }
return;
}
@@ -6134,11 +6189,14 @@ public class NotificationManagerService extends SystemService {
return !getServices().isEmpty();
}
- protected void upgradeXml(final int xmlVersion, final int userId) {
- if (xmlVersion == 0) {
- // one time approval of the OOB assistant
- Slog.d(TAG, "Approving default notification assistant for user " + userId);
- readDefaultAssistant(userId);
+ protected void ensureAssistant() {
+ final List<UserInfo> activeUsers = mUm.getUsers(true);
+ for (UserInfo userInfo : activeUsers) {
+ int userId = userInfo.getUserHandle().getIdentifier();
+ if (getAllowedPackages(userId).isEmpty()) {
+ Slog.d(TAG, "Approving default notification assistant for user " + userId);
+ readDefaultAssistant(userId);
+ }
}
}
}
diff --git a/com/android/server/notification/NotificationRecord.java b/com/android/server/notification/NotificationRecord.java
index c8870855..9bd3e529 100644
--- a/com/android/server/notification/NotificationRecord.java
+++ b/com/android/server/notification/NotificationRecord.java
@@ -149,6 +149,8 @@ public final class NotificationRecord {
private final NotificationStats mStats;
private int mUserSentiment;
private boolean mIsInterruptive;
+ private int mNumberOfSmartRepliesAdded;
+ private boolean mHasSeenSmartReplies;
@VisibleForTesting
public NotificationRecord(Context context, StatusBarNotification sbn,
@@ -962,6 +964,22 @@ public final class NotificationRecord {
mStats.setViewedSettings();
}
+ public void setNumSmartRepliesAdded(int noReplies) {
+ mNumberOfSmartRepliesAdded = noReplies;
+ }
+
+ public int getNumSmartRepliesAdded() {
+ return mNumberOfSmartRepliesAdded;
+ }
+
+ public boolean hasSeenSmartReplies() {
+ return mHasSeenSmartReplies;
+ }
+
+ public void setSeenSmartReplies(boolean hasSeenSmartReplies) {
+ mHasSeenSmartReplies = hasSeenSmartReplies;
+ }
+
public Set<Uri> getNotificationUris() {
Notification notification = getNotification();
Set<Uri> uris = new ArraySet<>();
diff --git a/com/android/server/notification/RankingHelper.java b/com/android/server/notification/RankingHelper.java
index 98d5c9a5..43d393a2 100644
--- a/com/android/server/notification/RankingHelper.java
+++ b/com/android/server/notification/RankingHelper.java
@@ -15,6 +15,8 @@
*/
package com.android.server.notification;
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
@@ -619,7 +621,7 @@ public class RankingHelper implements RankingConfig {
updateConfig();
return;
}
- if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
+ if (channel.getImportance() < IMPORTANCE_NONE
|| channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
throw new IllegalArgumentException("Invalid importance level");
}
@@ -959,6 +961,23 @@ public class RankingHelper implements RankingConfig {
return deletedCount;
}
+ public int getBlockedChannelCount(String pkg, int uid) {
+ Preconditions.checkNotNull(pkg);
+ int blockedCount = 0;
+ Record r = getRecord(pkg, uid);
+ if (r == null) {
+ return blockedCount;
+ }
+ int N = r.channels.size();
+ for (int i = 0; i < N; i++) {
+ final NotificationChannel nc = r.channels.valueAt(i);
+ if (!nc.isDeleted() && IMPORTANCE_NONE == nc.getImportance()) {
+ blockedCount++;
+ }
+ }
+ return blockedCount;
+ }
+
/**
* Sets importance.
*/
@@ -969,12 +988,12 @@ public class RankingHelper implements RankingConfig {
}
public void setEnabled(String packageName, int uid, boolean enabled) {
- boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
+ boolean wasEnabled = getImportance(packageName, uid) != IMPORTANCE_NONE;
if (wasEnabled == enabled) {
return;
}
setImportance(packageName, uid,
- enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
+ enabled ? DEFAULT_IMPORTANCE : IMPORTANCE_NONE);
}
@VisibleForTesting
@@ -1199,7 +1218,7 @@ public class RankingHelper implements RankingConfig {
ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
for (int i = 0; i < N; i++) {
final Record r = mRecords.valueAt(i);
- if (r.importance == NotificationManager.IMPORTANCE_NONE) {
+ if (r.importance == IMPORTANCE_NONE) {
packageBans.put(r.uid, r.pkg);
}
}
diff --git a/com/android/server/notification/ValidateNotificationPeople.java b/com/android/server/notification/ValidateNotificationPeople.java
index 6cf8f86a..639cc70f 100644
--- a/com/android/server/notification/ValidateNotificationPeople.java
+++ b/com/android/server/notification/ValidateNotificationPeople.java
@@ -18,6 +18,7 @@ package com.android.server.notification;
import android.annotation.Nullable;
import android.app.Notification;
+import android.app.Person;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
@@ -332,8 +333,8 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
return array;
}
- if (arrayList.get(0) instanceof Notification.Person) {
- ArrayList<Notification.Person> list = (ArrayList<Notification.Person>) arrayList;
+ if (arrayList.get(0) instanceof Person) {
+ ArrayList<Person> list = (ArrayList<Person>) arrayList;
final int N = list.size();
String[] array = new String[N];
for (int i = 0; i < N; i++) {
diff --git a/com/android/server/notification/ZenModeHelper.java b/com/android/server/notification/ZenModeHelper.java
index 586abc16..156f7025 100644
--- a/com/android/server/notification/ZenModeHelper.java
+++ b/com/android/server/notification/ZenModeHelper.java
@@ -32,6 +32,7 @@ import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.database.ContentObserver;
+import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
@@ -1198,8 +1199,6 @@ public class ZenModeHelper {
@VisibleForTesting
protected Notification createZenUpgradeNotification() {
- Intent intent = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
mContext.getResources().getString(R.string.global_action_settings));
@@ -1210,15 +1209,20 @@ public class ZenModeHelper {
title = R.string.zen_upgrade_notification_visd_title;
content = R.string.zen_upgrade_notification_visd_content;
}
+ Intent onboardingIntent = new Intent(Settings.ZEN_MODE_ONBOARDING);
+ onboardingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
return new Notification.Builder(mContext, SystemNotificationChannels.DO_NOT_DISTURB)
+ .setAutoCancel(true)
.setSmallIcon(R.drawable.ic_settings_24dp)
+ .setLargeIcon(Icon.createWithResource(mContext, R.drawable.ic_zen_24dp))
.setContentTitle(mContext.getResources().getString(title))
.setContentText(mContext.getResources().getString(content))
+ .setContentIntent(PendingIntent.getActivity(mContext, 0, onboardingIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT))
.setAutoCancel(true)
.setLocalOnly(true)
.addExtras(extras)
.setStyle(new Notification.BigTextStyle())
- .setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0, null))
.build();
}
diff --git a/com/android/server/om/OverlayManagerSettings.java b/com/android/server/om/OverlayManagerSettings.java
index 863045c2..e176351f 100644
--- a/com/android/server/om/OverlayManagerSettings.java
+++ b/com/android/server/om/OverlayManagerSettings.java
@@ -179,15 +179,19 @@ final class OverlayManagerSettings {
List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName,
final int userId) {
+ // Static RROs targeting "android" are loaded from AssetManager, and so they should be
+ // ignored in OverlayManagerService.
return selectWhereTarget(targetPackageName, userId)
- .filter((i) -> !i.isStatic())
+ .filter((i) -> !(i.isStatic() && "android".equals(i.getTargetPackageName())))
.map(SettingsItem::getOverlayInfo)
.collect(Collectors.toList());
}
ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
+ // Static RROs targeting "android" are loaded from AssetManager, and so they should be
+ // ignored in OverlayManagerService.
return selectWhereUser(userId)
- .filter((i) -> !i.isStatic())
+ .filter((i) -> !(i.isStatic() && "android".equals(i.getTargetPackageName())))
.map(SettingsItem::getOverlayInfo)
.collect(Collectors.groupingBy(info -> info.targetPackageName, ArrayMap::new,
Collectors.toList()));
diff --git a/com/android/server/pm/Installer.java b/com/android/server/pm/Installer.java
index 9d3f48b4..45f1a2b8 100644
--- a/com/android/server/pm/Installer.java
+++ b/com/android/server/pm/Installer.java
@@ -67,6 +67,8 @@ public class Installer extends SystemService {
public static final int DEXOPT_ENABLE_HIDDEN_API_CHECKS = 1 << 10;
/** Indicates that dexopt should convert to CompactDex. */
public static final int DEXOPT_GENERATE_COMPACT_DEX = 1 << 11;
+ /** Indicates that dexopt should generate an app image */
+ public static final int DEXOPT_GENERATE_APP_IMAGE = 1 << 12;
// NOTE: keep in sync with installd
public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 8;
diff --git a/com/android/server/pm/InstantAppResolver.java b/com/android/server/pm/InstantAppResolver.java
index bc9fa4b7..dbf0940f 100644
--- a/com/android/server/pm/InstantAppResolver.java
+++ b/com/android/server/pm/InstantAppResolver.java
@@ -256,8 +256,6 @@ public abstract class InstantAppResolver {
int flags = origIntent.getFlags();
final Intent intent = new Intent();
intent.setFlags(flags
- | Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_HISTORY
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
if (token != null) {
diff --git a/com/android/server/pm/LauncherAppsService.java b/com/android/server/pm/LauncherAppsService.java
index 8e78703f..595de9e3 100644
--- a/com/android/server/pm/LauncherAppsService.java
+++ b/com/android/server/pm/LauncherAppsService.java
@@ -39,6 +39,7 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.content.pm.UserInfo;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Binder;
@@ -49,6 +50,7 @@ import android.os.ParcelFileDescriptor;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.UserManagerInternal;
import android.provider.Settings;
import android.util.Log;
@@ -101,6 +103,7 @@ public class LauncherAppsService extends SystemService {
private static final boolean DEBUG = false;
private static final String TAG = "LauncherAppsService";
private final Context mContext;
+ private final UserManager mUm;
private final UserManagerInternal mUserManagerInternal;
private final ActivityManagerInternal mActivityManagerInternal;
private final ShortcutServiceInternal mShortcutServiceInternal;
@@ -113,6 +116,7 @@ public class LauncherAppsService extends SystemService {
public LauncherAppsImpl(Context context) {
mContext = context;
+ mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
mUserManagerInternal = Preconditions.checkNotNull(
LocalServices.getService(UserManagerInternal.class));
mActivityManagerInternal = Preconditions.checkNotNull(
@@ -233,6 +237,22 @@ public class LauncherAppsService extends SystemService {
* group.
*/
private boolean canAccessProfile(int targetUserId, String message) {
+ final int callingUserId = injectCallingUserId();
+
+ if (targetUserId == callingUserId) return true;
+
+ long ident = injectClearCallingIdentity();
+ try {
+ final UserInfo callingUserInfo = mUm.getUserInfo(callingUserId);
+ if (callingUserInfo != null && callingUserInfo.isManagedProfile()) {
+ Slog.w(TAG, message + " for another profile "
+ + targetUserId + " from " + callingUserId + " not allowed");
+ return false;
+ }
+ } finally {
+ injectRestoreCallingIdentity(ident);
+ }
+
return mUserManagerInternal.isProfileAccessible(injectCallingUserId(), targetUserId,
message, true);
}
diff --git a/com/android/server/pm/OtaDexoptService.java b/com/android/server/pm/OtaDexoptService.java
index 5a7893aa..320affb1 100644
--- a/com/android/server/pm/OtaDexoptService.java
+++ b/com/android/server/pm/OtaDexoptService.java
@@ -267,7 +267,7 @@ public class OtaDexoptService extends IOtaDexopt.Stub {
final StringBuilder builder = new StringBuilder();
// The current version.
- builder.append("8 ");
+ builder.append("9 ");
builder.append("dexopt");
diff --git a/com/android/server/pm/PackageDexOptimizer.java b/com/android/server/pm/PackageDexOptimizer.java
index 892fa12d..ebab1a72 100644
--- a/com/android/server/pm/PackageDexOptimizer.java
+++ b/com/android/server/pm/PackageDexOptimizer.java
@@ -60,6 +60,7 @@ import static com.android.server.pm.Installer.DEXOPT_STORAGE_DE;
import static com.android.server.pm.Installer.DEXOPT_IDLE_BACKGROUND_JOB;
import static com.android.server.pm.Installer.DEXOPT_ENABLE_HIDDEN_API_CHECKS;
import static com.android.server.pm.Installer.DEXOPT_GENERATE_COMPACT_DEX;
+import static com.android.server.pm.Installer.DEXOPT_GENERATE_APP_IMAGE;
import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
@@ -521,6 +522,10 @@ public class PackageDexOptimizer {
return getDexFlags(pkg.applicationInfo, compilerFilter, options);
}
+ private boolean isAppImageEnabled() {
+ return SystemProperties.get("dalvik.vm.appimageformat", "").length() > 0;
+ }
+
private int getDexFlags(ApplicationInfo info, String compilerFilter, DexoptOptions options) {
int flags = info.flags;
boolean debuggable = (flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
@@ -547,6 +552,14 @@ public class PackageDexOptimizer {
case PackageManagerService.REASON_INSTALL:
generateCompactDex = false;
}
+ // Use app images only if it is enabled and we are compiling
+ // profile-guided (so the app image doesn't conservatively contain all classes).
+ // If the app didn't request for the splits to be loaded in isolation or if it does not
+ // declare inter-split dependencies, then all the splits will be loaded in the base
+ // apk class loader (in the order of their definition, otherwise disable app images
+ // because they are unsupported for multiple class loaders. b/7269679
+ boolean generateAppImage = isProfileGuidedFilter && (info.splitDependencies == null ||
+ !info.requestsIsolatedSplitLoading()) && isAppImageEnabled();
int dexFlags =
(isPublic ? DEXOPT_PUBLIC : 0)
| (debuggable ? DEXOPT_DEBUGGABLE : 0)
@@ -554,6 +567,7 @@ public class PackageDexOptimizer {
| (options.isBootComplete() ? DEXOPT_BOOTCOMPLETE : 0)
| (options.isDexoptIdleBackgroundJob() ? DEXOPT_IDLE_BACKGROUND_JOB : 0)
| (generateCompactDex ? DEXOPT_GENERATE_COMPACT_DEX : 0)
+ | (generateAppImage ? DEXOPT_GENERATE_APP_IMAGE : 0)
| hiddenApiFlag;
return adjustDexoptFlags(dexFlags);
}
diff --git a/com/android/server/pm/PackageInstallerSession.java b/com/android/server/pm/PackageInstallerSession.java
index ee326185..f7a02156 100644
--- a/com/android/server/pm/PackageInstallerSession.java
+++ b/com/android/server/pm/PackageInstallerSession.java
@@ -122,8 +122,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private static final boolean LOGD = true;
private static final String REMOVE_SPLIT_MARKER_EXTENSION = ".removed";
- private static final int MSG_COMMIT = 0;
- private static final int MSG_ON_PACKAGE_INSTALLED = 1;
+ private static final int MSG_EARLY_BIND = 0;
+ private static final int MSG_COMMIT = 1;
+ private static final int MSG_ON_PACKAGE_INSTALLED = 2;
/** XML constants used for persisting a session */
static final String TAG_SESSION = "session";
@@ -280,6 +281,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
+ case MSG_EARLY_BIND:
+ earlyBindToDefContainer();
+ break;
case MSG_COMMIT:
synchronized (mLock) {
try {
@@ -315,6 +319,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
};
+ private void earlyBindToDefContainer() {
+ mPm.earlyBindToDefContainer();
+ }
+
/**
* @return {@code true} iff the installing is app an device owner or affiliated profile owner.
*/
@@ -410,6 +418,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
} finally {
Binder.restoreCallingIdentity(identity);
}
+ // attempt to bind to the DefContainer as early as possible
+ if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_EARLY_BIND));
+ }
}
public SessionInfo generateInfo() {
diff --git a/com/android/server/pm/PackageManagerService.java b/com/android/server/pm/PackageManagerService.java
index 2e530af8..a0476041 100644
--- a/com/android/server/pm/PackageManagerService.java
+++ b/com/android/server/pm/PackageManagerService.java
@@ -17,12 +17,12 @@
package com.android.server.pm;
import static android.Manifest.permission.DELETE_PACKAGES;
-import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
-import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
import static android.Manifest.permission.INSTALL_PACKAGES;
+import static android.Manifest.permission.MANAGE_DEVICE_ADMINS;
import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.REQUEST_DELETE_PACKAGES;
+import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.pm.PackageManager.CERT_INPUT_RAW_X509;
import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
@@ -167,8 +167,8 @@ import android.content.pm.PackageInfoLite;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageList;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.LegacyPackageDeleteObserver;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManagerInternal.PackageListObserver;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.ActivityIntentInfo;
@@ -274,6 +274,7 @@ import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IMediaContainerService;
import com.android.internal.app.ResolverActivity;
+import com.android.internal.app.SuspendedAppActivity;
import com.android.internal.content.NativeLibraryHelper;
import com.android.internal.content.PackageHelper;
import com.android.internal.logging.MetricsLogger;
@@ -310,10 +311,10 @@ import com.android.server.pm.dex.DexoptOptions;
import com.android.server.pm.dex.PackageDexUsage;
import com.android.server.pm.permission.BasePermission;
import com.android.server.pm.permission.DefaultPermissionGrantPolicy;
-import com.android.server.pm.permission.PermissionManagerService;
-import com.android.server.pm.permission.PermissionManagerInternal;
import com.android.server.pm.permission.DefaultPermissionGrantPolicy.DefaultPermissionGrantedCallback;
+import com.android.server.pm.permission.PermissionManagerInternal;
import com.android.server.pm.permission.PermissionManagerInternal.PermissionCallback;
+import com.android.server.pm.permission.PermissionManagerService;
import com.android.server.pm.permission.PermissionsState;
import com.android.server.pm.permission.PermissionsState.PermissionState;
import com.android.server.security.VerityUtils;
@@ -1326,6 +1327,7 @@ public class PackageManagerService extends IPackageManager.Stub
static final int INTENT_FILTER_VERIFIED = 18;
static final int WRITE_PACKAGE_LIST = 19;
static final int INSTANT_APP_RESOLUTION_PHASE_TWO = 20;
+ static final int DEF_CONTAINER_BIND = 21;
static final int WRITE_SETTINGS_DELAY = 10*1000; // 10 seconds
@@ -1417,8 +1419,7 @@ public class PackageManagerService extends IPackageManager.Stub
new ArrayList<HandlerParams>();
private boolean connectToService() {
- if (DEBUG_SD_INSTALL) Log.i(TAG, "Trying to bind to" +
- " DefaultContainerService");
+ if (DEBUG_INSTALL) Log.i(TAG, "Trying to bind to DefaultContainerService");
Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
if (mContext.bindServiceAsUser(service, mDefContainerConn,
@@ -1453,6 +1454,17 @@ public class PackageManagerService extends IPackageManager.Stub
void doHandleMessage(Message msg) {
switch (msg.what) {
+ case DEF_CONTAINER_BIND:
+ if (!mBound) {
+ Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "earlyBindingMCS",
+ System.identityHashCode(mHandler));
+ if (!connectToService()) {
+ Slog.e(TAG, "Failed to bind to media container service");
+ }
+ Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "earlyBindingMCS",
+ System.identityHashCode(mHandler));
+ }
+ break;
case INIT_COPY: {
HandlerParams params = (HandlerParams) msg.obj;
int idx = mPendingInstalls.size();
@@ -1511,7 +1523,6 @@ public class PackageManagerService extends IPackageManager.Stub
Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER,
params.traceMethod, params.traceCookie);
}
- return;
}
mPendingInstalls.clear();
} else {
@@ -3935,7 +3946,7 @@ public class PackageManagerService extends IPackageManager.Stub
ai.uid = UserHandle.getUid(userId, ps.appId);
ai.primaryCpuAbi = ps.primaryCpuAbiString;
ai.secondaryCpuAbi = ps.secondaryCpuAbiString;
- ai.versionCode = ps.versionCode;
+ ai.setVersionCode(ps.versionCode);
ai.flags = ps.pkgFlags;
ai.privateFlags = ps.pkgPrivateFlags;
pi.applicationInfo = PackageParser.generateApplicationInfo(ai, flags, state, userId);
@@ -5932,8 +5943,8 @@ public class PackageManagerService extends IPackageManager.Stub
@Override
public ResolveInfo resolveIntent(Intent intent, String resolvedType,
int flags, int userId) {
- return resolveIntentInternal(
- intent, resolvedType, flags, userId, false /*resolveForStart*/);
+ return resolveIntentInternal(intent, resolvedType, flags, userId, false,
+ Binder.getCallingUid());
}
/**
@@ -5942,19 +5953,19 @@ public class PackageManagerService extends IPackageManager.Stub
* since we need to allow the system to start any installed application.
*/
private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType,
- int flags, int userId, boolean resolveForStart) {
+ int flags, int userId, boolean resolveForStart, int filterCallingUid) {
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "resolveIntent");
if (!sUserManager.exists(userId)) return null;
final int callingUid = Binder.getCallingUid();
- flags = updateFlagsForResolve(flags, userId, intent, callingUid, resolveForStart);
+ flags = updateFlagsForResolve(flags, userId, intent, filterCallingUid, resolveForStart);
mPermissionManager.enforceCrossUserPermission(callingUid, userId,
false /*requireFullPermission*/, false /*checkShell*/, "resolve intent");
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "queryIntentActivities");
final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType,
- flags, callingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
+ flags, filterCallingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
final ResolveInfo bestChoice =
@@ -6762,7 +6773,7 @@ public class PackageManagerService extends IPackageManager.Stub
// the instant application, we'll do the right thing.
final ApplicationInfo ai = localInstantApp.activityInfo.applicationInfo;
auxiliaryResponse = new AuxiliaryResolveInfo(null /* failureActivity */,
- ai.packageName, ai.versionCode, null /* splitName */);
+ ai.packageName, ai.longVersionCode, null /* splitName */);
}
}
if (intent.isWebIntent() && auxiliaryResponse == null) {
@@ -6946,7 +6957,7 @@ public class PackageManagerService extends IPackageManager.Stub
installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
installFailureActivity,
info.activityInfo.packageName,
- info.activityInfo.applicationInfo.versionCode,
+ info.activityInfo.applicationInfo.longVersionCode,
info.activityInfo.splitName);
// add a non-generic filter
installerInfo.filter = new IntentFilter();
@@ -7692,7 +7703,7 @@ public class PackageManagerService extends IPackageManager.Stub
installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
null /* installFailureActivity */,
info.serviceInfo.packageName,
- info.serviceInfo.applicationInfo.versionCode,
+ info.serviceInfo.applicationInfo.longVersionCode,
info.serviceInfo.splitName);
// add a non-generic filter
installerInfo.filter = new IntentFilter();
@@ -7810,7 +7821,7 @@ public class PackageManagerService extends IPackageManager.Stub
installerInfo.auxiliaryInfo = new AuxiliaryResolveInfo(
null /*failureActivity*/,
info.providerInfo.packageName,
- info.providerInfo.applicationInfo.versionCode,
+ info.providerInfo.applicationInfo.longVersionCode,
info.providerInfo.splitName);
// add a non-generic filter
installerInfo.filter = new IntentFilter();
@@ -8185,35 +8196,22 @@ public class PackageManagerService extends IPackageManager.Stub
private ProviderInfo resolveContentProviderInternal(String name, int flags, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, name);
- final String instantAppPkgName = getInstantAppPackageName(Binder.getCallingUid());
- // reader
+ final int callingUid = Binder.getCallingUid();
synchronized (mPackages) {
final PackageParser.Provider provider = mProvidersByAuthority.get(name);
PackageSetting ps = provider != null
? mSettings.mPackages.get(provider.owner.packageName)
: null;
if (ps != null) {
- final boolean isInstantApp = ps.getInstantApp(userId);
- // normal application; filter out instant application provider
- if (instantAppPkgName == null && isInstantApp) {
- return null;
- }
- // instant application; filter out other instant applications
- if (instantAppPkgName != null
- && isInstantApp
- && !provider.owner.packageName.equals(instantAppPkgName)) {
- return null;
- }
- // instant application; filter out non-exposed provider
- if (instantAppPkgName != null
- && !isInstantApp
- && (provider.info.flags & ProviderInfo.FLAG_VISIBLE_TO_INSTANT_APP) == 0) {
- return null;
- }
// provider not enabled
if (!mSettings.isEnabledAndMatchLPr(provider.info, flags, userId)) {
return null;
}
+ final ComponentName component =
+ new ComponentName(provider.info.packageName, provider.info.name);
+ if (filterAppAccessLPr(ps, callingUid, component, TYPE_PROVIDER, userId)) {
+ return null;
+ }
return PackageParser.generateProviderInfo(
provider, flags, ps.readUserState(userId), userId);
}
@@ -8702,7 +8700,7 @@ public class PackageManagerService extends IPackageManager.Stub
disabledPkgSetting /* pkgSetting */, null /* disabledPkgSetting */,
null /* originalPkgSetting */, null, parseFlags, scanFlags,
(pkg == mPlatformPackage), user);
- applyPolicy(pkg, parseFlags, scanFlags);
+ applyPolicy(pkg, parseFlags, scanFlags, mPlatformPackage);
scanPackageOnlyLI(request, mFactoryTest, -1L);
}
}
@@ -9709,7 +9707,7 @@ public class PackageManagerService extends IPackageManager.Stub
if (expectedCertDigests.length > 1) {
// For apps targeting O MR1 we require explicit enumeration of all certs.
- final String[] libCertDigests = (targetSdk > Build.VERSION_CODES.O)
+ final String[] libCertDigests = (targetSdk >= Build.VERSION_CODES.O_MR1)
? PackageUtils.computeSignaturesSha256Digests(
libPkg.mSigningDetails.signatures)
: PackageUtils.computeSignaturesSha256Digests(
@@ -10021,7 +10019,7 @@ public class PackageManagerService extends IPackageManager.Stub
scanFlags = adjustScanFlags(scanFlags, pkgSetting, disabledPkgSetting, user, pkg);
synchronized (mPackages) {
- applyPolicy(pkg, parseFlags, scanFlags);
+ applyPolicy(pkg, parseFlags, scanFlags, mPlatformPackage);
assertPackageIsValid(pkg, parseFlags, scanFlags);
SharedUserSetting sharedUserSetting = null;
@@ -10184,20 +10182,10 @@ public class PackageManagerService extends IPackageManager.Stub
// The signature has changed, but this package is in the system
// image... let's recover!
pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
- // However... if this package is part of a shared user, but it
- // doesn't match the signature of the shared user, let's fail.
- // What this means is that you can't change the signatures
- // associated with an overall shared user, which doesn't seem all
- // that unreasonable.
+ // If the system app is part of a shared user we allow that shared user to change
+ // signatures as well in part as part of an OTA.
if (signatureCheckPs.sharedUser != null) {
- if (compareSignatures(
- signatureCheckPs.sharedUser.signatures.mSigningDetails.signatures,
- pkg.mSigningDetails.signatures) != PackageManager.SIGNATURE_MATCH) {
- throw new PackageManagerException(
- INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
- "Signature mismatch for shared user: "
- + pkgSetting.sharedUser);
- }
+ signatureCheckPs.sharedUser.signatures.mSigningDetails = pkg.mSigningDetails;
}
// File a report about this.
String msg = "System package " + pkg.packageName
@@ -10701,7 +10689,7 @@ public class PackageManagerService extends IPackageManager.Stub
* ideally be static, but, it requires locks to read system state.
*/
private static void applyPolicy(PackageParser.Package pkg, final @ParseFlags int parseFlags,
- final @ScanFlags int scanFlags) {
+ final @ScanFlags int scanFlags, PackageParser.Package platformPkg) {
if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
if (pkg.applicationInfo.isDirectBootAware()) {
@@ -10787,6 +10775,15 @@ public class PackageManagerService extends IPackageManager.Stub
pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRODUCT;
}
+ // Check if the package is signed with the same key as the platform package.
+ if (PLATFORM_PACKAGE_NAME.equals(pkg.packageName) ||
+ (platformPkg != null && compareSignatures(
+ platformPkg.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures) == PackageManager.SIGNATURE_MATCH)) {
+ pkg.applicationInfo.privateFlags |=
+ ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY;
+ }
+
if (!isSystemApp(pkg)) {
// Only system apps can use these features.
pkg.mOriginalPackages = null;
@@ -13634,6 +13631,14 @@ public class PackageManagerService extends IPackageManager.Stub
return installReason;
}
+ /**
+ * Attempts to bind to the default container service explicitly instead of doing so lazily on
+ * install commit.
+ */
+ void earlyBindToDefContainer() {
+ mHandler.sendMessage(mHandler.obtainMessage(DEF_CONTAINER_BIND));
+ }
+
void installStage(String packageName, File stagedDir,
IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams,
String installerPackageName, int installerUid, UserHandle user,
@@ -13991,8 +13996,8 @@ public class PackageManagerService extends IPackageManager.Stub
@Override
public String[] setPackagesSuspendedAsUser(String[] packageNames, boolean suspended,
- PersistableBundle appExtras, PersistableBundle launcherExtras, String callingPackage,
- int userId) {
+ PersistableBundle appExtras, PersistableBundle launcherExtras, String dialogMessage,
+ String callingPackage, int userId) {
try {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.SUSPEND_APPS, null);
} catch (SecurityException e) {
@@ -14006,7 +14011,7 @@ public class PackageManagerService extends IPackageManager.Stub
"setPackagesSuspended for user " + userId);
if (callingUid != Process.ROOT_UID &&
!UserHandle.isSameApp(getPackageUid(callingPackage, 0, userId), callingUid)) {
- throw new IllegalArgumentException("callingPackage " + callingPackage + " does not"
+ throw new IllegalArgumentException("CallingPackage " + callingPackage + " does not"
+ " belong to calling app id " + UserHandle.getAppId(callingUid));
}
@@ -14030,20 +14035,18 @@ public class PackageManagerService extends IPackageManager.Stub
final PackageSetting pkgSetting = mSettings.mPackages.get(packageName);
if (pkgSetting == null
|| filterAppAccessLPr(pkgSetting, callingUid, userId)) {
- Slog.w(TAG, "Could not find package setting for package \"" + packageName
- + "\". Skipping suspending/un-suspending.");
+ Slog.w(TAG, "Could not find package setting for package: " + packageName
+ + ". Skipping suspending/un-suspending.");
unactionedPackages.add(packageName);
continue;
}
- if (pkgSetting.getSuspended(userId) != suspended) {
- if (!canSuspendPackageForUserLocked(packageName, userId)) {
- unactionedPackages.add(packageName);
- continue;
- }
- pkgSetting.setSuspended(suspended, callingPackage, appExtras,
- launcherExtras, userId);
- changedPackagesList.add(packageName);
+ if (!canSuspendPackageForUserLocked(packageName, userId)) {
+ unactionedPackages.add(packageName);
+ continue;
}
+ pkgSetting.setSuspended(suspended, callingPackage, dialogMessage, appExtras,
+ launcherExtras, userId);
+ changedPackagesList.add(packageName);
}
}
} finally {
@@ -14058,7 +14061,6 @@ public class PackageManagerService extends IPackageManager.Stub
scheduleWritePackageRestrictionsLocked(userId);
}
}
-
return unactionedPackages.toArray(new String[unactionedPackages.size()]);
}
@@ -14066,7 +14068,8 @@ public class PackageManagerService extends IPackageManager.Stub
public PersistableBundle getSuspendedPackageAppExtras(String packageName, int userId) {
final int callingUid = Binder.getCallingUid();
if (getPackageUid(packageName, 0, userId) != callingUid) {
- mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS, null);
+ throw new SecurityException("Calling package " + packageName
+ + " does not belong to calling uid " + callingUid);
}
synchronized (mPackages) {
final PackageSetting ps = mSettings.mPackages.get(packageName);
@@ -14081,25 +14084,6 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- @Override
- public void setSuspendedPackageAppExtras(String packageName, PersistableBundle appExtras,
- int userId) {
- final int callingUid = Binder.getCallingUid();
- mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS, null);
- synchronized (mPackages) {
- final PackageSetting ps = mSettings.mPackages.get(packageName);
- if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
- throw new IllegalArgumentException("Unknown target package: " + packageName);
- }
- final PackageUserState packageUserState = ps.readUserState(userId);
- if (packageUserState.suspended) {
- packageUserState.suspendedAppExtras = appExtras;
- sendMyPackageSuspendedOrUnsuspended(new String[] {packageName}, true, appExtras,
- userId);
- }
- }
- }
-
private void sendMyPackageSuspendedOrUnsuspended(String[] affectedPackages, boolean suspended,
PersistableBundle appExtras, int userId) {
final String action;
@@ -14142,9 +14126,6 @@ public class PackageManagerService extends IPackageManager.Stub
mPermissionManager.enforceCrossUserPermission(callingUid, userId,
true /* requireFullPermission */, false /* checkShell */,
"isPackageSuspendedForUser for user " + userId);
- if (getPackageUid(packageName, 0, userId) != callingUid) {
- mContext.enforceCallingOrSelfPermission(Manifest.permission.SUSPEND_APPS, null);
- }
synchronized (mPackages) {
final PackageSetting ps = mSettings.mPackages.get(packageName);
if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
@@ -14154,18 +14135,26 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- void onSuspendingPackageRemoved(String packageName, int userId) {
- final int[] userIds = (userId == UserHandle.USER_ALL) ? sUserManager.getUserIds()
- : new int[] {userId};
- synchronized (mPackages) {
- for (PackageSetting ps : mSettings.mPackages.values()) {
- for (int user : userIds) {
- final PackageUserState pus = ps.readUserState(user);
+ void onSuspendingPackageRemoved(String packageName, int removedForUser) {
+ final int[] userIds = (removedForUser == UserHandle.USER_ALL) ? sUserManager.getUserIds()
+ : new int[] {removedForUser};
+ for (int userId : userIds) {
+ List<String> affectedPackages = new ArrayList<>();
+ synchronized (mPackages) {
+ for (PackageSetting ps : mSettings.mPackages.values()) {
+ final PackageUserState pus = ps.readUserState(userId);
if (pus.suspended && packageName.equals(pus.suspendingPackage)) {
- ps.setSuspended(false, null, null, null, user);
+ ps.setSuspended(false, null, null, null, null, userId);
+ affectedPackages.add(ps.name);
}
}
}
+ if (!affectedPackages.isEmpty()) {
+ final String[] packageArray = affectedPackages.toArray(
+ new String[affectedPackages.size()]);
+ sendMyPackageSuspendedOrUnsuspended(packageArray, false, null, userId);
+ sendPackagesSuspendedForUser(packageArray, userId, false, null);
+ }
}
}
@@ -18896,6 +18885,7 @@ public class PackageManagerService extends IPackageManager.Stub
false /*hidden*/,
false /*suspended*/,
null, /*suspendingPackage*/
+ null, /*dialogMessage*/
null, /*suspendedAppExtras*/
null, /*suspendedLauncherExtras*/
false /*instantApp*/,
@@ -23780,6 +23770,30 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
@Override
+ public boolean isPackageSuspended(String packageName, int userId) {
+ synchronized (mPackages) {
+ final PackageSetting ps = mSettings.mPackages.get(packageName);
+ return (ps != null) ? ps.getSuspended(userId) : false;
+ }
+ }
+
+ @Override
+ public String getSuspendingPackage(String suspendedPackage, int userId) {
+ synchronized (mPackages) {
+ final PackageSetting ps = mSettings.mPackages.get(suspendedPackage);
+ return (ps != null) ? ps.readUserState(userId).suspendingPackage : null;
+ }
+ }
+
+ @Override
+ public String getSuspendedDialogMessage(String suspendedPackage, int userId) {
+ synchronized (mPackages) {
+ final PackageSetting ps = mSettings.mPackages.get(suspendedPackage);
+ return (ps != null) ? ps.readUserState(userId).dialogMessage : null;
+ }
+ }
+
+ @Override
public int getPackageUid(String packageName, int flags, int userId) {
return PackageManagerService.this
.getPackageUid(packageName, flags, userId);
@@ -24001,9 +24015,9 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
@Override
public ResolveInfo resolveIntent(Intent intent, String resolvedType,
- int flags, int userId, boolean resolveForStart) {
+ int flags, int userId, boolean resolveForStart, int filterCallingUid) {
return resolveIntentInternal(
- intent, resolvedType, flags, userId, resolveForStart);
+ intent, resolvedType, flags, userId, resolveForStart, filterCallingUid);
}
@Override
diff --git a/com/android/server/pm/PackageManagerShellCommand.java b/com/android/server/pm/PackageManagerShellCommand.java
index 28e32a54..a92fbb67 100644
--- a/com/android/server/pm/PackageManagerShellCommand.java
+++ b/com/android/server/pm/PackageManagerShellCommand.java
@@ -1505,6 +1505,7 @@ class PackageManagerShellCommand extends ShellCommand {
private int runSuspend(boolean suspendedState) {
final PrintWriter pw = getOutPrintWriter();
int userId = UserHandle.USER_SYSTEM;
+ String dialogMessage = null;
final PersistableBundle appExtras = new PersistableBundle();
final PersistableBundle launcherExtras = new PersistableBundle();
String opt;
@@ -1513,6 +1514,9 @@ class PackageManagerShellCommand extends ShellCommand {
case "--user":
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
+ case "--dialogMessage":
+ dialogMessage = getNextArgRequired();
+ break;
case "--ael":
case "--aes":
case "--aed":
@@ -1553,7 +1557,7 @@ class PackageManagerShellCommand extends ShellCommand {
(Binder.getCallingUid() == Process.ROOT_UID) ? "root" : "com.android.shell";
try {
mInterface.setPackagesSuspendedAsUser(new String[]{packageName}, suspendedState,
- appExtras, launcherExtras, callingPackage, userId);
+ appExtras, launcherExtras, dialogMessage, callingPackage, userId);
pw.println("Package " + packageName + " new suspended state: "
+ mInterface.isPackageSuspendedForUser(packageName, userId));
return 0;
diff --git a/com/android/server/pm/PackageSettingBase.java b/com/android/server/pm/PackageSettingBase.java
index 008a81cd..138594cc 100644
--- a/com/android/server/pm/PackageSettingBase.java
+++ b/com/android/server/pm/PackageSettingBase.java
@@ -20,8 +20,6 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
-import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
-
import android.content.pm.ApplicationInfo;
import android.content.pm.IntentFilterVerificationInfo;
import android.content.pm.PackageManager;
@@ -398,11 +396,12 @@ public abstract class PackageSettingBase extends SettingBase {
return readUserState(userId).suspended;
}
- void setSuspended(boolean suspended, String suspendingPackage, PersistableBundle appExtras,
- PersistableBundle launcherExtras, int userId) {
+ void setSuspended(boolean suspended, String suspendingPackage, String dialogMessage,
+ PersistableBundle appExtras, PersistableBundle launcherExtras, int userId) {
final PackageUserState existingUserState = modifyUserState(userId);
existingUserState.suspended = suspended;
existingUserState.suspendingPackage = suspended ? suspendingPackage : null;
+ existingUserState.dialogMessage = suspended ? dialogMessage : null;
existingUserState.suspendedAppExtras = suspended ? appExtras : null;
existingUserState.suspendedLauncherExtras = suspended ? launcherExtras : null;
}
@@ -425,8 +424,8 @@ public abstract class PackageSettingBase extends SettingBase {
void setUserState(int userId, long ceDataInode, int enabled, boolean installed, boolean stopped,
boolean notLaunched, boolean hidden, boolean suspended, String suspendingPackage,
- PersistableBundle suspendedAppExtras, PersistableBundle suspendedLauncherExtras,
- boolean instantApp,
+ String dialogMessage, PersistableBundle suspendedAppExtras,
+ PersistableBundle suspendedLauncherExtras, boolean instantApp,
boolean virtualPreload, String lastDisableAppCaller,
ArraySet<String> enabledComponents, ArraySet<String> disabledComponents,
int domainVerifState, int linkGeneration, int installReason,
@@ -440,6 +439,7 @@ public abstract class PackageSettingBase extends SettingBase {
state.hidden = hidden;
state.suspended = suspended;
state.suspendingPackage = suspendingPackage;
+ state.dialogMessage = dialogMessage;
state.suspendedAppExtras = suspendedAppExtras;
state.suspendedLauncherExtras = suspendedLauncherExtras;
state.lastDisableAppCaller = lastDisableAppCaller;
diff --git a/com/android/server/pm/Settings.java b/com/android/server/pm/Settings.java
index d0e85443..898ecf3c 100644
--- a/com/android/server/pm/Settings.java
+++ b/com/android/server/pm/Settings.java
@@ -222,6 +222,7 @@ public final class Settings {
private static final String ATTR_HIDDEN = "hidden";
private static final String ATTR_SUSPENDED = "suspended";
private static final String ATTR_SUSPENDING_PACKAGE = "suspending-package";
+ private static final String ATTR_SUSPEND_DIALOG_MESSAGE = "suspend_dialog_message";
// Legacy, uninstall blocks are stored separately.
@Deprecated
private static final String ATTR_BLOCK_UNINSTALL = "blockUninstall";
@@ -734,6 +735,7 @@ public final class Settings {
false /*hidden*/,
false /*suspended*/,
null, /*suspendingPackage*/
+ null, /*dialogMessage*/
null, /*suspendedAppExtras*/
null, /*suspendedLauncherExtras*/
instantApp,
@@ -1628,6 +1630,7 @@ public final class Settings {
false /*hidden*/,
false /*suspended*/,
null, /*suspendingPackage*/
+ null, /*dialogMessage*/
null, /*suspendedAppExtras*/
null, /*suspendedLauncherExtras*/
false /*instantApp*/,
@@ -1704,6 +1707,8 @@ public final class Settings {
false);
String suspendingPackage = parser.getAttributeValue(null,
ATTR_SUSPENDING_PACKAGE);
+ final String dialogMessage = parser.getAttributeValue(null,
+ ATTR_SUSPEND_DIALOG_MESSAGE);
if (suspended && suspendingPackage == null) {
suspendingPackage = PLATFORM_PACKAGE_NAME;
}
@@ -1767,7 +1772,7 @@ public final class Settings {
setBlockUninstallLPw(userId, name, true);
}
ps.setUserState(userId, ceDataInode, enabled, installed, stopped, notLaunched,
- hidden, suspended, suspendingPackage, suspendedAppExtras,
+ hidden, suspended, suspendingPackage, dialogMessage, suspendedAppExtras,
suspendedLauncherExtras, instantApp, virtualPreload, enabledCaller,
enabledComponents, disabledComponents, verifState, linkGeneration,
installReason, harmfulAppWarning);
@@ -2077,7 +2082,14 @@ public final class Settings {
}
if (ustate.suspended) {
serializer.attribute(null, ATTR_SUSPENDED, "true");
- serializer.attribute(null, ATTR_SUSPENDING_PACKAGE, ustate.suspendingPackage);
+ if (ustate.suspendingPackage != null) {
+ serializer.attribute(null, ATTR_SUSPENDING_PACKAGE,
+ ustate.suspendingPackage);
+ }
+ if (ustate.dialogMessage != null) {
+ serializer.attribute(null, ATTR_SUSPEND_DIALOG_MESSAGE,
+ ustate.dialogMessage);
+ }
if (ustate.suspendedAppExtras != null) {
serializer.startTag(null, TAG_SUSPENDED_APP_EXTRAS);
try {
@@ -4750,8 +4762,11 @@ public final class Settings {
pw.print(" suspended=");
pw.print(ps.getSuspended(user.id));
if (ps.getSuspended(user.id)) {
+ final PackageUserState pus = ps.readUserState(user.id);
pw.print(" suspendingPackage=");
- pw.print(ps.readUserState(user.id).suspendingPackage);
+ pw.print(pus.suspendingPackage);
+ pw.print(" dialogMessage=");
+ pw.print(pus.dialogMessage);
}
pw.print(" stopped=");
pw.print(ps.getStopped(user.id));
diff --git a/com/android/server/pm/ShortcutPackageInfo.java b/com/android/server/pm/ShortcutPackageInfo.java
index eeaa3330..8c7871ff 100644
--- a/com/android/server/pm/ShortcutPackageInfo.java
+++ b/com/android/server/pm/ShortcutPackageInfo.java
@@ -21,6 +21,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ShortcutInfo;
import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
@@ -164,12 +165,13 @@ class ShortcutPackageInfo {
ShortcutService s, String packageName, @UserIdInt int packageUserId) {
final PackageInfo pi = s.getPackageInfoWithSignatures(packageName, packageUserId);
// retrieve the newest sigs
- Signature[][] signingHistory = pi.signingCertificateHistory;
- if (signingHistory == null || signingHistory.length == 0) {
+ SigningInfo signingInfo = pi.signingInfo;
+ if (signingInfo == null) {
Slog.e(TAG, "Can't get signatures: package=" + packageName);
return null;
}
- Signature[] signatures = signingHistory[signingHistory.length - 1];
+ // TODO (b/73988180) use entire signing history in case of rollbacks
+ Signature[] signatures = signingInfo.getApkContentsSigners();
final ShortcutPackageInfo ret = new ShortcutPackageInfo(pi.getLongVersionCode(),
pi.lastUpdateTime, BackupUtils.hashSignatureArray(signatures), /* shadow=*/ false);
@@ -192,13 +194,14 @@ class ShortcutPackageInfo {
return;
}
// retrieve the newest sigs
- Signature[][] signingHistory = pi.signingCertificateHistory;
- if (signingHistory == null || signingHistory.length == 0) {
+ SigningInfo signingInfo = pi.signingInfo;
+ if (signingInfo == null) {
Slog.w(TAG, "Not refreshing signature for " + pkg.getPackageName()
- + " since it appears to have no signature history.");
+ + " since it appears to have no signing info.");
return;
}
- Signature[] signatures = signingHistory[signingHistory.length - 1];
+ // TODO (b/73988180) use entire signing history in case of rollbacks
+ Signature[] signatures = signingInfo.getApkContentsSigners();
mSigHashes = BackupUtils.hashSignatureArray(signatures);
}
diff --git a/com/android/server/pm/ShortcutService.java b/com/android/server/pm/ShortcutService.java
index 15b46172..599e5a57 100644
--- a/com/android/server/pm/ShortcutService.java
+++ b/com/android/server/pm/ShortcutService.java
@@ -48,7 +48,6 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
-import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
@@ -100,7 +99,7 @@ import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
-import com.android.server.StatLogger;
+import com.android.internal.util.StatLogger;
import com.android.server.SystemService;
import com.android.server.pm.ShortcutUser.PackageWithUser;
diff --git a/com/android/server/pm/permission/BasePermission.java b/com/android/server/pm/permission/BasePermission.java
index bcf4b07d..1d002efc 100644
--- a/com/android/server/pm/permission/BasePermission.java
+++ b/com/android/server/pm/permission/BasePermission.java
@@ -411,17 +411,23 @@ public final class BasePermission {
}
public @NonNull PermissionInfo generatePermissionInfo(int adjustedProtectionLevel, int flags) {
- final boolean protectionLevelChanged = protectionLevel != adjustedProtectionLevel;
- // if we return different protection level, don't use the cached info
- if (perm != null && !protectionLevelChanged) {
- return PackageParser.generatePermissionInfo(perm, flags);
- }
- final PermissionInfo pi = new PermissionInfo();
- pi.name = name;
- pi.packageName = sourcePackageName;
- pi.nonLocalizedLabel = name;
- pi.protectionLevel = protectionLevelChanged ? adjustedProtectionLevel : protectionLevel;
- return pi;
+ PermissionInfo permissionInfo;
+ if (perm != null) {
+ final boolean protectionLevelChanged = protectionLevel != adjustedProtectionLevel;
+ permissionInfo = PackageParser.generatePermissionInfo(perm, flags);
+ if (protectionLevelChanged && permissionInfo == perm.info) {
+ // if we return different protection level, don't use the cached info
+ permissionInfo = new PermissionInfo(permissionInfo);
+ permissionInfo.protectionLevel = adjustedProtectionLevel;
+ }
+ return permissionInfo;
+ }
+ permissionInfo = new PermissionInfo();
+ permissionInfo.name = name;
+ permissionInfo.packageName = sourcePackageName;
+ permissionInfo.nonLocalizedLabel = name;
+ permissionInfo.protectionLevel = protectionLevel;
+ return permissionInfo;
}
public static boolean readLPw(@NonNull Map<String, BasePermission> out,
diff --git a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 518d464e..4055a475 100644
--- a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -1052,7 +1052,8 @@ public final class DefaultPermissionGrantPolicy {
private PackageParser.Package getDefaultSystemHandlerActivityPackage(
Intent intent, int userId) {
ResolveInfo handler = mServiceInternal.resolveIntent(intent,
- intent.resolveType(mContext.getContentResolver()), DEFAULT_FLAGS, userId, false);
+ intent.resolveType(mContext.getContentResolver()), DEFAULT_FLAGS, userId, false,
+ Binder.getCallingUid());
if (handler == null || handler.activityInfo == null) {
return null;
}
@@ -1093,7 +1094,7 @@ public final class DefaultPermissionGrantPolicy {
ResolveInfo homeActivity = mServiceInternal.resolveIntent(homeIntent,
homeIntent.resolveType(mContext.getContentResolver()), DEFAULT_FLAGS,
- userId, false);
+ userId, false, Binder.getCallingUid());
if (homeActivity != null) {
continue;
}
diff --git a/com/android/server/policy/PhoneWindowManager.java b/com/android/server/policy/PhoneWindowManager.java
index 9a25dcc5..46a636ca 100644
--- a/com/android/server/policy/PhoneWindowManager.java
+++ b/com/android/server/policy/PhoneWindowManager.java
@@ -87,6 +87,7 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS;
import static android.view.WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -618,6 +619,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
boolean mTranslucentDecorEnabled = true;
boolean mUseTvRouting;
int mVeryLongPressTimeout;
+ boolean mAllowStartActivityForLongPressOnPowerDuringSetup;
private boolean mHandleVolumeKeysInWM;
@@ -1622,7 +1624,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
: mKeyguardDelegate.isShowing();
if (!keyguardActive) {
Intent intent = new Intent(Intent.ACTION_VOICE_ASSIST);
- startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+ if (mAllowStartActivityForLongPressOnPowerDuringSetup) {
+ mContext.startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+ } else {
+ startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+ }
}
break;
}
@@ -2134,6 +2140,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
com.android.internal.R.integer.config_shortPressOnSleepBehavior);
mVeryLongPressTimeout = mContext.getResources().getInteger(
com.android.internal.R.integer.config_veryLongPressTimeout);
+ mAllowStartActivityForLongPressOnPowerDuringSetup = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_allowStartActivityForLongPressOnPowerInSetup);
mUseTvRouting = AudioSystem.getPlatformType(mContext) == AudioSystem.PLATFORM_TELEVISION;
@@ -5404,6 +5412,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
final boolean attachedInParent = attached != null && !layoutInScreen;
final boolean requestedHideNavigation =
(requestedSysUiFl & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0;
+
+ // TYPE_BASE_APPLICATION windows are never considered floating here because they don't get
+ // cropped / shifted to the displayFrame in WindowState.
+ final boolean floatingInScreenWindow = !attrs.isFullscreen() && layoutInScreen
+ && type != TYPE_BASE_APPLICATION;
+
// Ensure that windows with a DEFAULT or NEVER display cutout mode are laid out in
// the cutout safe zone.
if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
@@ -5438,7 +5452,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
// Windows that are attached to a parent and laid out in said parent already avoid
// the cutout according to that parent and don't need to be further constrained.
- if (!attachedInParent) {
+ // Floating IN_SCREEN windows get what they ask for and lay out in the full screen.
+ // They will later be cropped or shifted using the displayFrame in WindowState,
+ // which prevents overlap with the DisplayCutout.
+ if (!attachedInParent && !floatingInScreenWindow) {
mTmpRect.set(pf);
pf.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
parentFrameWasClippedByDisplayCutout |= !mTmpRect.equals(pf);
@@ -8013,29 +8030,35 @@ public class PhoneWindowManager implements WindowManagerPolicy {
private VibrationEffect getVibrationEffect(int effectId) {
long[] pattern;
switch (effectId) {
- case HapticFeedbackConstants.LONG_PRESS:
- pattern = mLongPressVibePattern;
- break;
case HapticFeedbackConstants.CLOCK_TICK:
+ case HapticFeedbackConstants.CONTEXT_CLICK:
return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
+ case HapticFeedbackConstants.KEYBOARD_RELEASE:
+ case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
+ case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
+ case HapticFeedbackConstants.ENTRY_BUMP:
+ case HapticFeedbackConstants.DRAG_CROSSING:
+ case HapticFeedbackConstants.GESTURE_END:
+ return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
+ case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS
+ case HapticFeedbackConstants.VIRTUAL_KEY:
+ case HapticFeedbackConstants.EDGE_RELEASE:
+ case HapticFeedbackConstants.CONFIRM:
+ case HapticFeedbackConstants.GESTURE_START:
+ return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+ case HapticFeedbackConstants.LONG_PRESS:
+ case HapticFeedbackConstants.EDGE_SQUEEZE:
+ return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+ case HapticFeedbackConstants.REJECT:
+ return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+
case HapticFeedbackConstants.CALENDAR_DATE:
pattern = mCalendarDateVibePattern;
break;
case HapticFeedbackConstants.SAFE_MODE_ENABLED:
pattern = mSafeModeEnabledVibePattern;
break;
- case HapticFeedbackConstants.CONTEXT_CLICK:
- return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
- case HapticFeedbackConstants.VIRTUAL_KEY:
- return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
- return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
- case HapticFeedbackConstants.KEYBOARD_PRESS: // == HapticFeedbackConstants.KEYBOARD_TAP
- return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- case HapticFeedbackConstants.KEYBOARD_RELEASE:
- return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
- case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
- return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
+
default:
return null;
}
@@ -8655,6 +8678,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
pw.print("mShortPressOnWindowBehavior=");
pw.println(shortPressOnWindowBehaviorToString(mShortPressOnWindowBehavior));
pw.print(prefix);
+ pw.print("mAllowStartActivityForLongPressOnPowerDuringSetup=");
+ pw.println(mAllowStartActivityForLongPressOnPowerDuringSetup);
+ pw.print(prefix);
pw.print("mHasSoftInput="); pw.print(mHasSoftInput);
pw.print(" mDismissImeOnBackKeyPressed="); pw.println(mDismissImeOnBackKeyPressed);
pw.print(prefix);
diff --git a/com/android/server/slice/DirtyTracker.java b/com/android/server/slice/DirtyTracker.java
new file mode 100644
index 00000000..4288edc8
--- /dev/null
+++ b/com/android/server/slice/DirtyTracker.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.android.server.slice;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * A parent object that cares when a Persistable changes and will schedule a serialization
+ * in response to the onPersistableDirty callback.
+ */
+public interface DirtyTracker {
+ void onPersistableDirty(Persistable obj);
+
+ /**
+ * An object that can be written to XML.
+ */
+ interface Persistable {
+ String getFileName();
+ void writeTo(XmlSerializer out) throws IOException;
+ }
+}
diff --git a/com/android/server/slice/SliceClientPermissions.java b/com/android/server/slice/SliceClientPermissions.java
new file mode 100644
index 00000000..e461e0d4
--- /dev/null
+++ b/com/android/server/slice/SliceClientPermissions.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.android.server.slice;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.server.slice.DirtyTracker.Persistable;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class SliceClientPermissions implements DirtyTracker, Persistable {
+
+ private static final String TAG = "SliceClientPermissions";
+
+ static final String TAG_CLIENT = "client";
+ private static final String TAG_AUTHORITY = "authority";
+ private static final String TAG_PATH = "path";
+ private static final String NAMESPACE = null;
+
+ private static final String ATTR_PKG = "pkg";
+ private static final String ATTR_AUTHORITY = "authority";
+ private static final String ATTR_FULL_ACCESS = "fullAccess";
+
+ private final PkgUser mPkg;
+ // Keyed off (authority, userId) rather than the standard (pkg, userId)
+ private final ArrayMap<PkgUser, SliceAuthority> mAuths = new ArrayMap<>();
+ private final DirtyTracker mTracker;
+ private boolean mHasFullAccess;
+
+ public SliceClientPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
+ mPkg = pkg;
+ mTracker = tracker;
+ }
+
+ public PkgUser getPkg() {
+ return mPkg;
+ }
+
+ public synchronized Collection<SliceAuthority> getAuthorities() {
+ return new ArrayList<>(mAuths.values());
+ }
+
+ public synchronized SliceAuthority getOrCreateAuthority(PkgUser authority, PkgUser provider) {
+ SliceAuthority ret = mAuths.get(authority);
+ if (ret == null) {
+ ret = new SliceAuthority(authority.getPkg(), provider, this);
+ mAuths.put(authority, ret);
+ onPersistableDirty(ret);
+ }
+ return ret;
+ }
+
+ public synchronized SliceAuthority getAuthority(PkgUser authority) {
+ return mAuths.get(authority);
+ }
+
+ public boolean hasFullAccess() {
+ return mHasFullAccess;
+ }
+
+ public void setHasFullAccess(boolean hasFullAccess) {
+ if (mHasFullAccess == hasFullAccess) return;
+ mHasFullAccess = hasFullAccess;
+ mTracker.onPersistableDirty(this);
+ }
+
+ public void removeAuthority(String authority, int userId) {
+ if (mAuths.remove(new PkgUser(authority, userId)) != null) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized boolean hasPermission(Uri uri, int userId) {
+ if (!Objects.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme())) return false;
+ SliceAuthority authority = getAuthority(new PkgUser(uri.getAuthority(), userId));
+ return authority != null && authority.hasPermission(uri.getPathSegments());
+ }
+
+ public void grantUri(Uri uri, PkgUser providerPkg) {
+ SliceAuthority authority = getOrCreateAuthority(
+ new PkgUser(uri.getAuthority(), providerPkg.getUserId()),
+ providerPkg);
+ authority.addPath(uri.getPathSegments());
+ }
+
+ public void revokeUri(Uri uri, PkgUser providerPkg) {
+ SliceAuthority authority = getOrCreateAuthority(
+ new PkgUser(uri.getAuthority(), providerPkg.getUserId()),
+ providerPkg);
+ authority.removePath(uri.getPathSegments());
+ }
+
+ public void clear() {
+ if (!mHasFullAccess && mAuths.isEmpty()) return;
+ mHasFullAccess = false;
+ mAuths.clear();
+ onPersistableDirty(this);
+ }
+
+ @Override
+ public void onPersistableDirty(Persistable obj) {
+ mTracker.onPersistableDirty(this);
+ }
+
+ @Override
+ public String getFileName() {
+ return getFileName(mPkg);
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ out.startTag(NAMESPACE, TAG_CLIENT);
+ out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString());
+ out.attribute(NAMESPACE, ATTR_FULL_ACCESS, mHasFullAccess ? "1" : "0");
+
+ final int N = mAuths.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_AUTHORITY);
+ out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority);
+ out.attribute(NAMESPACE, ATTR_PKG, mAuths.valueAt(i).mPkg.toString());
+
+ mAuths.valueAt(i).writeTo(out);
+
+ out.endTag(NAMESPACE, TAG_AUTHORITY);
+ }
+
+ out.endTag(NAMESPACE, TAG_CLIENT);
+ }
+
+ public static SliceClientPermissions createFrom(XmlPullParser parser, DirtyTracker tracker)
+ throws XmlPullParserException, IOException {
+ // Get to the beginning of the provider.
+ while (parser.getEventType() != XmlPullParser.START_TAG
+ || !TAG_CLIENT.equals(parser.getName())) {
+ parser.next();
+ }
+ int depth = parser.getDepth();
+ PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+ SliceClientPermissions provider = new SliceClientPermissions(pkgUser, tracker);
+ String fullAccess = parser.getAttributeValue(NAMESPACE, ATTR_FULL_ACCESS);
+ if (fullAccess == null) {
+ fullAccess = "0";
+ }
+ provider.mHasFullAccess = Integer.parseInt(fullAccess) != 0;
+ parser.next();
+
+ while (parser.getDepth() > depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_AUTHORITY.equals(parser.getName())) {
+ try {
+ PkgUser pkg = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+ SliceAuthority authority = new SliceAuthority(
+ parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), pkg, provider);
+ authority.readFrom(parser);
+ provider.mAuths.put(new PkgUser(authority.getAuthority(), pkg.getUserId()),
+ authority);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Couldn't read PkgUser", e);
+ }
+ }
+
+ parser.next();
+ }
+ return provider;
+ }
+
+ public static String getFileName(PkgUser pkg) {
+ return String.format("client_%s", pkg.toString());
+ }
+
+ public static class SliceAuthority implements Persistable {
+ public static final String DELIMITER = "/";
+ private final String mAuthority;
+ private final DirtyTracker mTracker;
+ private final PkgUser mPkg;
+ private final ArraySet<String[]> mPaths = new ArraySet<>();
+
+ public SliceAuthority(String authority, PkgUser pkg, DirtyTracker tracker) {
+ mAuthority = authority;
+ mPkg = pkg;
+ mTracker = tracker;
+ }
+
+ public String getAuthority() {
+ return mAuthority;
+ }
+
+ public PkgUser getPkg() {
+ return mPkg;
+ }
+
+ void addPath(List<String> path) {
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(existing, pathSegs)) {
+ // Nothing to add here.
+ return;
+ }
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ mPaths.removeAt(i);
+ }
+ }
+ mPaths.add(pathSegs);
+ mTracker.onPersistableDirty(this);
+ }
+
+ void removePath(List<String> path) {
+ boolean changed = false;
+ String[] pathSegs = path.toArray(new String[path.size()]);
+ for (int i = mPaths.size() - 1; i >= 0; i--) {
+ String[] existing = mPaths.valueAt(i);
+ if (isPathPrefixMatch(pathSegs, existing)) {
+ changed = true;
+ mPaths.removeAt(i);
+ }
+ }
+ if (changed) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized Collection<String[]> getPaths() {
+ return new ArraySet<>(mPaths);
+ }
+
+ public boolean hasPermission(List<String> path) {
+ for (String[] p : mPaths) {
+ if (isPathPrefixMatch(p, path.toArray(new String[path.size()]))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isPathPrefixMatch(String[] prefix, String[] path) {
+ final int prefixSize = prefix.length;
+ if (path.length < prefixSize) return false;
+
+ for (int i = 0; i < prefixSize; i++) {
+ if (!Objects.equals(path[i], prefix[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public String getFileName() {
+ return null;
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ final int N = mPaths.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_PATH);
+ out.text(encodeSegments(mPaths.valueAt(i)));
+ out.endTag(NAMESPACE, TAG_PATH);
+ }
+ }
+
+ public synchronized void readFrom(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ parser.next();
+ int depth = parser.getDepth();
+ while (parser.getDepth() >= depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_PATH.equals(parser.getName())) {
+ mPaths.add(decodeSegments(parser.nextText()));
+ }
+ parser.next();
+ }
+ }
+
+ private String encodeSegments(String[] s) {
+ String[] out = new String[s.length];
+ for (int i = 0; i < s.length; i++) {
+ out[i] = Uri.encode(s[i]);
+ }
+ return TextUtils.join(DELIMITER, out);
+ }
+
+ private String[] decodeSegments(String s) {
+ String[] sets = s.split(DELIMITER, -1);
+ for (int i = 0; i < sets.length; i++) {
+ sets[i] = Uri.decode(sets[i]);
+ }
+ return sets;
+ }
+
+ /**
+ * Only for testing, no deep equality of these are done normally.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+ SliceAuthority other = (SliceAuthority) obj;
+ if (mPaths.size() != other.mPaths.size()) return false;
+ ArrayList<String[]> p1 = new ArrayList<>(mPaths);
+ ArrayList<String[]> p2 = new ArrayList<>(other.mPaths);
+ p1.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
+ p2.sort(Comparator.comparing(o -> TextUtils.join(",", o)));
+ for (int i = 0; i < p1.size(); i++) {
+ String[] a1 = p1.get(i);
+ String[] a2 = p2.get(i);
+ if (a1.length != a2.length) return false;
+ for (int j = 0; j < a1.length; j++) {
+ if (!Objects.equals(a1[j], a2[j])) return false;
+ }
+ }
+ return Objects.equals(mAuthority, other.mAuthority)
+ && Objects.equals(mPkg, other.mPkg);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s, %s: %s)", mAuthority, mPkg.toString(), pathToString(mPaths));
+ }
+
+ private String pathToString(ArraySet<String[]> paths) {
+ return TextUtils.join(", ", paths.stream().map(s -> TextUtils.join("/", s))
+ .collect(Collectors.toList()));
+ }
+ }
+}
diff --git a/com/android/server/slice/SliceManagerService.java b/com/android/server/slice/SliceManagerService.java
index fd0b6f1e..b7b96126 100644
--- a/com/android/server/slice/SliceManagerService.java
+++ b/com/android/server/slice/SliceManagerService.java
@@ -31,14 +31,15 @@ import android.app.AppOpsManager;
import android.app.ContentProviderHolder;
import android.app.IActivityManager;
import android.app.slice.ISliceManager;
-import android.app.slice.SliceManager;
import android.app.slice.SliceSpec;
import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.net.Uri;
@@ -51,7 +52,6 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
-import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml.Encoding;
@@ -72,7 +72,6 @@ import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -91,13 +90,9 @@ public class SliceManagerService extends ISliceManager.Stub {
@GuardedBy("mLock")
private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>();
- @GuardedBy("mLock")
- private final ArraySet<SliceGrant> mUserGrants = new ArraySet<>();
private final Handler mHandler;
- @GuardedBy("mSliceAccessFile")
- private final AtomicFile mSliceAccessFile;
- @GuardedBy("mAccessList")
- private final SliceFullAccessList mAccessList;
+
+ private final SlicePermissionManager mPermissions;
private final UsageStatsManagerInternal mAppUsageStats;
public SliceManagerService(Context context) {
@@ -113,24 +108,9 @@ public class SliceManagerService extends ISliceManager.Stub {
mAssistUtils = new AssistUtils(context);
mHandler = new Handler(looper);
- final File systemDir = new File(Environment.getDataDirectory(), "system");
- mSliceAccessFile = new AtomicFile(new File(systemDir, "slice_access.xml"));
- mAccessList = new SliceFullAccessList(mContext);
mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
- synchronized (mSliceAccessFile) {
- if (!mSliceAccessFile.exists()) return;
- try {
- InputStream input = mSliceAccessFile.openRead();
- XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
- parser.setInput(input, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.readXml(parser);
- }
- } catch (IOException | XmlPullParserException e) {
- Slog.d(TAG, "Can't read slice access file", e);
- }
- }
+ mPermissions = new SlicePermissionManager(mContext, looper);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
@@ -211,26 +191,58 @@ public class SliceManagerService extends ISliceManager.Stub {
}
@Override
+ public void grantSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
+ verifyCaller(pkg);
+ int user = Binder.getCallingUserHandle().getIdentifier();
+ enforceOwner(pkg, uri, user);
+ mPermissions.grantSliceAccess(toPkg, user, pkg, user, uri);
+ }
+
+ @Override
+ public void revokeSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
+ verifyCaller(pkg);
+ int user = Binder.getCallingUserHandle().getIdentifier();
+ enforceOwner(pkg, uri, user);
+ mPermissions.revokeSliceAccess(toPkg, user, pkg, user, uri);
+ }
+
+ @Override
public int checkSlicePermission(Uri uri, String pkg, int pid, int uid,
- String[] autoGrantPermissions) throws RemoteException {
- if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
- == PERMISSION_GRANTED) {
- return SliceManager.PERMISSION_GRANTED;
- }
- if (hasFullSliceAccess(pkg, UserHandle.getUserId(uid))) {
- return SliceManager.PERMISSION_GRANTED;
- }
- for (String perm : autoGrantPermissions) {
- if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
- return SliceManager.PERMISSION_USER_GRANTED;
+ String[] autoGrantPermissions) {
+ int userId = UserHandle.getUserId(uid);
+ if (pkg == null) {
+ for (String p : mContext.getPackageManager().getPackagesForUid(uid)) {
+ if (checkSlicePermission(uri, p, pid, uid, autoGrantPermissions)
+ == PERMISSION_GRANTED) {
+ return PERMISSION_GRANTED;
+ }
}
- }
- synchronized (mLock) {
- if (mUserGrants.contains(new SliceGrant(uri, pkg, UserHandle.getUserId(uid)))) {
- return SliceManager.PERMISSION_USER_GRANTED;
+ return PERMISSION_DENIED;
+ }
+ if (hasFullSliceAccess(pkg, userId)) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ if (mPermissions.hasPermission(pkg, userId, uri)) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ if (autoGrantPermissions != null) {
+ // Need to own the Uri to call in with permissions to grant.
+ enforceOwner(pkg, uri, userId);
+ for (String perm : autoGrantPermissions) {
+ if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
+ int providerUser = ContentProvider.getUserIdFromUri(uri, userId);
+ String providerPkg = getProviderPkg(uri, providerUser);
+ mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, uri);
+ return PackageManager.PERMISSION_GRANTED;
+ }
}
}
- return SliceManager.PERMISSION_DENIED;
+ // Fallback to allowing uri permissions through.
+ if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ == PERMISSION_GRANTED) {
+ return PackageManager.PERMISSION_GRANTED;
+ }
+ return PackageManager.PERMISSION_DENIED;
}
@Override
@@ -238,16 +250,17 @@ public class SliceManagerService extends ISliceManager.Stub {
verifyCaller(callingPkg);
getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS,
"Slice granting requires MANAGE_SLICE_PERMISSIONS");
+ int userId = Binder.getCallingUserHandle().getIdentifier();
if (allSlices) {
- synchronized (mAccessList) {
- mAccessList.grantFullAccess(pkg, Binder.getCallingUserHandle().getIdentifier());
- }
- mHandler.post(mSaveAccessList);
+ mPermissions.grantFullAccess(pkg, userId);
} else {
- synchronized (mLock) {
- mUserGrants.add(new SliceGrant(uri, pkg,
- Binder.getCallingUserHandle().getIdentifier()));
- }
+ // When granting, grant to all slices in the provider.
+ Uri grantUri = uri.buildUpon()
+ .path("")
+ .build();
+ int providerUser = ContentProvider.getUserIdFromUri(grantUri, userId);
+ String providerPkg = getProviderPkg(grantUri, providerUser);
+ mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, grantUri);
}
long ident = Binder.clearCallingIdentity();
try {
@@ -268,19 +281,17 @@ public class SliceManagerService extends ISliceManager.Stub {
Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user);
return null;
}
- synchronized(mSliceAccessFile) {
- final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
- out.setOutput(baos, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.writeXml(out, user);
- }
- out.flush();
- return baos.toByteArray();
- } catch (IOException | XmlPullParserException e) {
- Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
- }
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+ out.setOutput(baos, Encoding.UTF_8.name());
+
+ mPermissions.writeBackup(out);
+
+ out.flush();
+ return baos.toByteArray();
+ } catch (IOException | XmlPullParserException e) {
+ Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
}
return null;
}
@@ -299,27 +310,21 @@ public class SliceManagerService extends ISliceManager.Stub {
Slog.w(TAG, "applyRestore: cannot restore policy for user " + user);
return;
}
- synchronized(mSliceAccessFile) {
- final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
- try {
- XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
- parser.setInput(bais, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.readXml(parser);
- }
- mHandler.post(mSaveAccessList);
- } catch (NumberFormatException | XmlPullParserException | IOException e) {
- Slog.w(TAG, "applyRestore: error reading payload", e);
- }
+ final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
+ try {
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(bais, Encoding.UTF_8.name());
+ mPermissions.readRestore(parser);
+ } catch (NumberFormatException | XmlPullParserException | IOException e) {
+ Slog.w(TAG, "applyRestore: error reading payload", e);
}
}
/// ----- internal code -----
- private void removeFullAccess(String pkg, int userId) {
- synchronized (mAccessList) {
- mAccessList.removeGrant(pkg, userId);
+ private void enforceOwner(String pkg, Uri uri, int user) {
+ if (!Objects.equals(getProviderPkg(uri, user), pkg) || pkg == null) {
+ throw new SecurityException("Caller must own " + uri);
}
- mHandler.post(mSaveAccessList);
}
protected void removePinnedSlice(Uri uri) {
@@ -368,19 +373,7 @@ public class SliceManagerService extends ISliceManager.Stub {
}
protected int checkAccess(String pkg, Uri uri, int uid, int pid) {
- int user = UserHandle.getUserId(uid);
- // Check for default launcher/assistant.
- if (!hasFullSliceAccess(pkg, user)) {
- // Also allow things with uri access.
- if (getContext().checkUriPermission(uri, pid, uid,
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != PERMISSION_GRANTED) {
- // Last fallback (if the calling app owns the authority, then it can have access).
- if (!Objects.equals(getProviderPkg(uri, user), pkg)) {
- return PERMISSION_DENIED;
- }
- }
- }
- return PERMISSION_GRANTED;
+ return checkSlicePermission(uri, pkg, uid, pid, null);
}
private String getProviderPkg(Uri uri, int user) {
@@ -425,15 +418,11 @@ public class SliceManagerService extends ISliceManager.Stub {
private void enforceAccess(String pkg, Uri uri) throws RemoteException {
if (checkAccess(pkg, uri, Binder.getCallingUid(), Binder.getCallingPid())
!= PERMISSION_GRANTED) {
- throw new SecurityException("Access to slice " + uri + " is required");
- }
- enforceCrossUser(pkg, uri);
- }
-
- private void enforceFullAccess(String pkg, String name, Uri uri) {
- int user = Binder.getCallingUserHandle().getIdentifier();
- if (!hasFullSliceAccess(pkg, user)) {
- throw new SecurityException(String.format("Call %s requires full slice access", name));
+ int userId = ContentProvider.getUserIdFromUri(uri,
+ Binder.getCallingUserHandle().getIdentifier());
+ if (!Objects.equals(pkg, getProviderPkg(uri, userId))) {
+ throw new SecurityException("Access to slice " + uri + " is required");
+ }
}
enforceCrossUser(pkg, uri);
}
@@ -513,9 +502,7 @@ public class SliceManagerService extends ISliceManager.Stub {
}
private boolean isGrantedFullAccess(String pkg, int userId) {
- synchronized (mAccessList) {
- return mAccessList.hasFullAccess(pkg, userId);
- }
+ return mPermissions.hasFullAccess(pkg, userId);
}
private static ServiceThread createHandler() {
@@ -525,34 +512,6 @@ public class SliceManagerService extends ISliceManager.Stub {
return handlerThread;
}
- private final Runnable mSaveAccessList = new Runnable() {
- @Override
- public void run() {
- synchronized (mSliceAccessFile) {
- final FileOutputStream stream;
- try {
- stream = mSliceAccessFile.startWrite();
- } catch (IOException e) {
- Slog.w(TAG, "Failed to save access file", e);
- return;
- }
-
- try {
- XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
- out.setOutput(stream, Encoding.UTF_8.name());
- synchronized (mAccessList) {
- mAccessList.writeXml(out, UserHandle.USER_ALL);
- }
- out.flush();
- mSliceAccessFile.finishWrite(stream);
- } catch (IOException | XmlPullParserException e) {
- Slog.w(TAG, "Failed to save access file, restoring backup", e);
- mSliceAccessFile.failWrite(stream);
- }
- }
- }
- };
-
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -572,11 +531,11 @@ public class SliceManagerService extends ISliceManager.Stub {
final boolean replacing =
intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
if (!replacing) {
- removeFullAccess(pkg, userId);
+ mPermissions.removePkg(pkg, userId);
}
break;
case Intent.ACTION_PACKAGE_DATA_CLEARED:
- removeFullAccess(pkg, userId);
+ mPermissions.removePkg(pkg, userId);
break;
}
}
diff --git a/com/android/server/slice/SlicePermissionManager.java b/com/android/server/slice/SlicePermissionManager.java
new file mode 100644
index 00000000..d25ec89e
--- /dev/null
+++ b/com/android/server/slice/SlicePermissionManager.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.android.server.slice;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml.Encoding;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.XmlUtils;
+import com.android.server.slice.SliceProviderPermissions.SliceAuthority;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+public class SlicePermissionManager implements DirtyTracker {
+
+ private static final String TAG = "SlicePermissionManager";
+
+ /**
+ * The amount of time we'll cache a SliceProviderPermissions or SliceClientPermissions
+ * in case they are used again.
+ */
+ private static final long PERMISSION_CACHE_PERIOD = 5 * DateUtils.MINUTE_IN_MILLIS;
+
+ /**
+ * The amount of time we delay flushing out permission changes to disk because they usually
+ * come in short bursts.
+ */
+ private static final long WRITE_GRACE_PERIOD = 500;
+
+ private static final String SLICE_DIR = "slice";
+
+ // If/when this bumps again we'll need to write it out in the disk somewhere.
+ // Currently we don't have a central file for this in version 2 and there is no
+ // reason to add one until we actually have incompatible version bumps.
+ // This does however block us from reading backups from P-DP1 which may contain
+ // a very different XML format for perms.
+ static final int DB_VERSION = 2;
+
+ private static final String TAG_LIST = "slice-access-list";
+ private final String ATT_VERSION = "version";
+
+ private final File mSliceDir;
+ private final Context mContext;
+ private final Handler mHandler;
+ private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>();
+ private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>();
+ private final ArraySet<Persistable> mDirty = new ArraySet<>();
+
+ @VisibleForTesting
+ SlicePermissionManager(Context context, Looper looper, File sliceDir) {
+ mContext = context;
+ mHandler = new H(looper);
+ mSliceDir = sliceDir;
+ }
+
+ public SlicePermissionManager(Context context, Looper looper) {
+ this(context, looper, new File(Environment.getDataDirectory(), "system/" + SLICE_DIR));
+ }
+
+ public void grantFullAccess(String pkg, int userId) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ SliceClientPermissions client = getClient(pkgUser);
+ client.setHasFullAccess(true);
+ }
+
+ public void grantSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
+ Uri uri) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
+
+ SliceClientPermissions client = getClient(pkgUser);
+ client.grantUri(uri, providerPkgUser);
+
+ SliceProviderPermissions provider = getProvider(providerPkgUser);
+ provider.getOrCreateAuthority(ContentProvider.getUriWithoutUserId(uri).getAuthority())
+ .addPkg(pkgUser);
+ }
+
+ public void revokeSliceAccess(String pkg, int userId, String providerPkg, int providerUser,
+ Uri uri) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ PkgUser providerPkgUser = new PkgUser(providerPkg, providerUser);
+
+ SliceClientPermissions client = getClient(pkgUser);
+ client.revokeUri(uri, providerPkgUser);
+ }
+
+ public void removePkg(String pkg, int userId) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ SliceProviderPermissions provider = getProvider(pkgUser);
+
+ for (SliceAuthority authority : provider.getAuthorities()) {
+ for (PkgUser p : authority.getPkgs()) {
+ getClient(p).removeAuthority(authority.getAuthority(), userId);
+ }
+ }
+ SliceClientPermissions client = getClient(pkgUser);
+ client.clear();
+ mHandler.obtainMessage(H.MSG_REMOVE, pkgUser);
+ }
+
+ public boolean hasFullAccess(String pkg, int userId) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ return getClient(pkgUser).hasFullAccess();
+ }
+
+ public boolean hasPermission(String pkg, int userId, Uri uri) {
+ PkgUser pkgUser = new PkgUser(pkg, userId);
+ SliceClientPermissions client = getClient(pkgUser);
+ int providerUserId = ContentProvider.getUserIdFromUri(uri, userId);
+ return client.hasFullAccess()
+ || client.hasPermission(ContentProvider.getUriWithoutUserId(uri), providerUserId);
+ }
+
+ @Override
+ public void onPersistableDirty(Persistable obj) {
+ mHandler.removeMessages(H.MSG_PERSIST);
+ mHandler.obtainMessage(H.MSG_ADD_DIRTY, obj).sendToTarget();
+ mHandler.sendEmptyMessageDelayed(H.MSG_PERSIST, WRITE_GRACE_PERIOD);
+ }
+
+ public void writeBackup(XmlSerializer out) throws IOException, XmlPullParserException {
+ synchronized (this) {
+ out.startTag(null, TAG_LIST);
+ out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
+
+ // Don't do anything with changes from the backup, because there shouldn't be any.
+ DirtyTracker tracker = obj -> { };
+ if (mHandler.hasMessages(H.MSG_PERSIST)) {
+ mHandler.removeMessages(H.MSG_PERSIST);
+ handlePersist();
+ }
+ for (String file : new File(mSliceDir.getAbsolutePath()).list()) {
+ if (file.isEmpty()) continue;
+ try (ParserHolder parser = getParser(file)) {
+ Persistable p;
+ while (parser.parser.getEventType() != XmlPullParser.START_TAG) {
+ parser.parser.next();
+ }
+ if (SliceClientPermissions.TAG_CLIENT.equals(parser.parser.getName())) {
+ p = SliceClientPermissions.createFrom(parser.parser, tracker);
+ } else {
+ p = SliceProviderPermissions.createFrom(parser.parser, tracker);
+ }
+ p.writeTo(out);
+ }
+ }
+
+ out.endTag(null, TAG_LIST);
+ }
+ }
+
+ public void readRestore(XmlPullParser parser) throws IOException, XmlPullParserException {
+ synchronized (this) {
+ while ((parser.getEventType() != XmlPullParser.START_TAG
+ || !TAG_LIST.equals(parser.getName()))
+ && parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ parser.next();
+ }
+ int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0);
+ if (xmlVersion < DB_VERSION) {
+ // No conversion support right now.
+ return;
+ }
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ if (SliceClientPermissions.TAG_CLIENT.equals(parser.getName())) {
+ SliceClientPermissions client = SliceClientPermissions.createFrom(parser,
+ this);
+ synchronized (mCachedClients) {
+ mCachedClients.put(client.getPkg(), client);
+ }
+ onPersistableDirty(client);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, client.getPkg()),
+ PERMISSION_CACHE_PERIOD);
+ } else if (SliceProviderPermissions.TAG_PROVIDER.equals(parser.getName())) {
+ SliceProviderPermissions provider = SliceProviderPermissions.createFrom(
+ parser, this);
+ synchronized (mCachedProviders) {
+ mCachedProviders.put(provider.getPkg(), provider);
+ }
+ onPersistableDirty(provider);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, provider.getPkg()),
+ PERMISSION_CACHE_PERIOD);
+ } else {
+ parser.next();
+ }
+ } else {
+ parser.next();
+ }
+ }
+ }
+ }
+
+ private SliceClientPermissions getClient(PkgUser pkgUser) {
+ SliceClientPermissions client;
+ synchronized (mCachedClients) {
+ client = mCachedClients.get(pkgUser);
+ }
+ if (client == null) {
+ try (ParserHolder parser = getParser(SliceClientPermissions.getFileName(pkgUser))) {
+ client = SliceClientPermissions.createFrom(parser.parser, this);
+ synchronized (mCachedClients) {
+ mCachedClients.put(pkgUser, client);
+ }
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_CLIENT, pkgUser),
+ PERMISSION_CACHE_PERIOD);
+ return client;
+ } catch (FileNotFoundException e) {
+ // No client exists yet.
+ } catch (IOException e) {
+ Log.e(TAG, "Can't read client", e);
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Can't read client", e);
+ }
+ // Can't read or no permissions exist, create a clean object.
+ client = new SliceClientPermissions(pkgUser, this);
+ }
+ return client;
+ }
+
+ private SliceProviderPermissions getProvider(PkgUser pkgUser) {
+ SliceProviderPermissions provider;
+ synchronized (mCachedProviders) {
+ provider = mCachedProviders.get(pkgUser);
+ }
+ if (provider == null) {
+ try (ParserHolder parser = getParser(SliceProviderPermissions.getFileName(pkgUser))) {
+ provider = SliceProviderPermissions.createFrom(parser.parser, this);
+ synchronized (mCachedProviders) {
+ mCachedProviders.put(pkgUser, provider);
+ }
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(H.MSG_CLEAR_PROVIDER, pkgUser),
+ PERMISSION_CACHE_PERIOD);
+ return provider;
+ } catch (FileNotFoundException e) {
+ // No provider exists yet.
+ } catch (IOException e) {
+ Log.e(TAG, "Can't read provider", e);
+ } catch (XmlPullParserException e) {
+ Log.e(TAG, "Can't read provider", e);
+ }
+ // Can't read or no permissions exist, create a clean object.
+ provider = new SliceProviderPermissions(pkgUser, this);
+ }
+ return provider;
+ }
+
+ private ParserHolder getParser(String fileName)
+ throws FileNotFoundException, XmlPullParserException {
+ AtomicFile file = getFile(fileName);
+ ParserHolder holder = new ParserHolder();
+ holder.input = file.openRead();
+ holder.parser = XmlPullParserFactory.newInstance().newPullParser();
+ holder.parser.setInput(holder.input, Encoding.UTF_8.name());
+ return holder;
+ }
+
+ private AtomicFile getFile(String fileName) {
+ if (!mSliceDir.exists()) {
+ mSliceDir.mkdir();
+ }
+ return new AtomicFile(new File(mSliceDir, fileName));
+ }
+
+ private void handlePersist() {
+ synchronized (this) {
+ for (Persistable persistable : mDirty) {
+ AtomicFile file = getFile(persistable.getFileName());
+ final FileOutputStream stream;
+ try {
+ stream = file.startWrite();
+ } catch (IOException e) {
+ Slog.w(TAG, "Failed to save access file", e);
+ return;
+ }
+
+ try {
+ XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
+ out.setOutput(stream, Encoding.UTF_8.name());
+
+ persistable.writeTo(out);
+
+ out.flush();
+ file.finishWrite(stream);
+ } catch (IOException | XmlPullParserException e) {
+ Slog.w(TAG, "Failed to save access file, restoring backup", e);
+ file.failWrite(stream);
+ }
+ }
+ mDirty.clear();
+ }
+ }
+
+ private void handleRemove(PkgUser pkgUser) {
+ getFile(SliceClientPermissions.getFileName(pkgUser)).delete();
+ getFile(SliceProviderPermissions.getFileName(pkgUser)).delete();
+ mDirty.remove(mCachedClients.remove(pkgUser));
+ mDirty.remove(mCachedProviders.remove(pkgUser));
+ }
+
+ private final class H extends Handler {
+ private static final int MSG_ADD_DIRTY = 1;
+ private static final int MSG_PERSIST = 2;
+ private static final int MSG_REMOVE = 3;
+ private static final int MSG_CLEAR_CLIENT = 4;
+ private static final int MSG_CLEAR_PROVIDER = 5;
+
+ public H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ADD_DIRTY:
+ mDirty.add((Persistable) msg.obj);
+ break;
+ case MSG_PERSIST:
+ handlePersist();
+ break;
+ case MSG_REMOVE:
+ handleRemove((PkgUser) msg.obj);
+ break;
+ case MSG_CLEAR_CLIENT:
+ synchronized (mCachedClients) {
+ mCachedClients.remove(msg.obj);
+ }
+ break;
+ case MSG_CLEAR_PROVIDER:
+ synchronized (mCachedProviders) {
+ mCachedProviders.remove(msg.obj);
+ }
+ break;
+ }
+ }
+ }
+
+ public static class PkgUser {
+ private static final String SEPARATOR = "@";
+ private static final String FORMAT = "%s" + SEPARATOR + "%d";
+ private final String mPkg;
+ private final int mUserId;
+
+ public PkgUser(String pkg, int userId) {
+ mPkg = pkg;
+ mUserId = userId;
+ }
+
+ public PkgUser(String pkgUserStr) throws IllegalArgumentException {
+ try {
+ String[] vals = pkgUserStr.split(SEPARATOR, 2);
+ mPkg = vals[0];
+ mUserId = Integer.parseInt(vals[1]);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public String getPkg() {
+ return mPkg;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ @Override
+ public int hashCode() {
+ return mPkg.hashCode() + mUserId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+ PkgUser other = (PkgUser) obj;
+ return Objects.equals(other.mPkg, mPkg) && other.mUserId == mUserId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(FORMAT, mPkg, mUserId);
+ }
+ }
+
+ private class ParserHolder implements AutoCloseable {
+
+ private InputStream input;
+ private XmlPullParser parser;
+
+ @Override
+ public void close() throws IOException {
+ input.close();
+ }
+ }
+}
diff --git a/com/android/server/slice/SliceProviderPermissions.java b/com/android/server/slice/SliceProviderPermissions.java
new file mode 100644
index 00000000..6e602d59
--- /dev/null
+++ b/com/android/server/slice/SliceProviderPermissions.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 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.android.server.slice;
+
+import android.annotation.NonNull;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.server.slice.DirtyTracker.Persistable;
+import com.android.server.slice.SlicePermissionManager.PkgUser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+
+public class SliceProviderPermissions implements DirtyTracker, Persistable {
+
+ private static final String TAG = "SliceProviderPermissions";
+
+ static final String TAG_PROVIDER = "provider";
+ private static final String TAG_AUTHORITY = "authority";
+ private static final String TAG_PKG = "pkg";
+ private static final String NAMESPACE = null;
+
+ private static final String ATTR_PKG = "pkg";
+ private static final String ATTR_AUTHORITY = "authority";
+
+ private final PkgUser mPkg;
+ private final ArrayMap<String, SliceAuthority> mAuths = new ArrayMap<>();
+ private final DirtyTracker mTracker;
+
+ public SliceProviderPermissions(@NonNull PkgUser pkg, @NonNull DirtyTracker tracker) {
+ mPkg = pkg;
+ mTracker = tracker;
+ }
+
+ public PkgUser getPkg() {
+ return mPkg;
+ }
+
+ public synchronized Collection<SliceAuthority> getAuthorities() {
+ return new ArrayList<>(mAuths.values());
+ }
+
+ public synchronized SliceAuthority getOrCreateAuthority(String authority) {
+ SliceAuthority ret = mAuths.get(authority);
+ if (ret == null) {
+ ret = new SliceAuthority(authority, this);
+ mAuths.put(authority, ret);
+ onPersistableDirty(ret);
+ }
+ return ret;
+ }
+
+ @Override
+ public void onPersistableDirty(Persistable obj) {
+ mTracker.onPersistableDirty(this);
+ }
+
+ @Override
+ public String getFileName() {
+ return getFileName(mPkg);
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ out.startTag(NAMESPACE, TAG_PROVIDER);
+ out.attribute(NAMESPACE, ATTR_PKG, mPkg.toString());
+
+ final int N = mAuths.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_AUTHORITY);
+ out.attribute(NAMESPACE, ATTR_AUTHORITY, mAuths.valueAt(i).mAuthority);
+
+ mAuths.valueAt(i).writeTo(out);
+
+ out.endTag(NAMESPACE, TAG_AUTHORITY);
+ }
+
+ out.endTag(NAMESPACE, TAG_PROVIDER);
+ }
+
+ public static SliceProviderPermissions createFrom(XmlPullParser parser, DirtyTracker tracker)
+ throws XmlPullParserException, IOException {
+ // Get to the beginning of the provider.
+ while (parser.getEventType() != XmlPullParser.START_TAG
+ || !TAG_PROVIDER.equals(parser.getName())) {
+ parser.next();
+ }
+ int depth = parser.getDepth();
+ PkgUser pkgUser = new PkgUser(parser.getAttributeValue(NAMESPACE, ATTR_PKG));
+ SliceProviderPermissions provider = new SliceProviderPermissions(pkgUser, tracker);
+ parser.next();
+
+ while (parser.getDepth() > depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_AUTHORITY.equals(parser.getName())) {
+ try {
+ SliceAuthority authority = new SliceAuthority(
+ parser.getAttributeValue(NAMESPACE, ATTR_AUTHORITY), provider);
+ authority.readFrom(parser);
+ provider.mAuths.put(authority.getAuthority(), authority);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Couldn't read PkgUser", e);
+ }
+ }
+
+ parser.next();
+ }
+ return provider;
+ }
+
+ public static String getFileName(PkgUser pkg) {
+ return String.format("provider_%s", pkg.toString());
+ }
+
+ public static class SliceAuthority implements Persistable {
+ private final String mAuthority;
+ private final DirtyTracker mTracker;
+ private final ArraySet<PkgUser> mPkgs = new ArraySet<>();
+
+ public SliceAuthority(String authority, DirtyTracker tracker) {
+ mAuthority = authority;
+ mTracker = tracker;
+ }
+
+ public String getAuthority() {
+ return mAuthority;
+ }
+
+ public synchronized void addPkg(PkgUser pkg) {
+ if (mPkgs.add(pkg)) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized void removePkg(PkgUser pkg) {
+ if (mPkgs.remove(pkg)) {
+ mTracker.onPersistableDirty(this);
+ }
+ }
+
+ public synchronized Collection<PkgUser> getPkgs() {
+ return new ArraySet<>(mPkgs);
+ }
+
+ @Override
+ public String getFileName() {
+ return null;
+ }
+
+ public synchronized void writeTo(XmlSerializer out) throws IOException {
+ final int N = mPkgs.size();
+ for (int i = 0; i < N; i++) {
+ out.startTag(NAMESPACE, TAG_PKG);
+ out.text(mPkgs.valueAt(i).toString());
+ out.endTag(NAMESPACE, TAG_PKG);
+ }
+ }
+
+ public synchronized void readFrom(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ parser.next();
+ int depth = parser.getDepth();
+ while (parser.getDepth() >= depth) {
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && TAG_PKG.equals(parser.getName())) {
+ mPkgs.add(new PkgUser(parser.nextText()));
+ }
+ parser.next();
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!getClass().equals(obj != null ? obj.getClass() : null)) return false;
+ SliceAuthority other = (SliceAuthority) obj;
+ return Objects.equals(mAuthority, other.mAuthority)
+ && Objects.equals(mPkgs, other.mPkgs);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s: %s)", mAuthority, mPkgs.toString());
+ }
+ }
+}
diff --git a/com/android/server/soundtrigger/SoundTriggerService.java b/com/android/server/soundtrigger/SoundTriggerService.java
index 11609435..cd524a51 100644
--- a/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/com/android/server/soundtrigger/SoundTriggerService.java
@@ -926,25 +926,24 @@ public class SoundTriggerService extends SystemService {
Slog.w(TAG, mPuuid + ": Dropped operation as too many operations were "
+ "run in last 24 hours");
}
- return;
- }
-
- mNumOps.addOp(currentTime);
+ } else {
+ mNumOps.addOp(currentTime);
- // Find a free opID
- int opId = mNumTotalOpsPerformed;
- do {
- mNumTotalOpsPerformed++;
- } while (mRunningOpIds.contains(opId));
+ // Find a free opID
+ int opId = mNumTotalOpsPerformed;
+ do {
+ mNumTotalOpsPerformed++;
+ } while (mRunningOpIds.contains(opId));
- // Run OP
- try {
- if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
+ // Run OP
+ try {
+ if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
- op.run(opId, mService);
- mRunningOpIds.add(opId);
- } catch (Exception e) {
- Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+ op.run(opId, mService);
+ mRunningOpIds.add(opId);
+ } catch (Exception e) {
+ Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+ }
}
// Unbind from service if no operations are left (i.e. if the operation failed)
diff --git a/com/android/server/stats/StatsCompanionService.java b/com/android/server/stats/StatsCompanionService.java
index fae0b24f..5f2ac4fe 100644
--- a/com/android/server/stats/StatsCompanionService.java
+++ b/com/android/server/stats/StatsCompanionService.java
@@ -273,7 +273,8 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
// Add in all the apps for every user/profile.
for (UserInfo profile : users) {
- List<PackageInfo> pi = pm.getInstalledPackagesAsUser(0, profile.id);
+ List<PackageInfo> pi =
+ pm.getInstalledPackagesAsUser(PackageManager.MATCH_DISABLED_COMPONENTS, profile.id);
for (int j = 0; j < pi.size(); j++) {
if (pi.get(j).applicationInfo != null) {
uids.add(pi.get(j).applicationInfo.uid);
@@ -379,7 +380,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG)
- Slog.d(TAG, "Time to poll something.");
+ Slog.d(TAG, "Time to trigger periodic alarm.");
synchronized (sStatsdLock) {
if (sStatsd == null) {
Slog.w(TAG, "Could not access statsd to inform it of periodic alarm firing.");
@@ -455,13 +456,13 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
public void setAlarmForSubscriberTriggering(long timestampMs) {
enforceCallingPermission();
if (DEBUG)
- Slog.d(TAG, "Setting periodic alarm at " + timestampMs);
+ Slog.d(TAG, "Setting periodic alarm in about " +
+ (timestampMs - SystemClock.elapsedRealtime()));
final long callingToken = Binder.clearCallingIdentity();
try {
// using ELAPSED_REALTIME, not ELAPSED_REALTIME_WAKEUP, so if device is asleep, will
// only fire when it awakens.
- // This alarm is inexact, leaving its exactness completely up to the OS optimizations.
- mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, timestampMs, mPeriodicAlarmIntent);
+ mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, timestampMs, mPeriodicAlarmIntent);
} finally {
Binder.restoreCallingIdentity(callingToken);
}
diff --git a/com/android/server/statusbar/StatusBarManagerService.java b/com/android/server/statusbar/StatusBarManagerService.java
index 8af1101a..36fa868b 100644
--- a/com/android/server/statusbar/StatusBarManagerService.java
+++ b/com/android/server/statusbar/StatusBarManagerService.java
@@ -23,7 +23,7 @@ import android.app.StatusBarManager;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.IBiometricPromptReceiver;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -547,7 +547,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
- public void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) {
+ public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) {
if (mBar != null) {
try {
mBar.showFingerprintDialog(bundle, receiver);
@@ -1096,6 +1096,30 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
+ public void onNotificationSmartRepliesAdded(String key, int replyCount)
+ throws RemoteException {
+ enforceStatusBarService();
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mNotificationDelegate.onNotificationSmartRepliesAdded(key, replyCount);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onNotificationSmartReplySent(String key, int replyIndex)
+ throws RemoteException {
+ enforceStatusBarService();
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mNotificationDelegate.onNotificationSmartReplySent(key, replyIndex);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void onNotificationSettingsViewed(String key) throws RemoteException {
enforceStatusBarService();
long identity = Binder.clearCallingIdentity();
diff --git a/com/android/server/usb/UsbDebuggingManager.java b/com/android/server/usb/UsbDebuggingManager.java
index 74d8e129..3b085053 100644
--- a/com/android/server/usb/UsbDebuggingManager.java
+++ b/com/android/server/usb/UsbDebuggingManager.java
@@ -461,7 +461,7 @@ public class UsbDebuggingManager {
long token = dump.start(idName, id);
dump.write("connected_to_adb", UsbDebuggingManagerProto.CONNECTED_TO_ADB, mThread != null);
- writeStringIfNotNull(dump, "last_key_received", UsbDebuggingManagerProto.LAST_KEY_RECEVIED,
+ writeStringIfNotNull(dump, "last_key_received", UsbDebuggingManagerProto.LAST_KEY_RECEIVED,
mFingerprints);
try {
diff --git a/com/android/server/wifi/ScanRequestProxy.java b/com/android/server/wifi/ScanRequestProxy.java
index d4c9b3ef..b9e48ea5 100644
--- a/com/android/server/wifi/ScanRequestProxy.java
+++ b/com/android/server/wifi/ScanRequestProxy.java
@@ -34,6 +34,8 @@ import com.android.server.wifi.util.WifiPermissionsUtil;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import javax.annotation.concurrent.NotThreadSafe;
@@ -51,15 +53,21 @@ import javax.annotation.concurrent.NotThreadSafe;
* c) Will send out the {@link WifiManager#SCAN_RESULTS_AVAILABLE_ACTION} broadcast when new
* scan results are available.
* d) Throttle scan requests from non-setting apps:
- * d.1) For foreground apps, throttle to a max of 1 scan per app every 30 seconds.
- * d.2) For background apps, throttle to a max of 1 scan from any app every 30 minutes.
+ * a) Each foreground app can request a max of
+ * {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS} scan every
+ * {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS}.
+ * b) Background apps combined can request 1 scan every
+ * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
* Note: This class is not thread-safe. It needs to be invoked from WifiStateMachine thread only.
*/
@NotThreadSafe
public class ScanRequestProxy {
private static final String TAG = "WifiScanRequestProxy";
+
+ @VisibleForTesting
+ public static final int SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS = 120 * 1000;
@VisibleForTesting
- public static final int SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS = 30 * 1000;
+ public static final int SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS = 4;
@VisibleForTesting
public static final int SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS = 30 * 60 * 1000;
@@ -81,8 +89,8 @@ public class ScanRequestProxy {
private boolean mIsScanProcessingComplete = true;
// Timestamps for the last scan requested by any background app.
private long mLastScanTimestampForBgApps = 0;
- // Timestamps for the last scan requested by each foreground app.
- private final ArrayMap<String, Long> mLastScanTimestampsForFgApps = new ArrayMap();
+ // Timestamps for the list of last few scan requests by each foreground app.
+ private final ArrayMap<String, LinkedList<Long>> mLastScanTimestampsForFgApps = new ArrayMap();
// Scan results cached from the last full single scan request.
private final List<ScanResult> mLastScanResults = new ArrayList<>();
// Common scan listener for scan requests.
@@ -223,19 +231,47 @@ public class ScanRequestProxy {
}
}
+ private void trimPastScanRequestTimesForForegroundApp(
+ List<Long> scanRequestTimestamps, long currentTimeMillis) {
+ Iterator<Long> timestampsIter = scanRequestTimestamps.iterator();
+ while (timestampsIter.hasNext()) {
+ Long scanRequestTimeMillis = timestampsIter.next();
+ if ((currentTimeMillis - scanRequestTimeMillis)
+ > SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS) {
+ timestampsIter.remove();
+ } else {
+ // This list is sorted by timestamps, so we can skip any more checks
+ break;
+ }
+ }
+ }
+
+ private LinkedList<Long> getOrCreateScanRequestTimestampsForForegroundApp(String packageName) {
+ LinkedList<Long> scanRequestTimestamps = mLastScanTimestampsForFgApps.get(packageName);
+ if (scanRequestTimestamps == null) {
+ scanRequestTimestamps = new LinkedList<>();
+ mLastScanTimestampsForFgApps.put(packageName, scanRequestTimestamps);
+ }
+ return scanRequestTimestamps;
+ }
+
/**
* Checks if the scan request from the app (specified by packageName) needs
* to be throttled.
+ * The throttle limit allows a max of {@link #SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS}
+ * in {@link #SCAN_REQUEST_THROTTLE_TIME_WINDOW_FG_APPS_MS} window.
*/
private boolean shouldScanRequestBeThrottledForForegroundApp(String packageName) {
- long lastScanMs = mLastScanTimestampsForFgApps.getOrDefault(packageName, 0L);
- long elapsedRealtime = mClock.getElapsedSinceBootMillis();
- if (lastScanMs != 0
- && (elapsedRealtime - lastScanMs) < SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS) {
+ LinkedList<Long> scanRequestTimestamps =
+ getOrCreateScanRequestTimestampsForForegroundApp(packageName);
+ long currentTimeMillis = mClock.getElapsedSinceBootMillis();
+ // First evict old entries from the list.
+ trimPastScanRequestTimesForForegroundApp(scanRequestTimestamps, currentTimeMillis);
+ if (scanRequestTimestamps.size() >= SCAN_REQUEST_THROTTLE_MAX_IN_TIME_WINDOW_FG_APPS) {
return true;
}
// Proceed with the scan request and record the time.
- mLastScanTimestampsForFgApps.put(packageName, elapsedRealtime);
+ scanRequestTimestamps.addLast(currentTimeMillis);
return false;
}
@@ -275,11 +311,6 @@ public class ScanRequestProxy {
/**
* Checks if the scan request from the app (specified by callingUid & packageName) needs
* to be throttled.
- *
- * a) Each foreground app can request 1 scan every
- * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_FG_APPS_MS}.
- * b) Background apps combined can request 1 scan every
- * {@link #SCAN_REQUEST_THROTTLE_INTERVAL_BG_APPS_MS}.
*/
private boolean shouldScanRequestBeThrottledForApp(int callingUid, String packageName) {
boolean isThrottled;
diff --git a/com/android/server/wifi/ScoredNetworkEvaluator.java b/com/android/server/wifi/ScoredNetworkEvaluator.java
index 223423eb..9bb764ea 100644
--- a/com/android/server/wifi/ScoredNetworkEvaluator.java
+++ b/com/android/server/wifi/ScoredNetworkEvaluator.java
@@ -114,13 +114,12 @@ public class ScoredNetworkEvaluator implements WifiNetworkSelector.NetworkEvalua
String packageName = mNetworkScoreManager.getActiveScorerPackage();
if (networkScorerAppData == null || packageName == null) return false;
int uid = networkScorerAppData.packageUid;
- boolean allow;
try {
- allow = mWifiPermissionsUtil.canAccessScanResults(packageName, uid);
+ mWifiPermissionsUtil.enforceCanAccessScanResults(packageName, uid);
+ return true;
} catch (SecurityException e) {
- allow = false;
+ return false;
}
- return allow;
}
@Override
diff --git a/com/android/server/wifi/ScoringParams.java b/com/android/server/wifi/ScoringParams.java
index 0cad2253..5b9951ff 100644
--- a/com/android/server/wifi/ScoringParams.java
+++ b/com/android/server/wifi/ScoringParams.java
@@ -64,6 +64,12 @@ public class ScoringParams {
public static final int MAX_HORIZON = 60;
public int horizon = 15;
+ /** Number 0-10 influencing requests for network unreachability detection */
+ public static final String KEY_NUD = "nud";
+ public static final int MIN_NUD = 0;
+ public static final int MAX_NUD = 10;
+ public int nud = 8;
+
Values() {
}
@@ -78,6 +84,7 @@ public class ScoringParams {
pps[i] = source.pps[i];
}
horizon = source.horizon;
+ nud = source.nud;
}
public void validate() throws IllegalArgumentException {
@@ -85,6 +92,7 @@ public class ScoringParams {
validateRssiArray(rssi5);
validateOrderedNonNegativeArray(pps);
validateRange(horizon, MIN_HORIZON, MAX_HORIZON);
+ validateRange(nud, MIN_NUD, MAX_NUD);
}
private void validateRssiArray(int[] rssi) throws IllegalArgumentException {
@@ -122,6 +130,7 @@ public class ScoringParams {
updateIntArray(rssi5, parser, KEY_RSSI5);
updateIntArray(pps, parser, KEY_PPS);
horizon = updateInt(parser, KEY_HORIZON, horizon);
+ nud = updateInt(parser, KEY_NUD, nud);
}
private int updateInt(KeyValueListParser parser, String key, int defaultValue)
@@ -153,11 +162,12 @@ public class ScoringParams {
appendInts(sb, rssi2);
appendKey(sb, KEY_RSSI5);
appendInts(sb, rssi5);
- //TODO(b/74613347) - leave these out, pending unit test updates
- // appendKey(sb, KEY_PPS);
- // appendInts(sb, pps);
+ appendKey(sb, KEY_PPS);
+ appendInts(sb, pps);
appendKey(sb, KEY_HORIZON);
sb.append(horizon);
+ appendKey(sb, KEY_NUD);
+ sb.append(nud);
return sb.toString();
}
@@ -327,6 +337,21 @@ public class ScoringParams {
return mVal.pps[2];
}
+ /**
+ * Returns a number between 0 and 10 inclusive that indicates
+ * how aggressive to be about asking for IP configuration checks
+ * (also known as Network Unreachabilty Detection, or NUD).
+ *
+ * 0 - no nud checks requested by scorer (framework still checks after roam)
+ * 1 - check when score becomes very low
+ * ...
+ * 10 - check when score first breaches threshold, and again as it gets worse
+ *
+ */
+ public int getNudKnob() {
+ return mVal.nud;
+ }
+
private static final int MINIMUM_5GHZ_BAND_FREQUENCY_IN_MEGAHERTZ = 5000;
private int[] getRssiArray(int frequency) {
diff --git a/com/android/server/wifi/SelfRecovery.java b/com/android/server/wifi/SelfRecovery.java
index d3985f5f..c9e95a77 100644
--- a/com/android/server/wifi/SelfRecovery.java
+++ b/com/android/server/wifi/SelfRecovery.java
@@ -38,11 +38,13 @@ public class SelfRecovery {
*/
public static final int REASON_LAST_RESORT_WATCHDOG = 0;
public static final int REASON_WIFINATIVE_FAILURE = 1;
+ public static final int REASON_STA_IFACE_DOWN = 2;
public static final long MAX_RESTARTS_IN_TIME_WINDOW = 2; // 2 restarts per hour
public static final long MAX_RESTARTS_TIME_WINDOW_MILLIS = 60 * 60 * 1000; // 1 hour
protected static final String[] REASON_STRINGS = {
- "Last Resort Watchdog", // REASON_LAST_RESORT_WATCHDOG
- "WifiNative Failure" // REASON_WIFINATIVE_FAILURE
+ "Last Resort Watchdog", // REASON_LAST_RESORT_WATCHDOG
+ "WifiNative Failure", // REASON_WIFINATIVE_FAILURE
+ "Sta Interface Down" // REASON_STA_IFACE_DOWN
};
private final WifiController mWifiController;
@@ -59,28 +61,39 @@ public class SelfRecovery {
* Trigger recovery.
*
* This method does the following:
- * 1. Raises a wtf.
- * 2. Sends {@link WifiController#CMD_RESTART_WIFI} to {@link WifiController} to initiate the
- * stack restart.
+ * 1. Checks reason code used to trigger recovery
+ * 2. Checks for sta iface down triggers and disables wifi by sending {@link
+ * WifiController#CMD_RECOVERY_DISABLE_WIFI} to {@link WifiController} to disable wifi.
+ * 3. Throttles restart calls for underlying native failures
+ * 4. Sends {@link WifiController#CMD_RECOVERY_RESTART_WIFI} to {@link WifiController} to
+ * initiate the stack restart.
* @param reason One of the above |REASON_*| codes.
*/
public void trigger(int reason) {
- if (!(reason == REASON_LAST_RESORT_WATCHDOG || reason == REASON_WIFINATIVE_FAILURE)) {
+ if (!(reason == REASON_LAST_RESORT_WATCHDOG || reason == REASON_WIFINATIVE_FAILURE
+ || reason == REASON_STA_IFACE_DOWN)) {
Log.e(TAG, "Invalid trigger reason. Ignoring...");
return;
}
+ if (reason == REASON_STA_IFACE_DOWN) {
+ Log.e(TAG, "STA interface down, disable wifi");
+ mWifiController.sendMessage(WifiController.CMD_RECOVERY_DISABLE_WIFI);
+ return;
+ }
+
Log.e(TAG, "Triggering recovery for reason: " + REASON_STRINGS[reason]);
if (reason == REASON_WIFINATIVE_FAILURE) {
trimPastRestartTimes();
// Ensure there haven't been too many restarts within MAX_RESTARTS_TIME_WINDOW
if (mPastRestartTimes.size() >= MAX_RESTARTS_IN_TIME_WINDOW) {
Log.e(TAG, "Already restarted wifi (" + MAX_RESTARTS_IN_TIME_WINDOW + ") times in"
- + " last (" + MAX_RESTARTS_TIME_WINDOW_MILLIS + "ms ). Ignoring...");
+ + " last (" + MAX_RESTARTS_TIME_WINDOW_MILLIS + "ms ). Disabling wifi");
+ mWifiController.sendMessage(WifiController.CMD_RECOVERY_DISABLE_WIFI);
return;
}
mPastRestartTimes.add(mClock.getElapsedSinceBootMillis());
}
- mWifiController.sendMessage(WifiController.CMD_RESTART_WIFI, reason);
+ mWifiController.sendMessage(WifiController.CMD_RECOVERY_RESTART_WIFI, reason);
}
/**
diff --git a/com/android/server/wifi/VelocityBasedConnectedScore.java b/com/android/server/wifi/VelocityBasedConnectedScore.java
index cb47978d..e7d61f73 100644
--- a/com/android/server/wifi/VelocityBasedConnectedScore.java
+++ b/com/android/server/wifi/VelocityBasedConnectedScore.java
@@ -62,6 +62,7 @@ public class VelocityBasedConnectedScore extends ConnectedScore {
@Override
public void reset() {
mLastMillis = 0;
+ mThresholdAdjustment = 0;
}
/**
@@ -153,8 +154,9 @@ public class VelocityBasedConnectedScore extends ConnectedScore {
if (txSuccessPps < mMinimumPpsForMeasuringSuccess) return;
if (rxSuccessPps < mMinimumPpsForMeasuringSuccess) return;
double txBadPps = wifiInfo.txBadRate;
- double probabilityOfSuccessfulTx = txSuccessPps / (txSuccessPps + txBadPps);
- if (probabilityOfSuccessfulTx >= 0.2) {
+ double txRetriesPps = wifiInfo.txRetriesRate;
+ double probabilityOfSuccessfulTx = txSuccessPps / (txSuccessPps + txBadPps + txRetriesPps);
+ if (probabilityOfSuccessfulTx > 0.2) {
// May want this amount to vary with how close to threshold we are
mThresholdAdjustment -= 0.5;
}
diff --git a/com/android/server/wifi/WakeupConfigStoreData.java b/com/android/server/wifi/WakeupConfigStoreData.java
index b936a4c6..d9856776 100644
--- a/com/android/server/wifi/WakeupConfigStoreData.java
+++ b/com/android/server/wifi/WakeupConfigStoreData.java
@@ -39,12 +39,14 @@ public class WakeupConfigStoreData implements StoreData {
private static final String XML_TAG_FEATURE_STATE_SECTION = "FeatureState";
private static final String XML_TAG_IS_ACTIVE = "IsActive";
private static final String XML_TAG_IS_ONBOARDED = "IsOnboarded";
+ private static final String XML_TAG_NOTIFICATIONS_SHOWN = "NotificationsShown";
private static final String XML_TAG_NETWORK_SECTION = "Network";
private static final String XML_TAG_SSID = "SSID";
private static final String XML_TAG_SECURITY = "Security";
private final DataSource<Boolean> mIsActiveDataSource;
private final DataSource<Boolean> mIsOnboardedDataSource;
+ private final DataSource<Integer> mNotificationsDataSource;
private final DataSource<Set<ScanResultMatchInfo>> mNetworkDataSource;
private boolean mHasBeenRead = false;
@@ -76,9 +78,11 @@ public class WakeupConfigStoreData implements StoreData {
public WakeupConfigStoreData(
DataSource<Boolean> isActiveDataSource,
DataSource<Boolean> isOnboardedDataSource,
+ DataSource<Integer> notificationsDataSource,
DataSource<Set<ScanResultMatchInfo>> networkDataSource) {
mIsActiveDataSource = isActiveDataSource;
mIsOnboardedDataSource = isOnboardedDataSource;
+ mNotificationsDataSource = notificationsDataSource;
mNetworkDataSource = networkDataSource;
}
@@ -116,6 +120,8 @@ public class WakeupConfigStoreData implements StoreData {
XmlUtil.writeNextValue(out, XML_TAG_IS_ACTIVE, mIsActiveDataSource.getData());
XmlUtil.writeNextValue(out, XML_TAG_IS_ONBOARDED, mIsOnboardedDataSource.getData());
+ XmlUtil.writeNextValue(out, XML_TAG_NOTIFICATIONS_SHOWN,
+ mNotificationsDataSource.getData());
XmlUtil.writeNextSectionEnd(out, XML_TAG_FEATURE_STATE_SECTION);
}
@@ -185,6 +191,7 @@ public class WakeupConfigStoreData implements StoreData {
throws IOException, XmlPullParserException {
boolean isActive = false;
boolean isOnboarded = false;
+ int notificationsShown = 0;
while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
String[] valueName = new String[1];
@@ -199,6 +206,9 @@ public class WakeupConfigStoreData implements StoreData {
case XML_TAG_IS_ONBOARDED:
isOnboarded = (Boolean) value;
break;
+ case XML_TAG_NOTIFICATIONS_SHOWN:
+ notificationsShown = (Integer) value;
+ break;
default:
throw new XmlPullParserException("Unknown value found: " + valueName[0]);
}
@@ -206,6 +216,7 @@ public class WakeupConfigStoreData implements StoreData {
mIsActiveDataSource.setData(isActive);
mIsOnboardedDataSource.setData(isOnboarded);
+ mNotificationsDataSource.setData(notificationsShown);
}
/**
@@ -248,6 +259,7 @@ public class WakeupConfigStoreData implements StoreData {
mNetworkDataSource.setData(Collections.emptySet());
mIsActiveDataSource.setData(false);
mIsOnboardedDataSource.setData(false);
+ mNotificationsDataSource.setData(0);
}
}
diff --git a/com/android/server/wifi/WakeupController.java b/com/android/server/wifi/WakeupController.java
index 9743390e..2af8ec92 100644
--- a/com/android/server/wifi/WakeupController.java
+++ b/com/android/server/wifi/WakeupController.java
@@ -26,7 +26,6 @@ import android.net.wifi.WifiScanner;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
-import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -38,6 +37,7 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
/**
@@ -127,24 +127,30 @@ public class WakeupController {
mContentObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
- mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
- mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
- Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
+ readWifiWakeupEnabledFromSettings();
+ mWakeupOnboarding.setOnboarded();
}
};
mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver);
- mContentObserver.onChange(false /* selfChange */);
+ readWifiWakeupEnabledFromSettings();
// registering the store data here has the effect of reading the persisted value of the
// data sources after system boot finishes
mWakeupConfigStoreData = new WakeupConfigStoreData(
new IsActiveDataSource(),
- mWakeupOnboarding.getDataSource(),
+ mWakeupOnboarding.getIsOnboadedDataSource(),
+ mWakeupOnboarding.getNotificationsDataSource(),
mWakeupLock.getDataSource());
wifiConfigStore.registerStoreData(mWakeupConfigStoreData);
}
+ private void readWifiWakeupEnabledFromSettings() {
+ mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
+ mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
+ Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
+ }
+
private void setActive(boolean isActive) {
if (mIsActive != isActive) {
Log.d(TAG, "Setting active to " + isActive);
@@ -164,9 +170,9 @@ public class WakeupController {
Log.d(TAG, "start()");
mWifiInjector.getWifiScanner().registerScanListener(mScanListener);
- // If already active, we don't want to re-initialize the lock, so return early.
+ // If already active, we don't want to restart the session, so return early.
if (mIsActive) {
- // TODO record metric for calls to start() when already active
+ mWifiWakeMetrics.recordIgnoredStart();
return;
}
setActive(true);
@@ -175,14 +181,14 @@ public class WakeupController {
if (isEnabled()) {
mWakeupOnboarding.maybeShowNotification();
- Set<ScanResultMatchInfo> mostRecentSavedScanResults = getMostRecentSavedScanResults();
-
+ Set<ScanResultMatchInfo> savedNetworksFromLatestScan = getSavedNetworksFromLatestScan();
if (mVerboseLoggingEnabled) {
- Log.d(TAG, "Saved networks in most recent scan:" + mostRecentSavedScanResults);
+ Log.d(TAG, "Saved networks in most recent scan:" + savedNetworksFromLatestScan);
}
- mWifiWakeMetrics.recordStartEvent(mostRecentSavedScanResults.size());
- mWakeupLock.initialize(mostRecentSavedScanResults);
+ mWifiWakeMetrics.recordStartEvent(savedNetworksFromLatestScan.size());
+ mWakeupLock.setLock(savedNetworksFromLatestScan);
+ // TODO(b/77291248): request low latency scan here
}
}
@@ -212,18 +218,30 @@ public class WakeupController {
mWakeupLock.enableVerboseLogging(mVerboseLoggingEnabled);
}
- /** Returns a list of saved networks from the last full scan. */
- private Set<ScanResultMatchInfo> getMostRecentSavedScanResults() {
- Set<ScanResultMatchInfo> goodSavedNetworks = getGoodSavedNetworks();
+ /** Returns a filtered list of saved networks from the last full scan. */
+ private Set<ScanResultMatchInfo> getSavedNetworksFromLatestScan() {
+ Set<ScanResult> filteredScanResults =
+ filterScanResults(mWifiInjector.getWifiScanner().getSingleScanResults());
+ Set<ScanResultMatchInfo> goodMatchInfos = toMatchInfos(filteredScanResults);
+ goodMatchInfos.retainAll(getGoodSavedNetworks());
+
+ return goodMatchInfos;
+ }
- List<ScanResult> scanResults = mWifiInjector.getWifiScanner().getSingleScanResults();
- Set<ScanResultMatchInfo> lastSeenNetworks = new HashSet<>(scanResults.size());
- for (ScanResult scanResult : scanResults) {
- lastSeenNetworks.add(ScanResultMatchInfo.fromScanResult(scanResult));
+ /** Returns a set of ScanResults with all DFS channels removed. */
+ private Set<ScanResult> filterScanResults(Collection<ScanResult> scanResults) {
+ int[] dfsChannels = mWifiInjector.getWifiNative()
+ .getChannelsForBand(WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY);
+ if (dfsChannels == null) {
+ dfsChannels = new int[0];
}
- lastSeenNetworks.retainAll(goodSavedNetworks);
- return lastSeenNetworks;
+ final Set<Integer> dfsChannelSet = Arrays.stream(dfsChannels).boxed()
+ .collect(Collectors.toSet());
+
+ return scanResults.stream()
+ .filter(scanResult -> !dfsChannelSet.contains(scanResult.frequency))
+ .collect(Collectors.toSet());
}
/** Returns a filtered list of saved networks from WifiConfigManager. */
@@ -245,16 +263,18 @@ public class WakeupController {
}
//TODO(b/69271702) implement WAN filtering
- private boolean isWideAreaNetwork(WifiConfiguration wifiConfiguration) {
+ private static boolean isWideAreaNetwork(WifiConfiguration config) {
return false;
}
/**
* Handles incoming scan results.
*
- * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is
- * empty, it evaluates scan results for a match with saved networks. If a match exists, it
- * enables wifi.
+ * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is not
+ * yet fully initialized, it adds the current scanResults to the lock and returns. If WakeupLock
+ * is initialized but not empty, the controller updates the lock with the current scan. If it is
+ * both initialized and empty, it evaluates scan results for a match with saved networks. If a
+ * match exists, it enables wifi.
*
* <p>The feature must be enabled and the store data must be loaded in order for the controller
* to handle scan results.
@@ -269,41 +289,22 @@ public class WakeupController {
// only count scan as handled if isEnabled
mNumScansHandled++;
-
if (mVerboseLoggingEnabled) {
- Log.d(TAG, "Incoming scan. Total scans handled: " + mNumScansHandled);
- Log.d(TAG, "ScanResults: " + scanResults);
+ Log.d(TAG, "Incoming scan #" + mNumScansHandled);
}
- // need to show notification here in case user enables Wifi Wake when Wifi is off
+ // need to show notification here in case user turns phone on while wifi is off
mWakeupOnboarding.maybeShowNotification();
- if (!mWakeupOnboarding.isOnboarded()) {
- return;
- }
-
- // only update the wakeup lock if it's not already empty
- if (!mWakeupLock.isEmpty()) {
- if (mVerboseLoggingEnabled) {
- Log.d(TAG, "WakeupLock not empty. Updating.");
- }
- Set<ScanResultMatchInfo> networks = new ArraySet<>();
- for (ScanResult scanResult : scanResults) {
- networks.add(ScanResultMatchInfo.fromScanResult(scanResult));
- }
- mWakeupLock.update(networks);
-
- // if wakeup lock is still not empty, return
- if (!mWakeupLock.isEmpty()) {
- return;
- }
+ Set<ScanResult> filteredScanResults = filterScanResults(scanResults);
- Log.d(TAG, "WakeupLock emptied");
- mWifiWakeMetrics.recordUnlockEvent(mNumScansHandled);
+ mWakeupLock.update(toMatchInfos(filteredScanResults));
+ if (!mWakeupLock.isUnlocked()) {
+ return;
}
ScanResult network =
- mWakeupEvaluator.findViableNetwork(scanResults, getGoodSavedNetworks());
+ mWakeupEvaluator.findViableNetwork(filteredScanResults, getGoodSavedNetworks());
if (network != null) {
Log.d(TAG, "Enabling wifi for network: " + network.SSID);
@@ -312,6 +313,15 @@ public class WakeupController {
}
/**
+ * Converts ScanResults to ScanResultMatchInfos.
+ */
+ private static Set<ScanResultMatchInfo> toMatchInfos(Collection<ScanResult> scanResults) {
+ return scanResults.stream()
+ .map(ScanResultMatchInfo::fromScanResult)
+ .collect(Collectors.toSet());
+ }
+
+ /**
* Enables wifi.
*
* <p>This method ignores all checks and assumes that {@link WifiStateMachine} is currently
diff --git a/com/android/server/wifi/WakeupLock.java b/com/android/server/wifi/WakeupLock.java
index 9e617a49..c6a8f5a3 100644
--- a/com/android/server/wifi/WakeupLock.java
+++ b/com/android/server/wifi/WakeupLock.java
@@ -16,6 +16,7 @@
package com.android.server.wifi;
+import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.Log;
@@ -29,7 +30,7 @@ import java.util.Map;
import java.util.Set;
/**
- * A lock to determine whether Auto Wifi can re-enable Wifi.
+ * A lock to determine whether Wifi Wake can re-enable Wifi.
*
* <p>Wakeuplock manages a list of networks to determine whether the device's location has changed.
*/
@@ -39,44 +40,147 @@ public class WakeupLock {
@VisibleForTesting
static final int CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT = 3;
-
+ @VisibleForTesting
+ static final long MAX_LOCK_TIME_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS;
private final WifiConfigManager mWifiConfigManager;
private final Map<ScanResultMatchInfo, Integer> mLockedNetworks = new ArrayMap<>();
+ private final WifiWakeMetrics mWifiWakeMetrics;
+ private final Clock mClock;
+
private boolean mVerboseLoggingEnabled;
+ private long mLockTimestamp;
+ private boolean mIsInitialized;
+ private int mNumScans;
- public WakeupLock(WifiConfigManager wifiConfigManager) {
+ public WakeupLock(WifiConfigManager wifiConfigManager, WifiWakeMetrics wifiWakeMetrics,
+ Clock clock) {
mWifiConfigManager = wifiConfigManager;
+ mWifiWakeMetrics = wifiWakeMetrics;
+ mClock = clock;
}
/**
- * Initializes the WakeupLock with the given {@link ScanResultMatchInfo} list.
+ * Sets the WakeupLock with the given {@link ScanResultMatchInfo} list.
*
- * <p>This saves the wakeup lock to the store.
+ * <p>This saves the wakeup lock to the store and begins the initialization process.
*
* @param scanResultList list of ScanResultMatchInfos to start the lock with
*/
- public void initialize(Collection<ScanResultMatchInfo> scanResultList) {
+ public void setLock(Collection<ScanResultMatchInfo> scanResultList) {
+ mLockTimestamp = mClock.getElapsedSinceBootMillis();
+ mIsInitialized = false;
+ mNumScans = 0;
+
mLockedNetworks.clear();
for (ScanResultMatchInfo scanResultMatchInfo : scanResultList) {
mLockedNetworks.put(scanResultMatchInfo, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
}
- Log.d(TAG, "Lock initialized. Number of networks: " + mLockedNetworks.size());
+ Log.d(TAG, "Lock set. Number of networks: " + mLockedNetworks.size());
mWifiConfigManager.saveToStore(false /* forceWrite */);
}
/**
- * Updates the lock with the given {@link ScanResultMatchInfo} list.
+ * Maybe sets the WakeupLock as initialized based on total scans handled.
+ *
+ * @param numScans total number of elapsed scans in the current WifiWake session
+ */
+ private void maybeSetInitializedByScans(int numScans) {
+ if (mIsInitialized) {
+ return;
+ }
+ boolean shouldBeInitialized = numScans >= CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT;
+ if (shouldBeInitialized) {
+ mIsInitialized = true;
+
+ Log.d(TAG, "Lock initialized by handled scans. Scans: " + numScans);
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "State of lock: " + mLockedNetworks);
+ }
+
+ // log initialize event
+ mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size());
+ }
+ }
+
+ /**
+ * Maybe sets the WakeupLock as initialized based on elapsed time.
+ *
+ * @param timestampMillis current timestamp
+ */
+ private void maybeSetInitializedByTimeout(long timestampMillis) {
+ if (mIsInitialized) {
+ return;
+ }
+ long elapsedTime = timestampMillis - mLockTimestamp;
+ boolean shouldBeInitialized = elapsedTime > MAX_LOCK_TIME_MILLIS;
+
+ if (shouldBeInitialized) {
+ mIsInitialized = true;
+
+ Log.d(TAG, "Lock initialized by timeout. Elapsed time: " + elapsedTime);
+ if (mNumScans == 0) {
+ Log.w(TAG, "Lock initialized with 0 handled scans!");
+ }
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "State of lock: " + mLockedNetworks);
+ }
+
+ // log initialize event
+ mWifiWakeMetrics.recordInitializeEvent(mNumScans, mLockedNetworks.size());
+ }
+ }
+
+ /** Returns whether the lock has been fully initialized. */
+ public boolean isInitialized() {
+ return mIsInitialized;
+ }
+
+ /**
+ * Adds the given networks to the lock.
+ *
+ * <p>This is called during the initialization step.
+ *
+ * @param networkList The list of networks to be added
+ */
+ private void addToLock(Collection<ScanResultMatchInfo> networkList) {
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Initializing lock with networks: " + networkList);
+ }
+
+ boolean hasChanged = false;
+
+ for (ScanResultMatchInfo network : networkList) {
+ if (!mLockedNetworks.containsKey(network)) {
+ mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
+ hasChanged = true;
+ }
+ }
+
+ if (hasChanged) {
+ mWifiConfigManager.saveToStore(false /* forceWrite */);
+ }
+
+ // Set initialized if the lock has handled enough scans, and log the event
+ maybeSetInitializedByScans(mNumScans);
+ }
+
+ /**
+ * Removes networks from the lock if not present in the given {@link ScanResultMatchInfo} list.
*
* <p>If a network in the lock is not present in the list, reduce the number of scans
* required to evict by one. Remove any entries in the list with 0 scans required to evict. If
* any entries in the lock are removed, the store is updated.
*
- * @param scanResultList list of present ScanResultMatchInfos to update the lock with
+ * @param networkList list of present ScanResultMatchInfos to update the lock with
*/
- public void update(Collection<ScanResultMatchInfo> scanResultList) {
+ private void removeFromLock(Collection<ScanResultMatchInfo> networkList) {
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Filtering lock with networks: " + networkList);
+ }
+
boolean hasChanged = false;
Iterator<Map.Entry<ScanResultMatchInfo, Integer>> it =
mLockedNetworks.entrySet().iterator();
@@ -84,7 +188,7 @@ public class WakeupLock {
Map.Entry<ScanResultMatchInfo, Integer> entry = it.next();
// if present in scan list, reset to max
- if (scanResultList.contains(entry.getKey())) {
+ if (networkList.contains(entry.getKey())) {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "Found network in lock: " + entry.getKey().networkSsid);
}
@@ -104,13 +208,48 @@ public class WakeupLock {
if (hasChanged) {
mWifiConfigManager.saveToStore(false /* forceWrite */);
}
+
+ if (isUnlocked()) {
+ Log.d(TAG, "Lock emptied. Recording unlock event.");
+ mWifiWakeMetrics.recordUnlockEvent(mNumScans);
+ }
}
/**
- * Returns whether the internal network set is empty.
+ * Updates the lock with the given {@link ScanResultMatchInfo} list.
+ *
+ * <p>Based on the current initialization state of the lock, either adds or removes networks
+ * from the lock.
+ *
+ * <p>The lock is initialized after {@link #CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT}
+ * scans have been handled, or after {@link #MAX_LOCK_TIME_MILLIS} milliseconds have elapsed
+ * since {@link #setLock(Collection)}.
+ *
+ * @param networkList list of present ScanResultMatchInfos to update the lock with
*/
- public boolean isEmpty() {
- return mLockedNetworks.isEmpty();
+ public void update(Collection<ScanResultMatchInfo> networkList) {
+ // update is no-op if already unlocked
+ if (isUnlocked()) {
+ return;
+ }
+ // Before checking handling the scan, we check to see whether we've exceeded the maximum
+ // time allowed for initialization. If so, we set initialized and treat this scan as a
+ // "removeFromLock()" instead of an "addToLock()".
+ maybeSetInitializedByTimeout(mClock.getElapsedSinceBootMillis());
+
+ mNumScans++;
+
+ // add or remove networks based on initialized status
+ if (mIsInitialized) {
+ removeFromLock(networkList);
+ } else {
+ addToLock(networkList);
+ }
+ }
+
+ /** Returns whether the WakeupLock is unlocked */
+ public boolean isUnlocked() {
+ return mIsInitialized && mLockedNetworks.isEmpty();
}
/** Returns the data source for the WakeupLock config store data. */
@@ -121,6 +260,8 @@ public class WakeupLock {
/** Dumps wakeup lock contents. */
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("WakeupLock: ");
+ pw.println("mNumScans: " + mNumScans);
+ pw.println("mIsInitialized: " + mIsInitialized);
pw.println("Locked networks: " + mLockedNetworks.size());
for (Map.Entry<ScanResultMatchInfo, Integer> entry : mLockedNetworks.entrySet()) {
pw.println(entry.getKey() + ", scans to evict: " + entry.getValue());
@@ -146,7 +287,8 @@ public class WakeupLock {
for (ScanResultMatchInfo network : data) {
mLockedNetworks.put(network, CONSECUTIVE_MISSED_SCANS_REQUIRED_TO_EVICT);
}
-
+ // lock is considered initialized if loaded from store
+ mIsInitialized = true;
}
}
}
diff --git a/com/android/server/wifi/WakeupNotificationFactory.java b/com/android/server/wifi/WakeupNotificationFactory.java
index 42ae4670..23f31a7d 100644
--- a/com/android/server/wifi/WakeupNotificationFactory.java
+++ b/com/android/server/wifi/WakeupNotificationFactory.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import com.android.internal.R;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
@@ -37,6 +38,9 @@ public class WakeupNotificationFactory {
public static final String ACTION_TURN_OFF_WIFI_WAKE =
"com.android.server.wifi.wakeup.TURN_OFF_WIFI_WAKE";
+ /** Notification channel ID for onboarding messages. */
+ public static final int ONBOARD_ID = SystemMessage.NOTE_WIFI_WAKE_ONBOARD;
+
private final Context mContext;
private final FrameworkFacade mFrameworkFacade;
diff --git a/com/android/server/wifi/WakeupOnboarding.java b/com/android/server/wifi/WakeupOnboarding.java
index d4caa0fd..b6bcbc3c 100644
--- a/com/android/server/wifi/WakeupOnboarding.java
+++ b/com/android/server/wifi/WakeupOnboarding.java
@@ -27,22 +27,31 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;
+import android.os.SystemClock;
import android.provider.Settings;
+import android.text.format.DateUtils;
import android.util.Log;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.annotations.VisibleForTesting;
/**
* Manages the WiFi Wake onboarding notification.
*
* <p>If a user disables wifi with Wifi Wake enabled, this notification is shown to explain that
- * wifi may turn back on automatically. Wifi will not automatically turn back on until after the
- * user interacts with the onboarding notification in some way (e.g. dismiss, tap).
+ * wifi may turn back on automatically. It will be displayed up to 3 times, or until the
+ * user either interacts with the onboarding notification in some way (e.g. dismiss, tap) or
+ * manually enables/disables the feature in WifiSettings.
*/
public class WakeupOnboarding {
private static final String TAG = "WakeupOnboarding";
+ @VisibleForTesting
+ static final int NOTIFICATIONS_UNTIL_ONBOARDED = 3;
+ @VisibleForTesting
+ static final long REQUIRED_NOTIFICATION_DELAY = DateUtils.DAY_IN_MILLIS;
+ private static final long NOT_SHOWN_TIMESTAMP = -1;
+
private final Context mContext;
private final WakeupNotificationFactory mWakeupNotificationFactory;
private NotificationManager mNotificationManager;
@@ -52,6 +61,8 @@ public class WakeupOnboarding {
private final FrameworkFacade mFrameworkFacade;
private boolean mIsOnboarded;
+ private int mTotalNotificationsShown;
+ private long mLastShownTimestamp = NOT_SHOWN_TIMESTAMP;
private boolean mIsNotificationShowing;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -104,17 +115,46 @@ public class WakeupOnboarding {
/** Shows the onboarding notification if applicable. */
public void maybeShowNotification() {
- if (isOnboarded() || mIsNotificationShowing) {
+ maybeShowNotification(SystemClock.elapsedRealtime());
+ }
+
+ @VisibleForTesting
+ void maybeShowNotification(long timestamp) {
+ if (!shouldShowNotification(timestamp)) {
return;
}
-
Log.d(TAG, "Showing onboarding notification.");
+ incrementTotalNotificationsShown();
+ mIsNotificationShowing = true;
+ mLastShownTimestamp = timestamp;
+
mContext.registerReceiver(mBroadcastReceiver, mIntentFilter,
null /* broadcastPermission */, mHandler);
- getNotificationManager().notify(SystemMessage.NOTE_WIFI_WAKE_ONBOARD,
+ getNotificationManager().notify(WakeupNotificationFactory.ONBOARD_ID,
mWakeupNotificationFactory.createOnboardingNotification());
- mIsNotificationShowing = true;
+ }
+
+ /**
+ * Increment the total number of shown notifications and onboard the user if reached the
+ * required amount.
+ */
+ private void incrementTotalNotificationsShown() {
+ mTotalNotificationsShown++;
+ if (mTotalNotificationsShown >= NOTIFICATIONS_UNTIL_ONBOARDED) {
+ setOnboarded();
+ } else {
+ mWifiConfigManager.saveToStore(false /* forceWrite */);
+ }
+ }
+
+ private boolean shouldShowNotification(long timestamp) {
+ if (isOnboarded() || mIsNotificationShowing) {
+ return false;
+ }
+
+ return mLastShownTimestamp == NOT_SHOWN_TIMESTAMP
+ || (timestamp - mLastShownTimestamp) > REQUIRED_NOTIFICATION_DELAY;
}
/** Handles onboarding cleanup on stop. */
@@ -132,11 +172,15 @@ public class WakeupOnboarding {
}
mContext.unregisterReceiver(mBroadcastReceiver);
- getNotificationManager().cancel(SystemMessage.NOTE_WIFI_WAKE_ONBOARD);
+ getNotificationManager().cancel(WakeupNotificationFactory.ONBOARD_ID);
mIsNotificationShowing = false;
}
- private void setOnboarded() {
+ /** Sets the user as onboarded and persists to store. */
+ public void setOnboarded() {
+ if (mIsOnboarded) {
+ return;
+ }
Log.d(TAG, "Setting user as onboarded.");
mIsOnboarded = true;
mWifiConfigManager.saveToStore(false /* forceWrite */);
@@ -150,12 +194,17 @@ public class WakeupOnboarding {
return mNotificationManager;
}
- /** Returns the {@link WakeupConfigStoreData.DataSource} for the {@link WifiConfigStore}. */
- public WakeupConfigStoreData.DataSource<Boolean> getDataSource() {
- return new OnboardingDataSource();
+ /** Returns the {@link WakeupConfigStoreData.DataSource} for the onboarded status. */
+ public WakeupConfigStoreData.DataSource<Boolean> getIsOnboadedDataSource() {
+ return new IsOnboardedDataSource();
}
- private class OnboardingDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
+ /** Returns the {@link WakeupConfigStoreData.DataSource} for the notification status. */
+ public WakeupConfigStoreData.DataSource<Integer> getNotificationsDataSource() {
+ return new NotificationsDataSource();
+ }
+
+ private class IsOnboardedDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
@Override
public Boolean getData() {
@@ -167,4 +216,17 @@ public class WakeupOnboarding {
mIsOnboarded = data;
}
}
+
+ private class NotificationsDataSource implements WakeupConfigStoreData.DataSource<Integer> {
+
+ @Override
+ public Integer getData() {
+ return mTotalNotificationsShown;
+ }
+
+ @Override
+ public void setData(Integer data) {
+ mTotalNotificationsShown = data;
+ }
+ }
}
diff --git a/com/android/server/wifi/WifiConfigStoreLegacy.java b/com/android/server/wifi/WifiConfigStoreLegacy.java
index 184ee2f6..ef6d82f3 100644
--- a/com/android/server/wifi/WifiConfigStoreLegacy.java
+++ b/com/android/server/wifi/WifiConfigStoreLegacy.java
@@ -81,16 +81,28 @@ public class WifiConfigStoreLegacy {
*/
private final WifiNetworkHistory mWifiNetworkHistory;
private final WifiNative mWifiNative;
- private final IpConfigStore mIpconfigStore;
+ private final IpConfigStoreWrapper mIpconfigStoreWrapper;
private final LegacyPasspointConfigParser mPasspointConfigParser;
+ /**
+ * Used to help mocking the static methods of IpconfigStore.
+ */
+ public static class IpConfigStoreWrapper {
+ /**
+ * Read IP configurations from Ip config store.
+ */
+ public SparseArray<IpConfiguration> readIpAndProxyConfigurations(String filePath) {
+ return IpConfigStore.readIpAndProxyConfigurations(filePath);
+ }
+ }
+
WifiConfigStoreLegacy(WifiNetworkHistory wifiNetworkHistory,
- WifiNative wifiNative, IpConfigStore ipConfigStore,
+ WifiNative wifiNative, IpConfigStoreWrapper ipConfigStore,
LegacyPasspointConfigParser passpointConfigParser) {
mWifiNetworkHistory = wifiNetworkHistory;
mWifiNative = wifiNative;
- mIpconfigStore = ipConfigStore;
+ mIpconfigStoreWrapper = ipConfigStore;
mPasspointConfigParser = passpointConfigParser;
}
@@ -105,7 +117,7 @@ public class WifiConfigStoreLegacy {
private static WifiConfiguration lookupWifiConfigurationUsingConfigKeyHash(
Map<String, WifiConfiguration> configurationMap, int hashCode) {
for (Map.Entry<String, WifiConfiguration> entry : configurationMap.entrySet()) {
- if (entry.getKey().hashCode() == hashCode) {
+ if (entry.getKey() != null && entry.getKey().hashCode() == hashCode) {
return entry.getValue();
}
}
@@ -122,7 +134,8 @@ public class WifiConfigStoreLegacy {
// This is a map of the hash code of the network's configKey to the corresponding
// IpConfiguration.
SparseArray<IpConfiguration> ipConfigurations =
- mIpconfigStore.readIpAndProxyConfigurations(IP_CONFIG_FILE.getAbsolutePath());
+ mIpconfigStoreWrapper.readIpAndProxyConfigurations(
+ IP_CONFIG_FILE.getAbsolutePath());
if (ipConfigurations == null || ipConfigurations.size() == 0) {
Log.w(TAG, "No ip configurations found in ipconfig store");
return;
diff --git a/com/android/server/wifi/WifiController.java b/com/android/server/wifi/WifiController.java
index 5cc0306e..76b44c82 100644
--- a/com/android/server/wifi/WifiController.java
+++ b/com/android/server/wifi/WifiController.java
@@ -76,21 +76,23 @@ public class WifiController extends StateMachine {
private static final int BASE = Protocol.BASE_WIFI_CONTROLLER;
- static final int CMD_EMERGENCY_MODE_CHANGED = BASE + 1;
- static final int CMD_SCAN_ALWAYS_MODE_CHANGED = BASE + 7;
- static final int CMD_WIFI_TOGGLED = BASE + 8;
- static final int CMD_AIRPLANE_TOGGLED = BASE + 9;
- static final int CMD_SET_AP = BASE + 10;
- static final int CMD_DEFERRED_TOGGLE = BASE + 11;
- static final int CMD_USER_PRESENT = BASE + 12;
- static final int CMD_AP_START_FAILURE = BASE + 13;
- static final int CMD_EMERGENCY_CALL_STATE_CHANGED = BASE + 14;
- static final int CMD_AP_STOPPED = BASE + 15;
- static final int CMD_STA_START_FAILURE = BASE + 16;
+ static final int CMD_EMERGENCY_MODE_CHANGED = BASE + 1;
+ static final int CMD_SCAN_ALWAYS_MODE_CHANGED = BASE + 7;
+ static final int CMD_WIFI_TOGGLED = BASE + 8;
+ static final int CMD_AIRPLANE_TOGGLED = BASE + 9;
+ static final int CMD_SET_AP = BASE + 10;
+ static final int CMD_DEFERRED_TOGGLE = BASE + 11;
+ static final int CMD_USER_PRESENT = BASE + 12;
+ static final int CMD_AP_START_FAILURE = BASE + 13;
+ static final int CMD_EMERGENCY_CALL_STATE_CHANGED = BASE + 14;
+ static final int CMD_AP_STOPPED = BASE + 15;
+ static final int CMD_STA_START_FAILURE = BASE + 16;
// Command used to trigger a wifi stack restart when in active mode
- static final int CMD_RESTART_WIFI = BASE + 17;
+ static final int CMD_RECOVERY_RESTART_WIFI = BASE + 17;
// Internal command used to complete wifi stack restart
- private static final int CMD_RESTART_WIFI_CONTINUE = BASE + 18;
+ private static final int CMD_RECOVERY_RESTART_WIFI_CONTINUE = BASE + 18;
+ // Command to disable wifi when SelfRecovery is throttled or otherwise not doing full recovery
+ static final int CMD_RECOVERY_DISABLE_WIFI = BASE + 19;
private DefaultState mDefaultState = new DefaultState();
private StaEnabledState mStaEnabledState = new StaEnabledState();
@@ -215,8 +217,9 @@ public class WifiController extends StateMachine {
case CMD_AP_START_FAILURE:
case CMD_AP_STOPPED:
case CMD_STA_START_FAILURE:
- case CMD_RESTART_WIFI:
- case CMD_RESTART_WIFI_CONTINUE:
+ case CMD_RECOVERY_RESTART_WIFI:
+ case CMD_RECOVERY_RESTART_WIFI_CONTINUE:
+ case CMD_RECOVERY_DISABLE_WIFI:
break;
case CMD_USER_PRESENT:
mFirstUserSignOnSeen = true;
@@ -297,7 +300,7 @@ public class WifiController extends StateMachine {
log("DEFERRED_TOGGLE handled");
sendMessage((Message)(msg.obj));
break;
- case CMD_RESTART_WIFI_CONTINUE:
+ case CMD_RECOVERY_RESTART_WIFI_CONTINUE:
transitionTo(mDeviceActiveState);
break;
default:
@@ -649,23 +652,27 @@ public class WifiController extends StateMachine {
}
mFirstUserSignOnSeen = true;
return HANDLED;
- } else if (msg.what == CMD_RESTART_WIFI) {
- final String bugTitle = "Wi-Fi BugReport";
+ } else if (msg.what == CMD_RECOVERY_RESTART_WIFI) {
+ final String bugTitle;
final String bugDetail;
- if (msg.obj != null && msg.arg1 < SelfRecovery.REASON_STRINGS.length
- && msg.arg1 >= 0) {
+ if (msg.arg1 < SelfRecovery.REASON_STRINGS.length && msg.arg1 >= 0) {
bugDetail = SelfRecovery.REASON_STRINGS[msg.arg1];
+ bugTitle = "Wi-Fi BugReport: " + bugDetail;
} else {
bugDetail = "";
+ bugTitle = "Wi-Fi BugReport";
}
if (msg.arg1 != SelfRecovery.REASON_LAST_RESORT_WATCHDOG) {
(new Handler(mWifiStateMachineLooper)).post(() -> {
mWifiStateMachine.takeBugReport(bugTitle, bugDetail);
});
}
- deferMessage(obtainMessage(CMD_RESTART_WIFI_CONTINUE));
+ deferMessage(obtainMessage(CMD_RECOVERY_RESTART_WIFI_CONTINUE));
transitionTo(mApStaDisabledState);
return HANDLED;
+ } else if (msg.what == CMD_RECOVERY_DISABLE_WIFI) {
+ loge("Recovery has been throttled, disable wifi");
+ transitionTo(mApStaDisabledState);
}
return NOT_HANDLED;
}
diff --git a/com/android/server/wifi/WifiInjector.java b/com/android/server/wifi/WifiInjector.java
index 04bb3f48..a1ecca2c 100644
--- a/com/android/server/wifi/WifiInjector.java
+++ b/com/android/server/wifi/WifiInjector.java
@@ -189,7 +189,7 @@ public class WifiInjector {
ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
mWifiNative = new WifiNative(
mWifiVendorHal, mSupplicantStaIfaceHal, mHostapdHal, mWificondControl,
- mNwManagementService, mPropertyService, mWifiMetrics);
+ mWifiMonitor, mNwManagementService, mPropertyService, mWifiMetrics);
mWifiP2pMonitor = new WifiP2pMonitor(this);
mSupplicantP2pIfaceHal = new SupplicantP2pIfaceHal(mWifiP2pMonitor);
mWifiP2pNative = new WifiP2pNative(mSupplicantP2pIfaceHal, mHalDeviceManager);
@@ -214,7 +214,7 @@ public class WifiInjector {
mWifiNetworkHistory = new WifiNetworkHistory(mContext, writer);
mIpConfigStore = new IpConfigStore(writer);
mWifiConfigStoreLegacy = new WifiConfigStoreLegacy(
- mWifiNetworkHistory, mWifiNative, mIpConfigStore,
+ mWifiNetworkHistory, mWifiNative, new WifiConfigStoreLegacy.IpConfigStoreWrapper(),
new LegacyPasspointConfigParser());
// Config Manager
mWifiConfigManager = new WifiConfigManager(mContext, mClock,
@@ -273,7 +273,8 @@ public class WifiInjector {
mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade,
wakeupNotificationFactory);
mWakeupController = new WakeupController(mContext,
- mWifiStateMachineHandlerThread.getLooper(), new WakeupLock(mWifiConfigManager),
+ mWifiStateMachineHandlerThread.getLooper(),
+ new WakeupLock(mWifiConfigManager, mWifiMetrics.getWakeupMetrics(), mClock),
WakeupEvaluator.fromContext(mContext), wakeupOnboarding, mWifiConfigManager,
mWifiConfigStore, mWifiMetrics.getWakeupMetrics(), this, mFrameworkFacade);
mLockManager = new WifiLockManager(mContext, BatteryStatsService.getService());
diff --git a/com/android/server/wifi/WifiMetrics.java b/com/android/server/wifi/WifiMetrics.java
index 669eff65..66605971 100644
--- a/com/android/server/wifi/WifiMetrics.java
+++ b/com/android/server/wifi/WifiMetrics.java
@@ -287,6 +287,8 @@ public class WifiMetrics {
public static final int FAILURE_ROAM_TIMEOUT = 9;
// DHCP failure
public static final int FAILURE_DHCP = 10;
+ // ASSOCIATION_TIMED_OUT
+ public static final int FAILURE_ASSOCIATION_TIMED_OUT = 11;
RouterFingerPrint mRouterFingerPrint;
private long mRealStartTime;
@@ -375,6 +377,10 @@ public class WifiMetrics {
break;
case FAILURE_DHCP:
sb.append("DHCP");
+ break;
+ case FAILURE_ASSOCIATION_TIMED_OUT:
+ sb.append("ASSOCIATION_TIMED_OUT");
+ break;
default:
sb.append("UNKNOWN");
break;
diff --git a/com/android/server/wifi/WifiNative.java b/com/android/server/wifi/WifiNative.java
index aebb236c..474a8977 100644
--- a/com/android/server/wifi/WifiNative.java
+++ b/com/android/server/wifi/WifiNative.java
@@ -74,21 +74,22 @@ public class WifiNative {
private final HostapdHal mHostapdHal;
private final WifiVendorHal mWifiVendorHal;
private final WificondControl mWificondControl;
+ private final WifiMonitor mWifiMonitor;
private final INetworkManagementService mNwManagementService;
private final PropertyService mPropertyService;
private final WifiMetrics mWifiMetrics;
private boolean mVerboseLoggingEnabled = false;
- // TODO(b/69426063): Remove interfaceName from constructor once WifiStateMachine switches over
- // to the new interface management methods.
public WifiNative(WifiVendorHal vendorHal,
SupplicantStaIfaceHal staIfaceHal, HostapdHal hostapdHal,
- WificondControl condControl, INetworkManagementService nwService,
+ WificondControl condControl, WifiMonitor wifiMonitor,
+ INetworkManagementService nwService,
PropertyService propertyService, WifiMetrics wifiMetrics) {
mWifiVendorHal = vendorHal;
mSupplicantStaIfaceHal = staIfaceHal;
mHostapdHal = hostapdHal;
mWificondControl = condControl;
+ mWifiMonitor = wifiMonitor;
mNwManagementService = nwService;
mPropertyService = propertyService;
mWifiMetrics = wifiMetrics;
@@ -391,6 +392,7 @@ public class WifiNative {
/** Helper method invoked to teardown client iface and perform necessary cleanup */
private void onClientInterfaceDestroyed(@NonNull Iface iface) {
synchronized (mLock) {
+ mWifiMonitor.stopMonitoring(iface.name);
if (!unregisterNetworkObserver(iface.networkObserver)) {
Log.e(TAG, "Failed to unregister network observer on " + iface);
}
@@ -840,6 +842,7 @@ public class WifiNative {
teardownInterface(iface.name);
return null;
}
+ mWifiMonitor.startMonitoring(iface.name);
// Just to avoid any race conditions with interface state change callbacks,
// update the interface state before we exit.
onInterfaceStateChanged(iface, isInterfaceUp(iface.name));
diff --git a/com/android/server/wifi/WifiScoreReport.java b/com/android/server/wifi/WifiScoreReport.java
index 32529f88..c6a7105b 100644
--- a/com/android/server/wifi/WifiScoreReport.java
+++ b/com/android/server/wifi/WifiScoreReport.java
@@ -39,9 +39,8 @@ public class WifiScoreReport {
private boolean mVerboseLoggingEnabled = false;
private static final long FIRST_REASONABLE_WALL_CLOCK = 1490000000000L; // mid-December 2016
- // Cache of the last score report.
- private String mReport;
- private boolean mReportValid = false;
+ // Cache of the last score
+ private int mScore = NetworkAgent.WIFI_BASE_SCORE;
private final ScoringParams mScoringParams;
private final Clock mClock;
@@ -58,39 +57,18 @@ public class WifiScoreReport {
}
/**
- * Method returning the String representation of the last score report.
- *
- * @return String score report
- */
- public String getLastReport() {
- return mReport;
- }
-
- /**
* Reset the last calculated score.
*/
public void reset() {
- mReport = "";
- if (mReportValid) {
- mSessionNumber++;
- mReportValid = false;
- }
+ mSessionNumber++;
+ mScore = NetworkAgent.WIFI_BASE_SCORE;
+ mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
mAggressiveConnectedScore.reset();
mVelocityBasedConnectedScore.reset();
if (mVerboseLoggingEnabled) Log.d(TAG, "reset");
}
/**
- * Checks if the last report data is valid or not. This will be cleared when {@link #reset()} is
- * invoked.
- *
- * @return true if valid, false otherwise.
- */
- public boolean isLastReportValid() {
- return mReportValid;
- }
-
- /**
* Enable/Disable verbose logging in score report generation.
*/
public void enableVerboseLogging(boolean enable) {
@@ -162,7 +140,7 @@ public class WifiScoreReport {
//report score
if (score != wifiInfo.score) {
if (mVerboseLoggingEnabled) {
- Log.d(TAG, " report new wifi score " + score);
+ Log.d(TAG, "report new wifi score " + score);
}
wifiInfo.score = score;
if (networkAgent != null) {
@@ -170,9 +148,67 @@ public class WifiScoreReport {
}
}
- mReport = String.format(Locale.US, " score=%d", score);
- mReportValid = true;
wifiMetrics.incrementWifiScoreCount(score);
+ mScore = score;
+ }
+
+ private static final double TIME_CONSTANT_MILLIS = 30.0e+3;
+ private static final long NUD_THROTTLE_MILLIS = 5000;
+ private long mLastKnownNudCheckTimeMillis = 0;
+ private int mLastKnownNudCheckScore = ConnectedScore.WIFI_TRANSITION_SCORE;
+ private int mNudYes = 0; // Counts when we voted for a NUD
+ private int mNudCount = 0; // Counts when we were told a NUD was sent
+
+ /**
+ * Recommends that a layer 3 check be done
+ *
+ * The caller can use this to (help) decide that an IP reachability check
+ * is desirable. The check is not done here; that is the caller's responsibility.
+ *
+ * @return true to indicate that an IP reachability check is recommended
+ */
+ public boolean shouldCheckIpLayer() {
+ int nud = mScoringParams.getNudKnob();
+ if (nud == 0) {
+ return false;
+ }
+ long millis = mClock.getWallClockMillis();
+ long deltaMillis = millis - mLastKnownNudCheckTimeMillis;
+ // Don't ever ask back-to-back - allow at least 5 seconds
+ // for the previous one to finish.
+ if (deltaMillis < NUD_THROTTLE_MILLIS) {
+ return false;
+ }
+ // nud is between 1 and 10 at this point
+ double deltaLevel = 11 - nud;
+ // nextNudBreach is the bar the score needs to cross before we ask for NUD
+ double nextNudBreach = ConnectedScore.WIFI_TRANSITION_SCORE;
+ // If we were below threshold the last time we checked, then compute a new bar
+ // that starts down from there and decays exponentially back up to the steady-state
+ // bar. If 5 time constants have passed, we are 99% of the way there, so skip the math.
+ if (mLastKnownNudCheckScore < ConnectedScore.WIFI_TRANSITION_SCORE
+ && deltaMillis < 5.0 * TIME_CONSTANT_MILLIS) {
+ double a = Math.exp(-deltaMillis / TIME_CONSTANT_MILLIS);
+ nextNudBreach = a * (mLastKnownNudCheckScore - deltaLevel) + (1.0 - a) * nextNudBreach;
+ }
+ if (mScore >= nextNudBreach) {
+ return false;
+ }
+ mNudYes++;
+ return true;
+ }
+
+ /**
+ * Should be called when a reachability check has been issued
+ *
+ * When the caller has requested an IP reachability check, calling this will
+ * help to rate-limit requests via shouldCheckIpLayer()
+ */
+ public void noteIpCheck() {
+ long millis = mClock.getWallClockMillis();
+ mLastKnownNudCheckTimeMillis = millis;
+ mLastKnownNudCheckScore = mScore;
+ mNudCount++;
}
/**
@@ -201,10 +237,11 @@ public class WifiScoreReport {
try {
String timestamp = new SimpleDateFormat("MM-dd HH:mm:ss.SSS").format(new Date(now));
s = String.format(Locale.US, // Use US to avoid comma/decimal confusion
- "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d",
+ "%s,%d,%d,%.1f,%.1f,%.1f,%d,%d,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d",
timestamp, mSessionNumber, netId,
rssi, filteredRssi, rssiThreshold, freq, linkSpeed,
txSuccessRate, txRetriesRate, txBadRate, rxSuccessRate,
+ mNudYes, mNudCount,
s1, s2, score);
} catch (Exception e) {
Log.e(TAG, "format problem", e);
@@ -235,7 +272,7 @@ public class WifiScoreReport {
history = new LinkedList<>(mLinkMetricsHistory);
}
pw.println("time,session,netid,rssi,filtered_rssi,rssi_threshold,"
- + "freq,linkspeed,tx_good,tx_retry,tx_bad,rx_pps,s1,s2,score");
+ + "freq,linkspeed,tx_good,tx_retry,tx_bad,rx_pps,nudrq,nuds,s1,s2,score");
for (String line : history) {
pw.println(line);
}
diff --git a/com/android/server/wifi/WifiServiceImpl.java b/com/android/server/wifi/WifiServiceImpl.java
index 3496b2c4..7d6c109c 100644
--- a/com/android/server/wifi/WifiServiceImpl.java
+++ b/com/android/server/wifi/WifiServiceImpl.java
@@ -54,10 +54,8 @@ import android.content.pm.ParceledListSlice;
import android.database.ContentObserver;
import android.net.DhcpInfo;
import android.net.DhcpResults;
-import android.net.IpConfiguration;
import android.net.Network;
import android.net.NetworkUtils;
-import android.net.StaticIpConfiguration;
import android.net.Uri;
import android.net.ip.IpClient;
import android.net.wifi.ISoftApCallback;
@@ -999,7 +997,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
.c(Binder.getCallingUid()).c(mode).flush();
// null wifiConfig is a meaningful input for CMD_SET_AP
- if (wifiConfig == null || isValid(wifiConfig)) {
+ if (wifiConfig == null || WifiApConfigStore.validateApWifiConfiguration(wifiConfig)) {
SoftApModeConfiguration softApConfig = new SoftApModeConfiguration(mode, wifiConfig);
mWifiController.sendMessage(CMD_SET_AP, 1, 0, softApConfig);
return true;
@@ -1520,12 +1518,13 @@ public class WifiServiceImpl extends IWifiManager.Stub {
/**
* see {@link WifiManager#setWifiApConfiguration(WifiConfiguration)}
* @param wifiConfig WifiConfiguration details for soft access point
- * @throws SecurityException if the caller does not have permission to write the sotap config
+ * @return boolean indicating success or failure of the operation
+ * @throws SecurityException if the caller does not have permission to write the softap config
*/
@Override
- public void setWifiApConfiguration(WifiConfiguration wifiConfig, String packageName) {
+ public boolean setWifiApConfiguration(WifiConfiguration wifiConfig, String packageName) {
if (enforceChangePermission(packageName) != MODE_ALLOWED) {
- return;
+ return false;
}
int uid = Binder.getCallingUid();
// only allow Settings UI to write the stored SoftApConfig
@@ -1536,13 +1535,15 @@ public class WifiServiceImpl extends IWifiManager.Stub {
}
mLog.info("setWifiApConfiguration uid=%").c(uid).flush();
if (wifiConfig == null)
- return;
- if (isValid(wifiConfig)) {
+ return false;
+ if (WifiApConfigStore.validateApWifiConfiguration(wifiConfig)) {
mWifiStateMachineHandler.post(() -> {
mWifiApConfigStore.setApConfiguration(wifiConfig);
});
+ return true;
} else {
Slog.e(TAG, "Invalid WifiConfiguration");
+ return false;
}
}
@@ -1942,13 +1943,13 @@ public class WifiServiceImpl extends IWifiManager.Stub {
== PackageManager.PERMISSION_GRANTED) {
hideDefaultMacAddress = false;
}
- if (mWifiPermissionsUtil.canAccessScanResults(callingPackage, uid)) {
- hideBssidAndSsid = false;
- }
+ mWifiPermissionsUtil.enforceCanAccessScanResults(callingPackage, uid);
+ hideBssidAndSsid = false;
} catch (RemoteException e) {
Log.e(TAG, "Error checking receiver permission", e);
} catch (SecurityException e) {
- Log.e(TAG, "Security exception checking receiver permission", e);
+ Log.e(TAG, "Security exception checking receiver permission"
+ + ", hiding ssid and bssid", e);
}
if (hideDefaultMacAddress) {
result.setMacAddress(WifiInfo.DEFAULT_MAC_ADDRESS);
@@ -1974,9 +1975,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
int uid = Binder.getCallingUid();
long ident = Binder.clearCallingIdentity();
try {
- if (!mWifiPermissionsUtil.canAccessScanResults(callingPackage, uid)) {
- return new ArrayList<ScanResult>();
- }
+ mWifiPermissionsUtil.enforceCanAccessScanResults(callingPackage, uid);
final List<ScanResult> scanResults = new ArrayList<>();
boolean success = mWifiInjector.getWifiStateMachineHandler().runWithScissors(() -> {
scanResults.addAll(mScanRequestProxy.getScanResults());
@@ -1985,6 +1984,8 @@ public class WifiServiceImpl extends IWifiManager.Stub {
Log.e(TAG, "Failed to post runnable to fetch scan results");
}
return scanResults;
+ } catch (SecurityException e) {
+ return new ArrayList<ScanResult>();
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2599,39 +2600,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
return false;
}
- public static boolean isValid(WifiConfiguration config) {
- String validity = checkValidity(config);
- return validity == null || logAndReturnFalse(validity);
- }
-
- public static String checkValidity(WifiConfiguration config) {
- if (config.allowedKeyManagement == null)
- return "allowed kmgmt";
-
- if (config.allowedKeyManagement.cardinality() > 1) {
- if (config.allowedKeyManagement.cardinality() != 2) {
- return "cardinality != 2";
- }
- if (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) {
- return "not WPA_EAP";
- }
- if ((!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.IEEE8021X))
- && (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK))) {
- return "not PSK or 8021X";
- }
- }
- if (config.getIpAssignment() == IpConfiguration.IpAssignment.STATIC) {
- StaticIpConfiguration staticIpConf = config.getStaticIpConfiguration();
- if (staticIpConf == null) {
- return "null StaticIpConfiguration";
- }
- if (staticIpConf.ipAddress == null) {
- return "null static ip Address";
- }
- }
- return null;
- }
-
@Override
public Network getCurrentNetwork() {
enforceAccessPermission();
diff --git a/com/android/server/wifi/WifiStateMachine.java b/com/android/server/wifi/WifiStateMachine.java
index eb3848dc..10050371 100644
--- a/com/android/server/wifi/WifiStateMachine.java
+++ b/com/android/server/wifi/WifiStateMachine.java
@@ -229,15 +229,23 @@ public class WifiStateMachine extends StateMachine {
private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
@Override
public void onDestroyed(String ifaceName) {
- sendMessage(CMD_INTERFACE_DESTROYED);
+ if (mInterfaceName != null && mInterfaceName.equals(ifaceName)) {
+ sendMessage(CMD_INTERFACE_DESTROYED);
+ }
}
@Override
public void onUp(String ifaceName) {
+ if (mInterfaceName != null && mInterfaceName.equals(ifaceName)) {
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1);
+ }
}
@Override
public void onDown(String ifaceName) {
+ if (mInterfaceName != null && mInterfaceName.equals(ifaceName)) {
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
+ }
}
};
private boolean mIpReachabilityDisconnectEnabled = true;
@@ -326,8 +334,6 @@ public class WifiStateMachine extends StateMachine {
*/
public static final String SUPPLICANT_BSSID_ANY = "any";
- private int mSupplicantRestartCount = 0;
-
/**
* The link properties of the wifi interface.
* Do not modify this directly; use updateLinkProperties instead.
@@ -459,12 +465,14 @@ public class WifiStateMachine extends StateMachine {
static final int CMD_STOP_SUPPLICANT = BASE + 12;
/* STA interface destroyed */
static final int CMD_INTERFACE_DESTROYED = BASE + 13;
+ /* STA interface down */
+ static final int CMD_INTERFACE_DOWN = BASE + 14;
/* Indicates Static IP succeeded */
static final int CMD_STATIC_IP_SUCCESS = BASE + 15;
/* Indicates Static IP failed */
static final int CMD_STATIC_IP_FAILURE = BASE + 16;
- /* A delayed message sent to start driver when it fail to come up */
- static final int CMD_DRIVER_START_TIMED_OUT = BASE + 19;
+ /* Interface status change */
+ static final int CMD_INTERFACE_STATUS_CHANGED = BASE + 20;
/* Start the soft access point */
static final int CMD_START_AP = BASE + 21;
@@ -769,8 +777,6 @@ public class WifiStateMachine extends StateMachine {
private State mDefaultState = new DefaultState();
/* Temporary initial state */
private State mInitialState = new InitialState();
- /* Driver loaded, waiting for supplicant to start */
- private State mSupplicantStartingState = new SupplicantStartingState();
/* Driver loaded and supplicant ready */
private State mSupplicantStartedState = new SupplicantStartedState();
/* Scan for networks, no connection will be established */
@@ -992,7 +998,6 @@ public class WifiStateMachine extends StateMachine {
// CHECKSTYLE:OFF IndentationCheck
addState(mDefaultState);
addState(mInitialState, mDefaultState);
- addState(mSupplicantStartingState, mInitialState);
addState(mSupplicantStartedState, mInitialState);
addState(mConnectModeState, mSupplicantStartedState);
addState(mL2ConnectedState, mConnectModeState);
@@ -1045,9 +1050,6 @@ public class WifiStateMachine extends StateMachine {
getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.RX_HS20_ANQP_ICON_EVENT,
getHandler());
- mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_CONNECTION_EVENT, getHandler());
- mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_DISCONNECTION_EVENT,
- getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT,
getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_REQUEST_IDENTITY,
@@ -2208,9 +2210,7 @@ public class WifiStateMachine extends StateMachine {
if (report != null) {
sb.append(" ").append(report);
}
- if (mWifiScoreReport.isLastReportValid()) {
- sb.append(mWifiScoreReport.getLastReport());
- }
+ sb.append(String.format(" score=%d", mWifiInfo.score));
break;
case CMD_START_CONNECT:
case WifiManager.CONNECT_NETWORK:
@@ -2890,17 +2890,6 @@ public class WifiStateMachine extends StateMachine {
mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
}
- private void handleSupplicantConnectionLoss(boolean killSupplicant) {
- /* Socket connection can be lost when we do a graceful shutdown
- * or when the driver is hung. Ensure supplicant is stopped here.
- */
- if (killSupplicant) {
- mWifiMonitor.stopAllMonitoring();
- }
- sendSupplicantConnectionChangedBroadcast(false);
- setWifiState(WIFI_STATE_DISABLED);
- }
-
void handlePreDhcpSetup() {
if (!mBluetoothConnectionActive) {
/*
@@ -3380,6 +3369,25 @@ public class WifiStateMachine extends StateMachine {
+ macRandomizationEnabled);
}
+ /**
+ * Handle the error case where our underlying interface went down (if we do not have mac
+ * randomization enabled (b/72459123).
+ *
+ * This method triggers SelfRecovery with the error of REASON_STA_IFACE_DOWN. SelfRecovery then
+ * decides if wifi should be restarted or disabled.
+ */
+ private void handleInterfaceDown() {
+ if (mEnableConnectedMacRandomization.get()) {
+ // interface will go down when mac randomization is active, skip
+ Log.d(TAG, "MacRandomization enabled, ignoring iface down");
+ return;
+ }
+
+ Log.e(TAG, "Detected an interface down, report failure to SelfRecovery");
+ // report a failure
+ mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_STA_IFACE_DOWN);
+ }
+
/********************************************************
* HSM states
*******************************************************/
@@ -3488,7 +3496,6 @@ public class WifiStateMachine extends StateMachine {
break;
case CMD_START_SUPPLICANT:
case CMD_STOP_SUPPLICANT:
- case CMD_DRIVER_START_TIMED_OUT:
case CMD_START_AP_FAILURE:
case CMD_STOP_AP:
case CMD_AP_STOPPED:
@@ -3496,8 +3503,6 @@ public class WifiStateMachine extends StateMachine {
case CMD_RECONNECT:
case CMD_REASSOCIATE:
case CMD_RELOAD_TLS_AND_RECONNECT:
- case WifiMonitor.SUP_CONNECTION_EVENT:
- case WifiMonitor.SUP_DISCONNECTION_EVENT:
case WifiMonitor.NETWORK_CONNECTION_EVENT:
case WifiMonitor.NETWORK_DISCONNECTION_EVENT:
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
@@ -3524,6 +3529,8 @@ public class WifiStateMachine extends StateMachine {
case CMD_SELECT_TX_POWER_SCENARIO:
case CMD_WIFINATIVE_FAILURE:
case CMD_INTERFACE_DESTROYED:
+ case CMD_INTERFACE_DOWN:
+ case CMD_INTERFACE_STATUS_CHANGED:
messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
break;
case CMD_START_AP:
@@ -3712,14 +3719,28 @@ public class WifiStateMachine extends StateMachine {
}
class InitialState extends State {
+ private boolean mIfaceIsUp;
+
+ private void onUpChanged(boolean isUp) {
+ if (isUp == mIfaceIsUp) {
+ return; // no change
+ }
+ mIfaceIsUp = isUp;
+ if (isUp) {
+ Log.d(TAG, "Client mode interface is up");
+ // for now, do nothing - client mode has never waited for iface up
+ } else {
+ // A driver/firmware hang can now put the interface in a down state.
+ // We detect the interface going down and recover from it
+ handleInterfaceDown();
+ }
+ }
+
private void cleanup() {
// tell scanning service that scans are not available - about to kill the interface and
// supplicant
sendWifiScanAvailable(false);
- // Tearing down the client interfaces below is going to stop our supplicant.
- mWifiMonitor.stopAllMonitoring();
-
mWifiNative.registerStatusListener(mWifiNativeStatusListener);
// TODO: This teardown should ideally be handled in STOP_SUPPLICANT to be consistent
// with other mode managers. But, client mode is not yet controlled by
@@ -3727,11 +3748,12 @@ public class WifiStateMachine extends StateMachine {
// TODO: Remove this big hammer. We cannot support concurrent interfaces with this!
mWifiNative.teardownAllInterfaces();
mInterfaceName = null;
+ mIfaceIsUp = false;
}
@Override
public void enter() {
- mWifiMonitor.stopAllMonitoring();
+ mIfaceIsUp = false;
mWifiStateTracker.updateState(WifiStateTracker.INVALID);
cleanup();
sendMessage(CMD_START_SUPPLICANT);
@@ -3751,15 +3773,17 @@ public class WifiStateMachine extends StateMachine {
transitionTo(mDefaultState);
break;
}
+ // now that we have the interface, initialize our up/down status
+ onUpChanged(mWifiNative.isInterfaceUp(mInterfaceName));
+
mIpClient = mFacade.makeIpClient(
mContext, mInterfaceName, new IpClientCallback());
mIpClient.setMulticastFilter(true);
if (mVerboseLoggingEnabled) log("Supplicant start successful");
registerForWifiMonitorEvents();
- mWifiMonitor.startMonitoring(mInterfaceName);
mWifiInjector.getWifiLastResortWatchdog().clearAllFailureCounts();
setSupplicantLogLevel();
- transitionTo(mSupplicantStartingState);
+ transitionTo(mSupplicantStartedState);
break;
case CMD_SET_OPERATIONAL_MODE:
if (message.arg1 == CONNECT_MODE) {
@@ -3773,46 +3797,20 @@ public class WifiStateMachine extends StateMachine {
WifiDiagnostics.REPORT_REASON_WIFINATIVE_FAILURE);
mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_WIFINATIVE_FAILURE);
break;
- default:
- return NOT_HANDLED;
- }
- return HANDLED;
- }
- }
-
- class SupplicantStartingState extends State {
-
- @Override
- public boolean processMessage(Message message) {
- logStateAndMessage(message, this);
-
- switch(message.what) {
- case WifiMonitor.SUP_CONNECTION_EVENT:
- if (mVerboseLoggingEnabled) log("Supplicant connection established");
-
- mSupplicantRestartCount = 0;
- /* Reset the supplicant state to indicate the supplicant
- * state is not known at this time */
- mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
- /* Initialize data structures */
- mLastBssid = null;
- mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
- mLastSignalLevel = -1;
- mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
- // Attempt to migrate data out of legacy store.
- if (!mWifiConfigManager.migrateFromLegacyStore()) {
- Log.e(TAG, "Failed to migrate from legacy config store");
+ case CMD_INTERFACE_STATUS_CHANGED:
+ boolean isUp = message.arg1 == 1;
+ // For now, this message can be triggered due to link state and/or interface
+ // status changes (b/77218676). First check if we really see an iface down by
+ // consulting our view of supplicant state.
+ if (!isUp && SupplicantState.isDriverActive(mWifiInfo.getSupplicantState())) {
+ // the driver is active, so this could just be part of normal operation, do
+ // not disable wifi in these cases (ex, a network was removed) or worry
+ // about the link status
+ break;
}
- sendSupplicantConnectionChangedBroadcast(true);
- transitionTo(mSupplicantStartedState);
- break;
- case WifiMonitor.SUP_DISCONNECTION_EVENT:
- // since control is split between WSM and WSMP - do not worry about supplicant
- // dying if we haven't seen it up yet
+
+ onUpChanged(isUp);
break;
- case CMD_START_SUPPLICANT:
- case CMD_STOP_SUPPLICANT:
- case CMD_STOP_AP:
default:
return NOT_HANDLED;
}
@@ -3827,6 +3825,19 @@ public class WifiStateMachine extends StateMachine {
logd("SupplicantStartedState enter");
}
+ // reset state related to supplicant starting
+ mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
+ // Initialize data structures
+ mLastBssid = null;
+ mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
+ mLastSignalLevel = -1;
+ mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
+ // Attempt to migrate data out of legacy store.
+ if (!mWifiConfigManager.migrateFromLegacyStore()) {
+ Log.e(TAG, "Failed to migrate from legacy config store");
+ }
+ sendSupplicantConnectionChangedBroadcast(true);
+
mWifiNative.setExternalSim(mInterfaceName, true);
setRandomMacOui();
@@ -3897,19 +3908,6 @@ public class WifiStateMachine extends StateMachine {
logStateAndMessage(message, this);
switch(message.what) {
- case WifiMonitor.SUP_DISCONNECTION_EVENT: /* Supplicant connection lost */
- // first check if we are expecting a mode switch
- if (mModeChange) {
- logd("expecting a mode change, do not restart supplicant");
- return HANDLED;
- }
- loge("Connection lost, restart supplicant");
- handleSupplicantConnectionLoss(true);
- handleNetworkDisconnect();
- mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
- sendMessage(CMD_START_SUPPLICANT);
- transitionTo(mInitialState);
- break;
case CMD_TARGET_BSSID:
// Trying to associate to this BSSID
if (message.obj != null) {
@@ -4082,12 +4080,6 @@ public class WifiStateMachine extends StateMachine {
case WifiManager.FORGET_NETWORK:
s = "FORGET_NETWORK";
break;
- case WifiMonitor.SUP_CONNECTION_EVENT:
- s = "SUP_CONNECTION_EVENT";
- break;
- case WifiMonitor.SUP_DISCONNECTION_EVENT:
- s = "SUP_DISCONNECTION_EVENT";
- break;
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
s = "SUPPLICANT_STATE_CHANGE_EVENT";
break;
@@ -4295,7 +4287,9 @@ public class WifiStateMachine extends StateMachine {
mSupplicantStateTracker.sendMessage(WifiMonitor.ASSOCIATION_REJECTION_EVENT);
// If rejection occurred while Metrics is tracking a ConnnectionEvent, end it.
reportConnectionAttemptEnd(
- WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_REJECTION,
+ timedOut
+ ? WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_TIMED_OUT
+ : WifiMetrics.ConnectionEvent.FAILURE_ASSOCIATION_REJECTION,
WifiMetricsProto.ConnectionEvent.HLF_NONE);
mWifiInjector.getWifiLastResortWatchdog()
.noteConnectionFailureAndTriggerIfNeeded(
@@ -4336,20 +4330,6 @@ public class WifiStateMachine extends StateMachine {
break;
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
SupplicantState state = handleSupplicantStateChange(message);
- // A driver/firmware hang can now put the interface in a down state.
- // We detect the interface going down and recover from it
- if (!SupplicantState.isDriverActive(state) && !mModeChange
- && !mEnableConnectedMacRandomization.get()) {
- if (mNetworkInfo.getState() != NetworkInfo.State.DISCONNECTED) {
- handleNetworkDisconnect();
- }
- log("Detected an interface down, restart driver");
- // Rely on the fact that this will force us into killing supplicant and then
- // restart supplicant from a clean state.
- sendMessage(CMD_START_SUPPLICANT);
- transitionTo(mInitialState);
- break;
- }
// Supplicant can fail to report a NETWORK_DISCONNECTION_EVENT
// when authentication times out after a successful connection,
@@ -4370,7 +4350,20 @@ public class WifiStateMachine extends StateMachine {
// interest (e.g. routers); harmless if none are configured.
if (state == SupplicantState.COMPLETED) {
mIpClient.confirmConfiguration();
+ mWifiScoreReport.noteIpCheck();
+ }
+
+ if (!SupplicantState.isDriverActive(state)) {
+ // still use supplicant to detect interface down while work to
+ // mitigate b/77218676 is in progress
+ // note: explicitly using this command to dedup iface down notification
+ // paths (onUpChanged filters out duplicate updates)
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "detected interface down via supplicant");
+ }
}
+
break;
case WifiP2pServiceImpl.DISCONNECT_WIFI_REQUEST:
if (message.arg1 == 1) {
@@ -5195,6 +5188,10 @@ public class WifiStateMachine extends StateMachine {
// Send the update score to network agent.
mWifiScoreReport.calculateAndReportScore(
mWifiInfo, mNetworkAgent, mWifiMetrics);
+ if (mWifiScoreReport.shouldCheckIpLayer()) {
+ mIpClient.confirmConfiguration();
+ mWifiScoreReport.noteIpCheck();
+ }
}
sendMessageDelayed(obtainMessage(CMD_RSSI_POLL, mRssiPollToken, 0),
mPollRssiIntervalMsecs);
diff --git a/com/android/server/wifi/WifiWakeMetrics.java b/com/android/server/wifi/WifiWakeMetrics.java
index fba72369..5b700062 100644
--- a/com/android/server/wifi/WifiWakeMetrics.java
+++ b/com/android/server/wifi/WifiWakeMetrics.java
@@ -42,6 +42,8 @@ public class WifiWakeMetrics {
private boolean mIsInSession = false;
private int mTotalSessions = 0;
+ private int mTotalWakeups = 0;
+ private int mIgnoredStarts = 0;
private final Object mLock = new Object();
@@ -50,7 +52,7 @@ public class WifiWakeMetrics {
*
* <p>Starts the session.
*
- * @param numNetworks The total number of networks stored in the WakeupLock at initialization.
+ * @param numNetworks The total number of networks stored in the WakeupLock at start.
*/
public void recordStartEvent(int numNetworks) {
synchronized (mLock) {
@@ -60,10 +62,29 @@ public class WifiWakeMetrics {
}
/**
+ * Records the initialize event of the current Wifi Wake session.
+ *
+ * <p>Note: The start event must be recorded before this event, otherwise this call will be
+ * ignored.
+ *
+ * @param numScans The total number of elapsed scans since start.
+ * @param numNetworks The total number of networks in the lock.
+ */
+ public void recordInitializeEvent(int numScans, int numNetworks) {
+ synchronized (mLock) {
+ if (!mIsInSession) {
+ return;
+ }
+ mCurrentSession.recordInitializeEvent(numScans, numNetworks,
+ SystemClock.elapsedRealtime());
+ }
+ }
+
+ /**
* Records the unlock event of the current Wifi Wake session.
*
* <p>The unlock event occurs when the WakeupLock has all of its networks removed. This event
- * will not be recorded if the start event recorded 0 locked networks.
+ * will not be recorded if the initialize event recorded 0 locked networks.
*
* <p>Note: The start event must be recorded before this event, otherwise this call will be
* ignored.
@@ -116,6 +137,11 @@ public class WifiWakeMetrics {
}
mCurrentSession.recordResetEvent(numScans, SystemClock.elapsedRealtime());
+ // tally successful wakeups here since this is the actual point when wifi is turned on
+ if (mCurrentSession.hasWakeupTriggered()) {
+ mTotalWakeups++;
+ }
+
mTotalSessions++;
if (mSessions.size() < MAX_RECORDED_SESSIONS) {
mSessions.add(mCurrentSession);
@@ -125,12 +151,21 @@ public class WifiWakeMetrics {
}
/**
+ * Records instance of the start event being ignored due to the controller already being active.
+ */
+ public void recordIgnoredStart() {
+ mIgnoredStarts++;
+ }
+
+ /**
* Returns the consolidated WifiWakeStats proto for WifiMetrics.
*/
public WifiWakeStats buildProto() {
WifiWakeStats proto = new WifiWakeStats();
proto.numSessions = mTotalSessions;
+ proto.numWakeups = mTotalWakeups;
+ proto.numIgnoredStarts = mIgnoredStarts;
proto.sessions = new WifiWakeStats.Session[mSessions.size()];
for (int i = 0; i < mSessions.size(); i++) {
@@ -148,6 +183,8 @@ public class WifiWakeMetrics {
synchronized (mLock) {
pw.println("-------WifiWake metrics-------");
pw.println("mTotalSessions: " + mTotalSessions);
+ pw.println("mTotalWakeups: " + mTotalWakeups);
+ pw.println("mIgnoredStarts: " + mIgnoredStarts);
pw.println("mIsInSession: " + mIsInSession);
pw.println("Stored Sessions: " + mSessions.size());
for (Session session : mSessions) {
@@ -167,21 +204,29 @@ public class WifiWakeMetrics {
* <p>Keeps the current WifiWake session.
*/
public void clear() {
- mSessions.clear();
- mTotalSessions = 0;
+ synchronized (mLock) {
+ mSessions.clear();
+ mTotalSessions = 0;
+ mTotalWakeups = 0;
+ mIgnoredStarts = 0;
+ }
}
/** A single WifiWake session. */
public static class Session {
private final long mStartTimestamp;
- private final int mNumNetworks;
+ private final int mStartNetworks;
+ private int mInitializeNetworks = 0;
@VisibleForTesting
@Nullable
Event mUnlockEvent;
@VisibleForTesting
@Nullable
+ Event mInitEvent;
+ @VisibleForTesting
+ @Nullable
Event mWakeupEvent;
@VisibleForTesting
@Nullable
@@ -189,11 +234,27 @@ public class WifiWakeMetrics {
/** Creates a new WifiWake session. */
public Session(int numNetworks, long timestamp) {
- mNumNetworks = numNetworks;
+ mStartNetworks = numNetworks;
mStartTimestamp = timestamp;
}
/**
+ * Records an initialize event.
+ *
+ * <p>Ignores subsequent calls.
+ *
+ * @param numScans Total number of scans at the time of this event.
+ * @param numNetworks Total number of networks in the lock.
+ * @param timestamp The timestamp of the event.
+ */
+ public void recordInitializeEvent(int numScans, int numNetworks, long timestamp) {
+ if (mInitEvent == null) {
+ mInitializeNetworks = numNetworks;
+ mInitEvent = new Event(numScans, timestamp - mStartTimestamp);
+ }
+ }
+
+ /**
* Records an unlock event.
*
* <p>Ignores subsequent calls.
@@ -222,6 +283,13 @@ public class WifiWakeMetrics {
}
/**
+ * Returns whether the current session has had its wakeup event triggered.
+ */
+ public boolean hasWakeupTriggered() {
+ return mWakeupEvent != null;
+ }
+
+ /**
* Records a reset event.
*
* <p>Ignores subsequent calls.
@@ -239,8 +307,12 @@ public class WifiWakeMetrics {
public WifiWakeStats.Session buildProto() {
WifiWakeStats.Session sessionProto = new WifiWakeStats.Session();
sessionProto.startTimeMillis = mStartTimestamp;
- sessionProto.lockedNetworksAtStart = mNumNetworks;
+ sessionProto.lockedNetworksAtStart = mStartNetworks;
+ if (mInitEvent != null) {
+ sessionProto.lockedNetworksAtInitialize = mInitializeNetworks;
+ sessionProto.initializeEvent = mInitEvent.buildProto();
+ }
if (mUnlockEvent != null) {
sessionProto.unlockEvent = mUnlockEvent.buildProto();
}
@@ -258,7 +330,9 @@ public class WifiWakeMetrics {
public void dump(PrintWriter pw) {
pw.println("WifiWakeMetrics.Session:");
pw.println("mStartTimestamp: " + mStartTimestamp);
- pw.println("mNumNetworks: " + mNumNetworks);
+ pw.println("mStartNetworks: " + mStartNetworks);
+ pw.println("mInitializeNetworks: " + mInitializeNetworks);
+ pw.println("mInitEvent: " + (mInitEvent == null ? "{}" : mInitEvent.toString()));
pw.println("mUnlockEvent: " + (mUnlockEvent == null ? "{}" : mUnlockEvent.toString()));
pw.println("mWakeupEvent: " + (mWakeupEvent == null ? "{}" : mWakeupEvent.toString()));
pw.println("mResetEvent: " + (mResetEvent == null ? "{}" : mResetEvent.toString()));
diff --git a/com/android/server/wifi/p2p/WifiP2pServiceImpl.java b/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
index b525555a..fdad6574 100644
--- a/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
+++ b/com/android/server/wifi/p2p/WifiP2pServiceImpl.java
@@ -3459,7 +3459,6 @@ public class WifiP2pServiceImpl extends IWifiP2pManager.Stub {
*/
private WifiP2pDeviceList getPeers(Bundle pkg, int uid) {
String pkgName = pkg.getString(WifiP2pManager.CALLING_PACKAGE);
- boolean scanPermission = false;
WifiPermissionsUtil wifiPermissionsUtil;
// getPeers() is guaranteed to be invoked after Wifi Service is up
// This ensures getInstance() will return a non-null object now
@@ -3468,13 +3467,10 @@ public class WifiP2pServiceImpl extends IWifiP2pManager.Stub {
}
wifiPermissionsUtil = mWifiInjector.getWifiPermissionsUtil();
try {
- scanPermission = wifiPermissionsUtil.canAccessScanResults(pkgName, uid);
- } catch (SecurityException e) {
- Log.e(TAG, "Security Exception, cannot access peer list");
- }
- if (scanPermission) {
+ wifiPermissionsUtil.enforceCanAccessScanResults(pkgName, uid);
return new WifiP2pDeviceList(mPeers);
- } else {
+ } catch (SecurityException e) {
+ Log.v(TAG, "Security Exception, cannot access peer list");
return new WifiP2pDeviceList();
}
}
diff --git a/com/android/server/wifi/util/WifiPermissionsUtil.java b/com/android/server/wifi/util/WifiPermissionsUtil.java
index 0f333d49..3d838645 100644
--- a/com/android/server/wifi/util/WifiPermissionsUtil.java
+++ b/com/android/server/wifi/util/WifiPermissionsUtil.java
@@ -166,12 +166,12 @@ public class WifiPermissionsUtil {
}
/**
- * API to determine if the caller has permissions to get scan results.
+ * API to determine if the caller has permissions to get scan results. Throws SecurityException
+ * if the caller has no permission.
* @param pkgName package name of the application requesting access
* @param uid The uid of the package
- * @return boolean true or false if permissions is granted
*/
- public boolean canAccessScanResults(String pkgName, int uid) throws SecurityException {
+ public void enforceCanAccessScanResults(String pkgName, int uid) throws SecurityException {
mAppOps.checkPackage(uid, pkgName);
// Check if the calling Uid has CAN_READ_PEER_MAC_ADDRESS permission.
boolean canCallingUidAccessLocation = checkCallerHasPeersMacAddressPermission(uid);
@@ -192,22 +192,18 @@ public class WifiPermissionsUtil {
if (!canCallingUidAccessLocation && !canAppPackageUseLocation) {
// also check if it is a connectivity app
if (!appTypeConnectivity) {
- mLog.tC("Denied: no location permission");
- return false;
+ throw new SecurityException("UID " + uid + " has no location permission");
}
}
// Check if Wifi Scan request is an operation allowed for this App.
if (!isScanAllowedbyApps(pkgName, uid)) {
- mLog.tC("Denied: app wifi scan not allowed");
- return false;
+ throw new SecurityException("UID " + uid + " has no wifi scan permission");
}
// If the User or profile is current, permission is granted
// Otherwise, uid must have INTERACT_ACROSS_USERS_FULL permission.
if (!isCurrentProfile(uid) && !checkInteractAcrossUsersFull(uid)) {
- mLog.tC("Denied: Profile not permitted");
- return false;
+ throw new SecurityException("UID " + uid + " profile not permitted");
}
- return true;
}
/**
diff --git a/com/android/server/wm/AccessibilityController.java b/com/android/server/wm/AccessibilityController.java
index 641a1ba6..608d0aa5 100644
--- a/com/android/server/wm/AccessibilityController.java
+++ b/com/android/server/wm/AccessibilityController.java
@@ -1102,35 +1102,37 @@ final class AccessibilityController {
}
}
- // Account for the space this window takes if the window
- // is not an accessibility overlay which does not change
- // the reported windows.
if (windowState.mAttrs.type !=
WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY) {
- unaccountedSpace.op(boundsInScreen, unaccountedSpace,
- Region.Op.REVERSE_DIFFERENCE);
- }
- // If a window is modal it prevents other windows from being touched
- if ((flags & (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)) == 0) {
- // Account for all space in the task, whether the windows in it are
- // touchable or not. The modal window blocks all touches from the task's
- // area.
- unaccountedSpace.op(windowState.getDisplayFrameLw(), unaccountedSpace,
+ // Account for the space this window takes if the window
+ // is not an accessibility overlay which does not change
+ // the reported windows.
+ unaccountedSpace.op(boundsInScreen, unaccountedSpace,
Region.Op.REVERSE_DIFFERENCE);
- if (task != null) {
- // If the window is associated with a particular task, we can skip the
- // rest of the windows for that task.
- skipRemainingWindowsForTasks.add(task.mTaskId);
- continue;
- } else {
- // If the window is not associated with a particular task, then it is
- // globally modal. In this case we can skip all remaining windows.
- break;
+ // If a window is modal it prevents other windows from being touched
+ if ((flags & (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)) == 0) {
+ // Account for all space in the task, whether the windows in it are
+ // touchable or not. The modal window blocks all touches from the task's
+ // area.
+ unaccountedSpace.op(windowState.getDisplayFrameLw(), unaccountedSpace,
+ Region.Op.REVERSE_DIFFERENCE);
+
+ if (task != null) {
+ // If the window is associated with a particular task, we can skip the
+ // rest of the windows for that task.
+ skipRemainingWindowsForTasks.add(task.mTaskId);
+ continue;
+ } else {
+ // If the window is not associated with a particular task, then it is
+ // globally modal. In this case we can skip all remaining windows.
+ break;
+ }
}
}
+
// We figured out what is touchable for the entire screen - done.
if (unaccountedSpace.isEmpty()) {
break;
diff --git a/com/android/server/wm/AnimatingAppWindowTokenRegistry.java b/com/android/server/wm/AnimatingAppWindowTokenRegistry.java
index 416469bd..9c00d1ab 100644
--- a/com/android/server/wm/AnimatingAppWindowTokenRegistry.java
+++ b/com/android/server/wm/AnimatingAppWindowTokenRegistry.java
@@ -37,6 +37,8 @@ class AnimatingAppWindowTokenRegistry {
private ArrayList<Runnable> mTmpRunnableList = new ArrayList<>();
+ private boolean mEndingDeferredFinish;
+
/**
* Notifies that an {@link AppWindowToken} has started animating.
*/
@@ -50,6 +52,11 @@ class AnimatingAppWindowTokenRegistry {
void notifyFinished(AppWindowToken token) {
mAnimatingTokens.remove(token);
mFinishedTokens.remove(token);
+
+ // If we were the last token, make sure the end all deferred finishes.
+ if (mAnimatingTokens.isEmpty()) {
+ endDeferringFinished();
+ }
}
/**
@@ -78,16 +85,28 @@ class AnimatingAppWindowTokenRegistry {
}
private void endDeferringFinished() {
- // Copy it into a separate temp list to avoid modifying the collection while iterating as
- // calling the callback may call back into notifyFinished.
- for (int i = mFinishedTokens.size() - 1; i >= 0; i--) {
- mTmpRunnableList.add(mFinishedTokens.valueAt(i));
+
+ // Don't start recursing. Running the finished listener invokes notifyFinished, which may
+ // invoked us again.
+ if (mEndingDeferredFinish) {
+ return;
}
- mFinishedTokens.clear();
- for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) {
- mTmpRunnableList.get(i).run();
+ try {
+ mEndingDeferredFinish = true;
+
+ // Copy it into a separate temp list to avoid modifying the collection while iterating
+ // as calling the callback may call back into notifyFinished.
+ for (int i = mFinishedTokens.size() - 1; i >= 0; i--) {
+ mTmpRunnableList.add(mFinishedTokens.valueAt(i));
+ }
+ mFinishedTokens.clear();
+ for (int i = mTmpRunnableList.size() - 1; i >= 0; i--) {
+ mTmpRunnableList.get(i).run();
+ }
+ mTmpRunnableList.clear();
+ } finally {
+ mEndingDeferredFinish = false;
}
- mTmpRunnableList.clear();
}
void dump(PrintWriter pw, String header, String prefix) {
diff --git a/com/android/server/wm/AppWindowContainerController.java b/com/android/server/wm/AppWindowContainerController.java
index 1575694d..165a4090 100644
--- a/com/android/server/wm/AppWindowContainerController.java
+++ b/com/android/server/wm/AppWindowContainerController.java
@@ -113,63 +113,73 @@ public class AppWindowContainerController
mListener.onWindowsGone();
};
- private final Runnable mAddStartingWindow = () -> {
- final StartingData startingData;
- final AppWindowToken container;
+ private final Runnable mAddStartingWindow = new Runnable() {
- synchronized (mWindowMap) {
- if (mContainer == null) {
- if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "mContainer was null while trying to"
- + " add starting window");
- return;
- }
- startingData = mContainer.startingData;
- container = mContainer;
- }
+ @Override
+ public void run() {
+ final StartingData startingData;
+ final AppWindowToken container;
+
+ synchronized (mWindowMap) {
+ if (mContainer == null) {
+ if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "mContainer was null while trying to"
+ + " add starting window");
+ return;
+ }
- if (startingData == null) {
- // Animation has been canceled... do nothing.
- if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "startingData was nulled out before handling"
- + " mAddStartingWindow: " + mContainer);
- return;
- }
+ // There can only be one adding request, silly caller!
+ mService.mAnimationHandler.removeCallbacks(this);
- if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Add starting "
- + this + ": startingData=" + container.startingData);
+ startingData = mContainer.startingData;
+ container = mContainer;
+ }
- StartingSurface surface = null;
- try {
- surface = startingData.createStartingSurface(container);
- } catch (Exception e) {
- Slog.w(TAG_WM, "Exception when adding starting window", e);
- }
- if (surface != null) {
- boolean abort = false;
- synchronized(mWindowMap) {
- // If the window was successfully added, then
- // we need to remove it.
- if (container.removed || container.startingData == null) {
- if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
- "Aborted starting " + container
- + ": removed=" + container.removed
- + " startingData=" + container.startingData);
- container.startingWindow = null;
- container.startingData = null;
- abort = true;
- } else {
- container.startingSurface = surface;
- }
- if (DEBUG_STARTING_WINDOW && !abort) Slog.v(TAG_WM,
- "Added starting " + mContainer
- + ": startingWindow="
- + container.startingWindow + " startingView="
- + container.startingSurface);
+ if (startingData == null) {
+ // Animation has been canceled... do nothing.
+ if (DEBUG_STARTING_WINDOW)
+ Slog.v(TAG_WM, "startingData was nulled out before handling"
+ + " mAddStartingWindow: " + mContainer);
+ return;
}
- if (abort) {
- surface.remove();
+
+ if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Add starting "
+ + AppWindowContainerController.this + ": startingData="
+ + container.startingData);
+
+ StartingSurface surface = null;
+ try {
+ surface = startingData.createStartingSurface(container);
+ } catch (Exception e) {
+ Slog.w(TAG_WM, "Exception when adding starting window", e);
+ }
+ if (surface != null) {
+ boolean abort = false;
+ synchronized (mWindowMap) {
+ // If the window was successfully added, then
+ // we need to remove it.
+ if (container.removed || container.startingData == null) {
+ if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
+ "Aborted starting " + container
+ + ": removed=" + container.removed
+ + " startingData=" + container.startingData);
+ container.startingWindow = null;
+ container.startingData = null;
+ abort = true;
+ } else {
+ container.startingSurface = surface;
+ }
+ if (DEBUG_STARTING_WINDOW && !abort) Slog.v(TAG_WM,
+ "Added starting " + mContainer
+ + ": startingWindow="
+ + container.startingWindow + " startingView="
+ + container.startingSurface);
+ }
+ if (abort) {
+ surface.remove();
+ }
+ } else if (DEBUG_STARTING_WINDOW) {
+ Slog.v(TAG_WM, "Surface returned was null: " + mContainer);
}
- } else if (DEBUG_STARTING_WINDOW) {
- Slog.v(TAG_WM, "Surface returned was null: " + mContainer);
}
};
@@ -558,8 +568,10 @@ public class AppWindowContainerController
// Note: we really want to do sendMessageAtFrontOfQueue() because we
// want to process the message ASAP, before any other queued
// messages.
- if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Enqueueing ADD_STARTING");
- mService.mAnimationHandler.postAtFrontOfQueue(mAddStartingWindow);
+ if (!mService.mAnimationHandler.hasCallbacks(mAddStartingWindow)) {
+ if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Enqueueing ADD_STARTING");
+ mService.mAnimationHandler.postAtFrontOfQueue(mAddStartingWindow);
+ }
}
private boolean createSnapshot(TaskSnapshot snapshot) {
diff --git a/com/android/server/wm/AppWindowToken.java b/com/android/server/wm/AppWindowToken.java
index f19c554a..5676f588 100644
--- a/com/android/server/wm/AppWindowToken.java
+++ b/com/android/server/wm/AppWindowToken.java
@@ -1312,7 +1312,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (prevWinMode != WINDOWING_MODE_UNDEFINED && winMode == WINDOWING_MODE_PINNED) {
// Entering PiP from fullscreen, reset the snap fraction
mDisplayContent.mPinnedStackControllerLocked.resetReentrySnapFraction(this);
- } else if (prevWinMode == WINDOWING_MODE_PINNED && winMode != WINDOWING_MODE_UNDEFINED) {
+ } else if (prevWinMode == WINDOWING_MODE_PINNED && winMode != WINDOWING_MODE_UNDEFINED
+ && !isHidden()) {
// Leaving PiP to fullscreen, save the snap fraction based on the pre-animation bounds
// for the next re-entry into PiP (assuming the activity is not hidden or destroyed)
final TaskStack pinnedStack = mDisplayContent.getPinnedStack();
@@ -1714,7 +1715,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
adapter = new LocalAnimationAdapter(
new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
mService.mAppTransition.canSkipFirstFrame(),
- mService.mAppTransition.getAppStackClipMode()),
+ mService.mAppTransition.getAppStackClipMode(),
+ true /* isAppAnimation */),
mService.mSurfaceAnimationRunner);
if (a.getZAdjustment() == Animation.ZORDER_TOP) {
mNeedsZBoost = true;
@@ -1887,7 +1889,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
"AppWindowToken");
clearThumbnail();
- setClientHidden(hiddenRequested);
+ setClientHidden(isHidden() && hiddenRequested);
if (mService.mInputMethodTarget != null && mService.mInputMethodTarget.mAppToken == this) {
getDisplayContent().computeImeTarget(true /* updateImeTarget */);
diff --git a/com/android/server/wm/BoundsAnimationController.java b/com/android/server/wm/BoundsAnimationController.java
index ba67ff6a..112d93c7 100644
--- a/com/android/server/wm/BoundsAnimationController.java
+++ b/com/android/server/wm/BoundsAnimationController.java
@@ -161,7 +161,10 @@ public class BoundsAnimationController {
// Timeout callback to ensure we continue the animation if waiting for resuming or app
// windows drawn fails
- private final Runnable mResumeRunnable = () -> resume();
+ private final Runnable mResumeRunnable = () -> {
+ if (DEBUG) Slog.d(TAG, "pause: timed out waiting for windows drawn");
+ resume();
+ };
BoundsAnimator(BoundsAnimationTarget target, Rect from, Rect to,
@SchedulePipModeChangedState int schedulePipModeChangedState,
@@ -213,7 +216,7 @@ public class BoundsAnimationController {
// When starting an animation from fullscreen, pause here and wait for the
// windows-drawn signal before we start the rest of the transition down into PiP.
- if (mMoveFromFullscreen) {
+ if (mMoveFromFullscreen && mTarget.shouldDeferStartOnMoveToFullscreen()) {
pause();
}
} else if (mPrevSchedulePipModeChangedState == SCHEDULE_PIP_MODE_CHANGED_ON_END &&
diff --git a/com/android/server/wm/BoundsAnimationTarget.java b/com/android/server/wm/BoundsAnimationTarget.java
index 647a2d6d..68be4e84 100644
--- a/com/android/server/wm/BoundsAnimationTarget.java
+++ b/com/android/server/wm/BoundsAnimationTarget.java
@@ -34,6 +34,12 @@ interface BoundsAnimationTarget {
void onAnimationStart(boolean schedulePipModeChangedCallback, boolean forceUpdate);
/**
+ * @return Whether the animation should be paused waiting for the windows to draw before
+ * entering PiP.
+ */
+ boolean shouldDeferStartOnMoveToFullscreen();
+
+ /**
* Sets the size of the target (without any intermediate steps, like scheduling animation)
* but freezes the bounds of any tasks in the target at taskBounds, to allow for more
* flexibility during resizing. Only works for the pinned stack at the moment. This will
diff --git a/com/android/server/wm/DisplayContent.java b/com/android/server/wm/DisplayContent.java
index c4f2bd4d..79eb2c9e 100644
--- a/com/android/server/wm/DisplayContent.java
+++ b/com/android/server/wm/DisplayContent.java
@@ -3568,7 +3568,8 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
if (s.inSplitScreenWindowingMode() && mSplitScreenDividerAnchor != null) {
t.setLayer(mSplitScreenDividerAnchor, layer++);
}
- if (s.isAppAnimating() && state != ALWAYS_ON_TOP_STATE) {
+ if ((s.isTaskAnimating() || s.isAppAnimating())
+ && state != ALWAYS_ON_TOP_STATE) {
// Ensure the animation layer ends up above the
// highest animating stack and no higher.
layerForAnimationLayer = layer++;
@@ -3727,8 +3728,12 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mLastWindowForcedOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
- if (policy.isKeyguardShowingAndNotOccluded()
- || mService.mAppTransition.getAppTransition() == TRANSIT_KEYGUARD_UNOCCLUDE) {
+ // Only allow force setting the orientation when all unknown visibilities have been
+ // resolved, as otherwise we just may be starting another occluding activity.
+ final boolean isUnoccluding =
+ mService.mAppTransition.getAppTransition() == TRANSIT_KEYGUARD_UNOCCLUDE
+ && mService.mUnknownAppVisibilityController.allResolved();
+ if (policy.isKeyguardShowingAndNotOccluded() || isUnoccluding) {
return mLastKeyguardForcedOrientation;
}
diff --git a/com/android/server/wm/DockedStackDividerController.java b/com/android/server/wm/DockedStackDividerController.java
index 5e2bb10f..2cd2ef12 100644
--- a/com/android/server/wm/DockedStackDividerController.java
+++ b/com/android/server/wm/DockedStackDividerController.java
@@ -666,13 +666,16 @@ public class DockedStackDividerController {
}
final TaskStack topSecondaryStack = mDisplayContent.getTopStackInWindowingMode(
WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+ final RecentsAnimationController recentsAnim = mService.getRecentsAnimationController();
+ final boolean minimizedForRecentsAnimation = recentsAnim != null &&
+ recentsAnim.isSplitScreenMinimized();
boolean homeVisible = homeTask.getTopVisibleAppToken() != null;
if (homeVisible && topSecondaryStack != null) {
// Home should only be considered visible if it is greater or equal to the top secondary
// stack in terms of z-order.
homeVisible = homeStack.compareTo(topSecondaryStack) >= 0;
}
- setMinimizedDockedStack(homeVisible, animate);
+ setMinimizedDockedStack(homeVisible || minimizedForRecentsAnimation, animate);
}
private boolean isWithinDisplay(Task task) {
diff --git a/com/android/server/wm/LocalAnimationAdapter.java b/com/android/server/wm/LocalAnimationAdapter.java
index 529aacc0..d89d6f05 100644
--- a/com/android/server/wm/LocalAnimationAdapter.java
+++ b/com/android/server/wm/LocalAnimationAdapter.java
@@ -146,6 +146,13 @@ class LocalAnimationAdapter implements AnimationAdapter {
return false;
}
+ /**
+ * @return {@code true} if we need to wake-up SurfaceFlinger earlier during this animation.
+ *
+ * @see Transaction#setEarlyWakeup
+ */
+ default boolean needsEarlyWakeup() { return false; }
+
void dump(PrintWriter pw, String prefix);
default void writeToProto(ProtoOutputStream proto, long fieldId) {
diff --git a/com/android/server/wm/RecentsAnimationController.java b/com/android/server/wm/RecentsAnimationController.java
index 7274aee3..1ee642a5 100644
--- a/com/android/server/wm/RecentsAnimationController.java
+++ b/com/android/server/wm/RecentsAnimationController.java
@@ -17,18 +17,18 @@
package com.android.server.wm;
import static android.app.ActivityManagerInternal.APP_TRANSITION_RECENTS_ANIM;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
-import static com.android.server.wm.WindowManagerService.H.NOTIFY_APP_TRANSITION_STARTING;
-import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
import static com.android.server.wm.AnimationAdapterProto.REMOTE;
+import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_RECENTS_ANIMATIONS;
+import static com.android.server.wm.WindowManagerService.H.NOTIFY_APP_TRANSITION_STARTING;
+import android.annotation.IntDef;
import android.app.ActivityManager.TaskSnapshot;
import android.app.WindowConfiguration;
import android.graphics.Point;
@@ -38,23 +38,22 @@ import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.ArraySet;
-import android.util.Log;
-import android.util.Slog;import android.util.proto.ProtoOutputStream;
+import android.util.Slog;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
import android.view.IRecentsAnimationController;
import android.view.IRecentsAnimationRunner;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
-
-import com.google.android.collect.Sets;
-
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
import com.android.server.wm.utils.InsetUtils;
-
+import com.google.android.collect.Sets;
import java.io.PrintWriter;
import java.util.ArrayList;
+
/**
* Controls a single instance of the remote driven recents animation. In particular, this allows
* the calling SystemUI to animate the visible task windows as a part of the transition. The remote
@@ -63,19 +62,31 @@ import java.util.ArrayList;
* app if it requires the animation to be canceled at any time (ie. due to timeout, etc.)
*/
public class RecentsAnimationController implements DeathRecipient {
- private static final String TAG = TAG_WITH_CLASS_NAME ? "RecentsAnimationController" : TAG_WM;
- private static final boolean DEBUG = false;
+ private static final String TAG = RecentsAnimationController.class.getSimpleName();
private static final long FAILSAFE_DELAY = 1000;
+ public static final int REORDER_KEEP_IN_PLACE = 0;
+ public static final int REORDER_MOVE_TO_TOP = 1;
+ public static final int REORDER_MOVE_TO_ORIGINAL_POSITION = 2;
+
+ @IntDef(prefix = { "REORDER_MODE_" }, value = {
+ REORDER_KEEP_IN_PLACE,
+ REORDER_MOVE_TO_TOP,
+ REORDER_MOVE_TO_ORIGINAL_POSITION
+ })
+ public @interface ReorderMode {}
+
private final WindowManagerService mService;
private final IRecentsAnimationRunner mRunner;
private final RecentsAnimationCallbacks mCallbacks;
private final ArrayList<TaskAnimationAdapter> mPendingAnimations = new ArrayList<>();
private final int mDisplayId;
- private final Runnable mFailsafeRunnable = this::cancelAnimation;
+ private final Runnable mFailsafeRunnable = () -> {
+ cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "failSafeRunnable");
+ };
// The recents component app token that is shown behind the visibile tasks
- private AppWindowToken mHomeAppToken;
+ private AppWindowToken mTargetAppToken;
private Rect mMinimizedHomeBounds = new Rect();
// We start the RecentsAnimationController in a pending-start state since we need to wait for
@@ -84,16 +95,22 @@ public class RecentsAnimationController implements DeathRecipient {
private boolean mPendingStart = true;
// Set when the animation has been canceled
- private boolean mCanceled = false;
+ private boolean mCanceled;
// Whether or not the input consumer is enabled. The input consumer must be both registered and
// enabled for it to start intercepting touch events.
private boolean mInputConsumerEnabled;
- private Rect mTmpRect = new Rect();
+ // Whether or not the recents animation should cause the primary split-screen stack to be
+ // minimized
+ private boolean mSplitScreenMinimized;
+
+ private final Rect mTmpRect = new Rect();
+
+ private boolean mLinkedToDeathOfRunner;
public interface RecentsAnimationCallbacks {
- void onAnimationFinished(boolean moveHomeToTop);
+ void onAnimationFinished(@ReorderMode int reorderMode);
}
private final IRecentsAnimationController mController =
@@ -101,8 +118,9 @@ public class RecentsAnimationController implements DeathRecipient {
@Override
public TaskSnapshot screenshotTask(int taskId) {
- if (DEBUG) Log.d(TAG, "screenshotTask(" + taskId + "): mCanceled=" + mCanceled);
- long token = Binder.clearCallingIdentity();
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "screenshotTask(" + taskId + "):"
+ + " mCanceled=" + mCanceled);
+ final long token = Binder.clearCallingIdentity();
try {
synchronized (mService.getWindowManagerLock()) {
if (mCanceled) {
@@ -130,8 +148,9 @@ public class RecentsAnimationController implements DeathRecipient {
@Override
public void finish(boolean moveHomeToTop) {
- if (DEBUG) Log.d(TAG, "finish(" + moveHomeToTop + "): mCanceled=" + mCanceled);
- long token = Binder.clearCallingIdentity();
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "finish(" + moveHomeToTop + "):"
+ + " mCanceled=" + mCanceled);
+ final long token = Binder.clearCallingIdentity();
try {
synchronized (mService.getWindowManagerLock()) {
if (mCanceled) {
@@ -141,7 +160,9 @@ public class RecentsAnimationController implements DeathRecipient {
// Note, the callback will handle its own synchronization, do not lock on WM lock
// prior to calling the callback
- mCallbacks.onAnimationFinished(moveHomeToTop);
+ mCallbacks.onAnimationFinished(moveHomeToTop
+ ? REORDER_MOVE_TO_TOP
+ : REORDER_MOVE_TO_ORIGINAL_POSITION);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -150,7 +171,7 @@ public class RecentsAnimationController implements DeathRecipient {
@Override
public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars)
throws RemoteException {
- long token = Binder.clearCallingIdentity();
+ final long token = Binder.clearCallingIdentity();
try {
synchronized (mService.getWindowManagerLock()) {
for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
@@ -165,9 +186,9 @@ public class RecentsAnimationController implements DeathRecipient {
@Override
public void setInputConsumerEnabled(boolean enabled) {
- if (DEBUG) Log.d(TAG, "setInputConsumerEnabled(" + enabled + "): mCanceled="
- + mCanceled);
- long token = Binder.clearCallingIdentity();
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "setInputConsumerEnabled(" + enabled + "):"
+ + " mCanceled=" + mCanceled);
+ final long token = Binder.clearCallingIdentity();
try {
synchronized (mService.getWindowManagerLock()) {
if (mCanceled) {
@@ -182,6 +203,23 @@ public class RecentsAnimationController implements DeathRecipient {
Binder.restoreCallingIdentity(token);
}
}
+
+ @Override
+ public void setSplitScreenMinimized(boolean minimized) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mService.getWindowManagerLock()) {
+ if (mCanceled) {
+ return;
+ }
+
+ mSplitScreenMinimized = minimized;
+ mService.checkSplitScreenMinimizedChanged(true /* animate */);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
};
/**
@@ -203,7 +241,7 @@ public class RecentsAnimationController implements DeathRecipient {
* because it may call cancelAnimation() which needs to properly clean up the controller
* in the window manager.
*/
- public void initialize(SparseBooleanArray recentTaskIds) {
+ public void initialize(int targetActivityType, SparseBooleanArray recentTaskIds) {
// Make leashes for each of the visible tasks and add it to the recents animation to be
// started
final DisplayContent dc = mService.mRoot.getDisplayContent(mDisplayId);
@@ -214,7 +252,7 @@ public class RecentsAnimationController implements DeathRecipient {
final WindowConfiguration config = task.getWindowConfiguration();
if (config.tasksAreFloating()
|| config.getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
- || config.getActivityType() == ACTIVITY_TYPE_HOME) {
+ || config.getActivityType() == targetActivityType) {
continue;
}
addAnimation(task, !recentTaskIds.get(task.mTaskId));
@@ -222,23 +260,24 @@ public class RecentsAnimationController implements DeathRecipient {
// Skip the animation if there is nothing to animate
if (mPendingAnimations.isEmpty()) {
- cancelAnimation();
+ cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-noVisibleTasks");
return;
}
try {
- mRunner.asBinder().linkToDeath(this, 0);
+ linkToDeathOfRunner();
} catch (RemoteException e) {
- cancelAnimation();
+ cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "initialize-failedToLinkToDeath");
return;
}
- // Adjust the wallpaper visibility for the showing home activity
- final AppWindowToken recentsComponentAppToken =
- dc.getHomeStack().getTopChild().getTopFullscreenAppToken();
+ // Adjust the wallpaper visibility for the showing target activity
+ final AppWindowToken recentsComponentAppToken = dc.getStack(WINDOWING_MODE_UNDEFINED,
+ targetActivityType).getTopChild().getTopFullscreenAppToken();
if (recentsComponentAppToken != null) {
- if (DEBUG) Log.d(TAG, "setHomeApp(" + recentsComponentAppToken.getName() + ")");
- mHomeAppToken = recentsComponentAppToken;
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "setHomeApp("
+ + recentsComponentAppToken.getName() + ")");
+ mTargetAppToken = recentsComponentAppToken;
if (recentsComponentAppToken.windowsCanBeWallpaperTarget()) {
dc.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
dc.setLayoutNeeded();
@@ -251,8 +290,10 @@ public class RecentsAnimationController implements DeathRecipient {
mService.mWindowPlacerLocked.performSurfacePlacement();
}
- private void addAnimation(Task task, boolean isRecentTaskInvisible) {
- if (DEBUG) Log.d(TAG, "addAnimation(" + task.getName() + ")");
+ @VisibleForTesting
+ AnimationAdapter addAnimation(Task task, boolean isRecentTaskInvisible) {
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "addAnimation(" + task.getName() + ")");
+ // TODO: Refactor this to use the task's animator
final SurfaceAnimator anim = new SurfaceAnimator(task, null /* animationFinishedCallback */,
mService);
final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task,
@@ -260,10 +301,20 @@ public class RecentsAnimationController implements DeathRecipient {
anim.startAnimation(task.getPendingTransaction(), taskAdapter, false /* hidden */);
task.commitPendingTransaction();
mPendingAnimations.add(taskAdapter);
+ return taskAdapter;
+ }
+
+ @VisibleForTesting
+ void removeAnimation(TaskAnimationAdapter taskAdapter) {
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "removeAnimation("
+ + taskAdapter.mTask.mTaskId + ")");
+ taskAdapter.mTask.setCanAffectSystemUiFlags(true);
+ taskAdapter.mCapturedFinishCallback.onAnimationFinished(taskAdapter);
+ mPendingAnimations.remove(taskAdapter);
}
void startAnimation() {
- if (DEBUG) Log.d(TAG, "startAnimation(): mPendingStart=" + mPendingStart
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "startAnimation(): mPendingStart=" + mPendingStart
+ " mCanceled=" + mCanceled);
if (!mPendingStart || mCanceled) {
// Skip starting if we've already started or canceled the animation
@@ -272,24 +323,42 @@ public class RecentsAnimationController implements DeathRecipient {
try {
final ArrayList<RemoteAnimationTarget> appAnimations = new ArrayList<>();
for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final RemoteAnimationTarget target =
- mPendingAnimations.get(i).createRemoteAnimationApp();
+ final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
+ final RemoteAnimationTarget target = taskAdapter.createRemoteAnimationApp();
if (target != null) {
appAnimations.add(target);
+ } else {
+ removeAnimation(taskAdapter);
}
}
+
+ // Skip the animation if there is nothing to animate
+ if (appAnimations.isEmpty()) {
+ cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "startAnimation-noAppWindows");
+ return;
+ }
+
final RemoteAnimationTarget[] appTargets = appAnimations.toArray(
new RemoteAnimationTarget[appAnimations.size()]);
mPendingStart = false;
- final Rect minimizedHomeBounds =
- mHomeAppToken != null && mHomeAppToken.inSplitScreenSecondaryWindowingMode()
- ? mMinimizedHomeBounds : null;
- final Rect contentInsets =
- mHomeAppToken != null && mHomeAppToken.findMainWindow() != null
- ? mHomeAppToken.findMainWindow().mContentInsets : null;
+ final Rect minimizedHomeBounds = mTargetAppToken != null
+ && mTargetAppToken.inSplitScreenSecondaryWindowingMode()
+ ? mMinimizedHomeBounds
+ : null;
+ final Rect contentInsets = mTargetAppToken != null
+ && mTargetAppToken.findMainWindow() != null
+ ? mTargetAppToken.findMainWindow().mContentInsets
+ : null;
mRunner.onAnimationStart_New(mController, appTargets, contentInsets,
minimizedHomeBounds);
+ if (DEBUG_RECENTS_ANIMATIONS) {
+ Slog.d(TAG, "startAnimation(): Notify animation start:");
+ for (int i = 0; i < mPendingAnimations.size(); i++) {
+ final Task task = mPendingAnimations.get(i).mTask;
+ Slog.d(TAG, "\t" + task.mTaskId);
+ }
+ }
} catch (RemoteException e) {
Slog.e(TAG, "Failed to start recents animation", e);
}
@@ -299,8 +368,8 @@ public class RecentsAnimationController implements DeathRecipient {
reasons).sendToTarget();
}
- void cancelAnimation() {
- if (DEBUG) Log.d(TAG, "cancelAnimation()");
+ void cancelAnimation(@ReorderMode int reorderMode, String reason) {
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG, "cancelAnimation()");
synchronized (mService.getWindowManagerLock()) {
if (mCanceled) {
// We've already canceled the animation
@@ -314,26 +383,26 @@ public class RecentsAnimationController implements DeathRecipient {
Slog.e(TAG, "Failed to cancel recents animation", e);
}
}
+
// Clean up and return to the previous app
// Don't hold the WM lock here as it calls back to AM/RecentsAnimation
- mCallbacks.onAnimationFinished(false /* moveHomeToTop */);
+ mCallbacks.onAnimationFinished(reorderMode);
}
- void cleanupAnimation(boolean moveHomeToTop) {
- if (DEBUG) Log.d(TAG, "cleanupAnimation(): mPendingAnimations="
- + mPendingAnimations.size());
+ void cleanupAnimation(@ReorderMode int reorderMode) {
+ if (DEBUG_RECENTS_ANIMATIONS) Slog.d(TAG,
+ "cleanupAnimation(): Notify animation finished mPendingAnimations="
+ + mPendingAnimations.size() + " reorderMode=" + reorderMode);
for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
- final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
- adapter.mTask.setCanAffectSystemUiFlags(true);
- if (moveHomeToTop) {
- adapter.mTask.dontAnimateDimExit();
+ final TaskAnimationAdapter taskAdapter = mPendingAnimations.get(i);
+ if (reorderMode == REORDER_MOVE_TO_TOP || reorderMode == REORDER_KEEP_IN_PLACE) {
+ taskAdapter.mTask.dontAnimateDimExit();
}
- adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+ removeAnimation(taskAdapter);
}
- mPendingAnimations.clear();
-
- mRunner.asBinder().unlinkToDeath(this, 0);
+ unlinkToDeathOfRunner();
+ // Clear associated input consumers
mService.mInputMonitor.updateInputWindowsLw(true /*force*/);
mService.destroyInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
}
@@ -342,14 +411,28 @@ public class RecentsAnimationController implements DeathRecipient {
mService.mH.postDelayed(mFailsafeRunnable, FAILSAFE_DELAY);
}
+ private void linkToDeathOfRunner() throws RemoteException {
+ if (!mLinkedToDeathOfRunner) {
+ mRunner.asBinder().linkToDeath(this, 0);
+ mLinkedToDeathOfRunner = true;
+ }
+ }
+
+ private void unlinkToDeathOfRunner() {
+ if (mLinkedToDeathOfRunner) {
+ mRunner.asBinder().unlinkToDeath(this, 0);
+ mLinkedToDeathOfRunner = false;
+ }
+ }
+
@Override
public void binderDied() {
- cancelAnimation();
+ cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "binderDied");
}
void checkAnimationReady(WallpaperController wallpaperController) {
if (mPendingStart) {
- final boolean wallpaperReady = !isHomeAppOverWallpaper()
+ final boolean wallpaperReady = !isTargetOverWallpaper()
|| (wallpaperController.getWallpaperTarget() != null
&& wallpaperController.wallpaperTransitionReady());
if (wallpaperReady) {
@@ -358,9 +441,13 @@ public class RecentsAnimationController implements DeathRecipient {
}
}
+ boolean isSplitScreenMinimized() {
+ return mSplitScreenMinimized;
+ }
+
boolean isWallpaperVisible(WindowState w) {
- return w != null && w.mAppToken != null && mHomeAppToken == w.mAppToken
- && isHomeAppOverWallpaper();
+ return w != null && w.mAppToken != null && mTargetAppToken == w.mAppToken
+ && isTargetOverWallpaper();
}
boolean hasInputConsumerForApp(AppWindowToken appToken) {
@@ -369,12 +456,12 @@ public class RecentsAnimationController implements DeathRecipient {
boolean updateInputConsumerForApp(InputConsumerImpl recentsAnimationInputConsumer,
boolean hasFocus) {
- // Update the input consumer touchable region to match the home app main window
- final WindowState homeAppMainWindow = mHomeAppToken != null
- ? mHomeAppToken.findMainWindow()
+ // Update the input consumer touchable region to match the target app main window
+ final WindowState targetAppMainWindow = mTargetAppToken != null
+ ? mTargetAppToken.findMainWindow()
: null;
- if (homeAppMainWindow != null) {
- homeAppMainWindow.getBounds(mTmpRect);
+ if (targetAppMainWindow != null) {
+ targetAppMainWindow.getBounds(mTmpRect);
recentsAnimationInputConsumer.mWindowHandle.hasFocus = hasFocus;
recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(mTmpRect);
return true;
@@ -382,11 +469,20 @@ public class RecentsAnimationController implements DeathRecipient {
return false;
}
- private boolean isHomeAppOverWallpaper() {
- if (mHomeAppToken == null) {
+ private boolean isTargetOverWallpaper() {
+ if (mTargetAppToken == null) {
return false;
}
- return mHomeAppToken.windowsCanBeWallpaperTarget();
+ return mTargetAppToken.windowsCanBeWallpaperTarget();
+ }
+
+ boolean isAnimatingTask(Task task) {
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ if (task == mPendingAnimations.get(i).mTask) {
+ return true;
+ }
+ }
+ return false;
}
private boolean isAnimatingApp(AppWindowToken appToken) {
@@ -402,25 +498,26 @@ public class RecentsAnimationController implements DeathRecipient {
return false;
}
- private class TaskAnimationAdapter implements AnimationAdapter {
+ @VisibleForTesting
+ class TaskAnimationAdapter implements AnimationAdapter {
private final Task mTask;
private SurfaceControl mCapturedLeash;
private OnAnimationFinishedCallback mCapturedFinishCallback;
private final boolean mIsRecentTaskInvisible;
private RemoteAnimationTarget mTarget;
+ private final Point mPosition = new Point();
+ private final Rect mBounds = new Rect();
TaskAnimationAdapter(Task task, boolean isRecentTaskInvisible) {
mTask = task;
mIsRecentTaskInvisible = isRecentTaskInvisible;
+ final WindowContainer container = mTask.getParent();
+ container.getRelativePosition(mPosition);
+ container.getBounds(mBounds);
}
RemoteAnimationTarget createRemoteAnimationApp() {
- final Point position = new Point();
- final Rect bounds = new Rect();
- final WindowContainer container = mTask.getParent();
- container.getRelativePosition(position);
- container.getBounds(bounds);
final WindowState mainWindow = mTask.getTopVisibleAppMainWindow();
if (mainWindow == null) {
return null;
@@ -429,7 +526,7 @@ public class RecentsAnimationController implements DeathRecipient {
InsetUtils.addInsets(insets, mainWindow.mAppToken.getLetterboxInsets());
mTarget = new RemoteAnimationTarget(mTask.mTaskId, MODE_CLOSING, mCapturedLeash,
!mTask.fillsParent(), mainWindow.mWinAnimator.mLastClipRect,
- insets, mTask.getPrefixOrderIndex(), position, bounds,
+ insets, mTask.getPrefixOrderIndex(), mPosition, mBounds,
mTask.getWindowConfiguration(), mIsRecentTaskInvisible);
return mTarget;
}
@@ -452,13 +549,14 @@ public class RecentsAnimationController implements DeathRecipient {
@Override
public void startAnimation(SurfaceControl animationLeash, Transaction t,
OnAnimationFinishedCallback finishCallback) {
+ t.setPosition(animationLeash, mPosition.x, mPosition.y);
mCapturedLeash = animationLeash;
mCapturedFinishCallback = finishCallback;
}
@Override
public void onAnimationCancelled(SurfaceControl animationLeash) {
- cancelAnimation();
+ cancelAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION, "taskAnimationAdapterCanceled");
}
@Override
@@ -480,6 +578,10 @@ public class RecentsAnimationController implements DeathRecipient {
} else {
pw.print(prefix); pw.println("Target: null");
}
+ pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible);
+ pw.println("mPosition=" + mPosition);
+ pw.println("mBounds=" + mBounds);
+ pw.println("mIsRecentTaskInvisible=" + mIsRecentTaskInvisible);
}
@Override
@@ -496,6 +598,10 @@ public class RecentsAnimationController implements DeathRecipient {
final String innerPrefix = prefix + " ";
pw.print(prefix); pw.println(RecentsAnimationController.class.getSimpleName() + ":");
pw.print(innerPrefix); pw.println("mPendingStart=" + mPendingStart);
- pw.print(innerPrefix); pw.println("mHomeAppToken=" + mHomeAppToken);
+ pw.print(innerPrefix); pw.println("mCanceled=" + mCanceled);
+ pw.print(innerPrefix); pw.println("mInputConsumerEnabled=" + mInputConsumerEnabled);
+ pw.print(innerPrefix); pw.println("mSplitScreenMinimized=" + mSplitScreenMinimized);
+ pw.print(innerPrefix); pw.println("mTargetAppToken=" + mTargetAppToken);
+ pw.print(innerPrefix); pw.println("isTargetOverWallpaper=" + isTargetOverWallpaper());
}
}
diff --git a/com/android/server/wm/RemoteAnimationController.java b/com/android/server/wm/RemoteAnimationController.java
index 3be7b235..1b06b2fe 100644
--- a/com/android/server/wm/RemoteAnimationController.java
+++ b/com/android/server/wm/RemoteAnimationController.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import static com.android.server.wm.AnimationAdapterProto.REMOTE;
import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_REMOTE_ANIMATIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -49,7 +50,9 @@ import java.util.ArrayList;
* Helper class to run app animations in a remote process.
*/
class RemoteAnimationController implements DeathRecipient {
- private static final String TAG = TAG_WITH_CLASS_NAME ? "RemoteAnimationController" : TAG_WM;
+ private static final String TAG = TAG_WITH_CLASS_NAME
+ || (DEBUG_REMOTE_ANIMATIONS && !DEBUG_APP_TRANSITIONS)
+ ? "RemoteAnimationController" : TAG_WM;
private static final long TIMEOUT_MS = 2000;
private final WindowManagerService mService;
@@ -57,10 +60,11 @@ class RemoteAnimationController implements DeathRecipient {
private final ArrayList<RemoteAnimationAdapterWrapper> mPendingAnimations = new ArrayList<>();
private final Rect mTmpRect = new Rect();
private final Handler mHandler;
- private final Runnable mTimeoutRunnable = this::cancelAnimation;
+ private final Runnable mTimeoutRunnable = () -> cancelAnimation("timeoutRunnable");
private FinishedCallback mFinishedCallback;
private boolean mCanceled;
+ private boolean mLinkedToDeathOfRunner;
RemoteAnimationController(WindowManagerService service,
RemoteAnimationAdapter remoteAnimationAdapter, Handler handler) {
@@ -79,6 +83,7 @@ class RemoteAnimationController implements DeathRecipient {
*/
AnimationAdapter createAnimationAdapter(AppWindowToken appWindowToken, Point position,
Rect stackBounds) {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "createAnimationAdapter(): token=" + appWindowToken);
final RemoteAnimationAdapterWrapper adapter = new RemoteAnimationAdapterWrapper(
appWindowToken, position, stackBounds);
mPendingAnimations.add(adapter);
@@ -89,7 +94,10 @@ class RemoteAnimationController implements DeathRecipient {
* Called when the transition is ready to be started, and all leashes have been set up.
*/
void goodToGo() {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo()");
if (mPendingAnimations.isEmpty() || mCanceled) {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo(): Animation finished before good to go, canceled="
+ + mCanceled + " mPendingAnimations=" + mPendingAnimations.size());
onAnimationFinished();
return;
}
@@ -101,25 +109,32 @@ class RemoteAnimationController implements DeathRecipient {
final RemoteAnimationTarget[] animations = createAnimations();
if (animations.length == 0) {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "goodToGo(): No apps to animate");
onAnimationFinished();
return;
}
mService.mAnimator.addAfterPrepareSurfacesRunnable(() -> {
try {
- mRemoteAnimationAdapter.getRunner().asBinder().linkToDeath(this, 0);
+ linkToDeathOfRunner();
mRemoteAnimationAdapter.getRunner().onAnimationStart(animations, mFinishedCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to start remote animation", e);
onAnimationFinished();
}
+ if (DEBUG_REMOTE_ANIMATIONS) {
+ Slog.d(TAG, "startAnimation(): Notify animation start:");
+ for (int i = 0; i < mPendingAnimations.size(); i++) {
+ Slog.d(TAG, "\t" + mPendingAnimations.get(i).mAppWindowToken);
+ }
+ } else if (DEBUG_APP_TRANSITIONS) {
+ writeStartDebugStatement();
+ }
});
sendRunningRemoteAnimation(true);
- if (DEBUG_APP_TRANSITIONS) {
- writeStartDebugStatement();
- }
}
- private void cancelAnimation() {
+ private void cancelAnimation(String reason) {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "cancelAnimation(): reason=" + reason);
synchronized (mService.getWindowManagerLock()) {
if (mCanceled) {
return;
@@ -142,14 +157,16 @@ class RemoteAnimationController implements DeathRecipient {
}
private RemoteAnimationTarget[] createAnimations() {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "createAnimations()");
final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
final RemoteAnimationAdapterWrapper wrapper = mPendingAnimations.get(i);
- final RemoteAnimationTarget target =
- mPendingAnimations.get(i).createRemoteAppAnimation();
+ final RemoteAnimationTarget target = wrapper.createRemoteAppAnimation();
if (target != null) {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\tAdd token=" + wrapper.mAppWindowToken);
targets.add(target);
} else {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\tRemove token=" + wrapper.mAppWindowToken);
// We can't really start an animation but we still need to make sure to finish the
// pending animation that was started by SurfaceAnimator
@@ -163,22 +180,29 @@ class RemoteAnimationController implements DeathRecipient {
}
private void onAnimationFinished() {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "onAnimationFinished(): mPendingAnimations="
+ + mPendingAnimations.size());
mHandler.removeCallbacks(mTimeoutRunnable);
- mRemoteAnimationAdapter.getRunner().asBinder().unlinkToDeath(this, 0);
synchronized (mService.mWindowMap) {
+ unlinkToDeathOfRunner();
releaseFinishedCallback();
mService.openSurfaceTransaction();
try {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "onAnimationFinished(): Notify animation finished:");
for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
final RemoteAnimationAdapterWrapper adapter = mPendingAnimations.get(i);
adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "\t" + adapter.mAppWindowToken);
}
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to finish remote animation", e);
+ throw e;
} finally {
mService.closeSurfaceTransaction("RemoteAnimationController#finished");
}
}
sendRunningRemoteAnimation(false);
- if (DEBUG_APP_TRANSITIONS) Slog.i(TAG, "Finishing remote animation");
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.i(TAG, "Finishing remote animation");
}
private void invokeAnimationCancelled() {
@@ -204,9 +228,23 @@ class RemoteAnimationController implements DeathRecipient {
mService.sendSetRunningRemoteAnimation(pid, running);
}
+ private void linkToDeathOfRunner() throws RemoteException {
+ if (!mLinkedToDeathOfRunner) {
+ mRemoteAnimationAdapter.getRunner().asBinder().linkToDeath(this, 0);
+ mLinkedToDeathOfRunner = true;
+ }
+ }
+
+ private void unlinkToDeathOfRunner() {
+ if (mLinkedToDeathOfRunner) {
+ mRemoteAnimationAdapter.getRunner().asBinder().unlinkToDeath(this, 0);
+ mLinkedToDeathOfRunner = false;
+ }
+ }
+
@Override
public void binderDied() {
- cancelAnimation();
+ cancelAnimation("binderDied");
}
private static final class FinishedCallback extends IRemoteAnimationFinishedCallback.Stub {
@@ -219,6 +257,7 @@ class RemoteAnimationController implements DeathRecipient {
@Override
public void onAnimationFinished() throws RemoteException {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "app-onAnimationFinished(): mOuter=" + mOuter);
final long token = Binder.clearCallingIdentity();
try {
if (mOuter != null) {
@@ -238,6 +277,7 @@ class RemoteAnimationController implements DeathRecipient {
* to prevent memory leak.
*/
void release() {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "app-release(): mOuter=" + mOuter);
mOuter = null;
}
};
@@ -301,6 +341,7 @@ class RemoteAnimationController implements DeathRecipient {
@Override
public void startAnimation(SurfaceControl animationLeash, Transaction t,
OnAnimationFinishedCallback finishCallback) {
+ if (DEBUG_REMOTE_ANIMATIONS) Slog.d(TAG, "startAnimation");
// Restore z-layering, position and stack crop until client has a chance to modify it.
t.setLayer(animationLeash, mAppWindowToken.getPrefixOrderIndex());
diff --git a/com/android/server/wm/RootWindowContainer.java b/com/android/server/wm/RootWindowContainer.java
index 52d81777..fd965fbc 100644
--- a/com/android/server/wm/RootWindowContainer.java
+++ b/com/android/server/wm/RootWindowContainer.java
@@ -417,6 +417,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
}, true /* traverseTopToBottom */);
}
+ void updateHiddenWhileSuspendedState(final ArraySet<String> packages, final boolean suspended) {
+ forAllWindows((w) -> {
+ if (packages.contains(w.getOwningPackage())) {
+ w.setHiddenWhileSuspended(suspended);
+ }
+ }, false);
+ }
+
void updateAppOpsState() {
forAllWindows((w) -> {
w.updateAppOpsState();
diff --git a/com/android/server/wm/Session.java b/com/android/server/wm/Session.java
index 662d51d6..4003d5a1 100644
--- a/com/android/server/wm/Session.java
+++ b/com/android/server/wm/Session.java
@@ -194,7 +194,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
InputChannel outInputChannel) {
return addToDisplay(window, seq, attrs, viewVisibility, Display.DEFAULT_DISPLAY,
new Rect() /* outFrame */, outContentInsets, outStableInsets, null /* outOutsets */,
- null /* cutout */, outInputChannel);
+ new DisplayCutout.ParcelableWrapper() /* cutout */, outInputChannel);
}
@Override
@@ -218,7 +218,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
new Rect() /* outFrame */, outContentInsets, outStableInsets, null /* outOutsets */,
- null /* cutout */, null /* outInputChannel */);
+ new DisplayCutout.ParcelableWrapper() /* cutout */, null /* outInputChannel */);
}
@Override
@@ -233,16 +233,16 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
@Override
public int relayout(IWindow window, int seq, WindowManager.LayoutParams attrs,
- int requestedWidth, int requestedHeight, int viewFlags,
- int flags, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
- Rect outVisibleInsets, Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
- DisplayCutout.ParcelableWrapper cutout,
- MergedConfiguration mergedConfiguration, Surface outSurface) {
+ int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
+ Rect outFrame, Rect outOverscanInsets, Rect outContentInsets, Rect outVisibleInsets,
+ Rect outStableInsets, Rect outsets, Rect outBackdropFrame,
+ DisplayCutout.ParcelableWrapper cutout, MergedConfiguration mergedConfiguration,
+ Surface outSurface) {
if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from "
+ Binder.getCallingPid());
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag);
int res = mService.relayoutWindow(this, window, seq, attrs,
- requestedWidth, requestedHeight, viewFlags, flags,
+ requestedWidth, requestedHeight, viewFlags, flags, frameNumber,
outFrame, outOverscanInsets, outContentInsets, outVisibleInsets,
outStableInsets, outsets, outBackdropFrame, cutout,
mergedConfiguration, outSurface);
diff --git a/com/android/server/wm/SurfaceAnimationRunner.java b/com/android/server/wm/SurfaceAnimationRunner.java
index 98fcb0be..7211533c 100644
--- a/com/android/server/wm/SurfaceAnimationRunner.java
+++ b/com/android/server/wm/SurfaceAnimationRunner.java
@@ -220,6 +220,9 @@ class SurfaceAnimationRunner {
}
private void applyTransformation(RunningAnimation a, Transaction t, long currentPlayTime) {
+ if (a.mAnimSpec.needsEarlyWakeup()) {
+ t.setEarlyWakeup();
+ }
a.mAnimSpec.apply(t, a.mLeash, currentPlayTime);
}
diff --git a/com/android/server/wm/Task.java b/com/android/server/wm/Task.java
index e8d32109..95223d84 100644
--- a/com/android/server/wm/Task.java
+++ b/com/android/server/wm/Task.java
@@ -44,6 +44,7 @@ import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.Surface;
+import android.view.SurfaceControl;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
@@ -559,6 +560,23 @@ class Task extends WindowContainer<AppWindowToken> {
&& !mStack.isAnimatingBoundsToFullscreen() && !mPreserveNonFloatingState;
}
+ @Override
+ public SurfaceControl getAnimationLeashParent() {
+ // Reparent to the animation layer so that we aren't clipped by the non-minimized
+ // stack bounds, currently we only animate the task for the recents animation
+ return getAppAnimationLayer(false /* boosted */);
+ }
+
+ boolean isTaskAnimating() {
+ final RecentsAnimationController recentsAnim = mService.getRecentsAnimationController();
+ if (recentsAnim != null) {
+ if (recentsAnim.isAnimatingTask(this)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
WindowState getTopVisibleAppMainWindow() {
final AppWindowToken token = getTopVisibleAppToken();
return token != null ? token.findMainWindow() : null;
diff --git a/com/android/server/wm/TaskSnapshotSurface.java b/com/android/server/wm/TaskSnapshotSurface.java
index a9e53a14..5721bd89 100644
--- a/com/android/server/wm/TaskSnapshotSurface.java
+++ b/com/android/server/wm/TaskSnapshotSurface.java
@@ -173,6 +173,8 @@ class TaskSnapshotSurface implements StartingSurface {
windowFlags = topFullscreenWindow.getAttrs().flags;
windowPrivateFlags = topFullscreenWindow.getAttrs().privateFlags;
+ layoutParams.packageName = mainWindow.getAttrs().packageName;
+ layoutParams.windowAnimations = mainWindow.getAttrs().windowAnimations;
layoutParams.dimAmount = mainWindow.getAttrs().dimAmount;
layoutParams.type = TYPE_APPLICATION_STARTING;
layoutParams.format = snapshot.getSnapshot().getFormat();
@@ -213,8 +215,8 @@ class TaskSnapshotSurface implements StartingSurface {
currentOrientation);
window.setOuter(snapshotSurface);
try {
- session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, tmpFrame,
- tmpRect, tmpContentInsets, tmpRect, tmpStableInsets, tmpRect, tmpRect,
+ session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, -1,
+ tmpFrame, tmpRect, tmpContentInsets, tmpRect, tmpStableInsets, tmpRect, tmpRect,
tmpCutout, tmpMergedConfiguration, surface);
} catch (RemoteException e) {
// Local call.
diff --git a/com/android/server/wm/TaskStack.java b/com/android/server/wm/TaskStack.java
index 62754ada..ae9e8026 100644
--- a/com/android/server/wm/TaskStack.java
+++ b/com/android/server/wm/TaskStack.java
@@ -33,12 +33,11 @@ import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;
import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
-import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.StackProto.ADJUSTED_BOUNDS;
import static com.android.server.wm.StackProto.ADJUSTED_FOR_IME;
import static com.android.server.wm.StackProto.ADJUST_DIVIDER_AMOUNT;
import static com.android.server.wm.StackProto.ADJUST_IME_AMOUNT;
+import static com.android.server.wm.StackProto.ANIMATING_BOUNDS;
import static com.android.server.wm.StackProto.ANIMATION_BACKGROUND_SURFACE_IS_DIMMING;
import static com.android.server.wm.StackProto.BOUNDS;
import static com.android.server.wm.StackProto.DEFER_REMOVAL;
@@ -47,6 +46,8 @@ import static com.android.server.wm.StackProto.ID;
import static com.android.server.wm.StackProto.MINIMIZE_AMOUNT;
import static com.android.server.wm.StackProto.TASKS;
import static com.android.server.wm.StackProto.WINDOW_CONTAINER;
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_MOVEMENT;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.CallSuper;
import android.content.res.Configuration;
@@ -62,12 +63,10 @@ import android.util.proto.ProtoOutputStream;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.SurfaceControl;
-
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
import com.android.internal.policy.DockedDividerUtils;
import com.android.server.EventLogTags;
-
import java.io.PrintWriter;
public class TaskStack extends WindowContainer<Task> implements
@@ -829,6 +828,14 @@ public class TaskStack extends WindowContainer<Task> implements
}
}
+ if (inSplitScreenSecondaryWindowingMode()) {
+ // When the stack is resized due to entering split screen secondary, offset the
+ // windows to compensate for the new stack position.
+ forAllWindows(w -> {
+ w.mWinAnimator.setOffsetPositionForStackResize(true);
+ }, true);
+ }
+
updateDisplayInfo(bounds);
updateSurfaceBounds();
}
@@ -1333,6 +1340,20 @@ public class TaskStack extends WindowContainer<Task> implements
return mMinimizeAmount != 0f;
}
+ /**
+ * @return {@code true} if we have a {@link Task} that is animating (currently only used for the
+ * recents animation); {@code false} otherwise.
+ */
+ boolean isTaskAnimating() {
+ for (int j = mChildren.size() - 1; j >= 0; j--) {
+ final Task task = mChildren.get(j);
+ if (task.isTaskAnimating()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@CallSuper
@Override
public void writeToProto(ProtoOutputStream proto, long fieldId, boolean trim) {
@@ -1351,6 +1372,7 @@ public class TaskStack extends WindowContainer<Task> implements
proto.write(ADJUST_IME_AMOUNT, mAdjustImeAmount);
proto.write(ADJUST_DIVIDER_AMOUNT, mAdjustDividerAmount);
mAdjustedBounds.writeToProto(proto, ADJUSTED_BOUNDS);
+ proto.write(ANIMATING_BOUNDS, mBoundsAnimating);
proto.end(token);
}
@@ -1687,6 +1709,25 @@ public class TaskStack extends WindowContainer<Task> implements
}
}
+ @Override
+ public boolean shouldDeferStartOnMoveToFullscreen() {
+ // Workaround for the recents animation -- normally we need to wait for the new activity to
+ // show before starting the PiP animation, but because we start and show the home activity
+ // early for the recents animation prior to the PiP animation starting, there is no
+ // subsequent all-drawn signal. In this case, we can skip the pause when the home stack is
+ // already visible and drawn.
+ final TaskStack homeStack = mDisplayContent.getHomeStack();
+ if (homeStack == null) {
+ return true;
+ }
+ final Task homeTask = homeStack.getTopChild();
+ final AppWindowToken homeApp = homeTask.getTopVisibleAppToken();
+ if (!homeTask.isVisible() || homeApp == null) {
+ return true;
+ }
+ return !homeApp.allDrawn;
+ }
+
/**
* @return True if we are currently animating the pinned stack from fullscreen to non-fullscreen
* bounds and we have a deferred PiP mode changed callback set with the animation.
diff --git a/com/android/server/wm/WallpaperController.java b/com/android/server/wm/WallpaperController.java
index c509980e..c63da77d 100644
--- a/com/android/server/wm/WallpaperController.java
+++ b/com/android/server/wm/WallpaperController.java
@@ -26,6 +26,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
+import static com.android.server.wm.RecentsAnimationController.REORDER_MOVE_TO_ORIGINAL_POSITION;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
@@ -618,7 +619,8 @@ class WallpaperController {
// If there was a recents animation in progress, cancel that animation
if (mService.getRecentsAnimationController() != null) {
- mService.getRecentsAnimationController().cancelAnimation();
+ mService.getRecentsAnimationController().cancelAnimation(
+ REORDER_MOVE_TO_ORIGINAL_POSITION, "wallpaperDrawPendingTimeout");
}
return true;
}
diff --git a/com/android/server/wm/WindowAnimationSpec.java b/com/android/server/wm/WindowAnimationSpec.java
index 7b7cb30a..548e23a4 100644
--- a/com/android/server/wm/WindowAnimationSpec.java
+++ b/com/android/server/wm/WindowAnimationSpec.java
@@ -47,21 +47,24 @@ public class WindowAnimationSpec implements AnimationSpec {
private final Point mPosition = new Point();
private final ThreadLocal<TmpValues> mThreadLocalTmps = ThreadLocal.withInitial(TmpValues::new);
private final boolean mCanSkipFirstFrame;
+ private final boolean mIsAppAnimation;
private final Rect mStackBounds = new Rect();
private int mStackClipMode;
private final Rect mTmpRect = new Rect();
public WindowAnimationSpec(Animation animation, Point position, boolean canSkipFirstFrame) {
- this(animation, position, null /* stackBounds */, canSkipFirstFrame, STACK_CLIP_NONE);
+ this(animation, position, null /* stackBounds */, canSkipFirstFrame, STACK_CLIP_NONE,
+ false /* isAppAnimation */);
}
public WindowAnimationSpec(Animation animation, Point position, Rect stackBounds,
- boolean canSkipFirstFrame, int stackClipMode) {
+ boolean canSkipFirstFrame, int stackClipMode, boolean isAppAnimation) {
mAnimation = animation;
if (position != null) {
mPosition.set(position.x, position.y);
}
mCanSkipFirstFrame = canSkipFirstFrame;
+ mIsAppAnimation = isAppAnimation;
mStackClipMode = stackClipMode;
if (stackBounds != null) {
mStackBounds.set(stackBounds);
@@ -135,6 +138,11 @@ public class WindowAnimationSpec implements AnimationSpec {
}
@Override
+ public boolean needsEarlyWakeup() {
+ return mIsAppAnimation;
+ }
+
+ @Override
public void dump(PrintWriter pw, String prefix) {
pw.print(prefix); pw.println(mAnimation);
}
diff --git a/com/android/server/wm/WindowManagerDebugConfig.java b/com/android/server/wm/WindowManagerDebugConfig.java
index 9d9805ab..990eb97e 100644
--- a/com/android/server/wm/WindowManagerDebugConfig.java
+++ b/com/android/server/wm/WindowManagerDebugConfig.java
@@ -74,6 +74,9 @@ public class WindowManagerDebugConfig {
static final boolean SHOW_STACK_CRAWLS = false;
static final boolean DEBUG_WINDOW_CROP = false;
static final boolean DEBUG_UNKNOWN_APP_VISIBILITY = false;
+ // TODO (b/73188263): Reset debugging flags
+ static final boolean DEBUG_RECENTS_ANIMATIONS = true;
+ static final boolean DEBUG_REMOTE_ANIMATIONS = DEBUG_APP_TRANSITIONS || true;
static final String TAG_KEEP_SCREEN_ON = "DebugKeepScreenOn";
static final boolean DEBUG_KEEP_SCREEN_ON = false;
diff --git a/com/android/server/wm/WindowManagerService.java b/com/android/server/wm/WindowManagerService.java
index f1cd46bc..407312a9 100644
--- a/com/android/server/wm/WindowManagerService.java
+++ b/com/android/server/wm/WindowManagerService.java
@@ -128,6 +128,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.Bitmap;
@@ -264,6 +265,7 @@ import java.lang.annotation.RetentionPolicy;
import java.net.Socket;
import java.text.DateFormat;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.List;
/** {@hide} */
@@ -426,6 +428,7 @@ public class WindowManagerService extends IWindowManager.Stub
final ActivityManagerInternal mAmInternal;
final AppOpsManager mAppOps;
+ final PackageManagerInternal mPmInternal;
final DisplaySettings mDisplaySettings;
@@ -1006,6 +1009,22 @@ public class WindowManagerService extends IWindowManager.Stub
mAppOps.startWatchingMode(OP_SYSTEM_ALERT_WINDOW, null, opListener);
mAppOps.startWatchingMode(AppOpsManager.OP_TOAST_WINDOW, null, opListener);
+ mPmInternal = LocalServices.getService(PackageManagerInternal.class);
+ final IntentFilter suspendPackagesFilter = new IntentFilter();
+ suspendPackagesFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+ suspendPackagesFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+ context.registerReceiverAsUser(new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String[] affectedPackages =
+ intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+ final boolean suspended =
+ Intent.ACTION_PACKAGES_SUSPENDED.equals(intent.getAction());
+ updateHiddenWhileSuspendedState(new ArraySet<>(Arrays.asList(affectedPackages)),
+ suspended);
+ }
+ }, UserHandle.ALL, suspendPackagesFilter, null, null);
+
// Get persisted window scale setting
mWindowAnimationScaleSetting = Settings.Global.getFloat(context.getContentResolver(),
Settings.Global.WINDOW_ANIMATION_SCALE, mWindowAnimationScaleSetting);
@@ -1224,6 +1243,10 @@ public class WindowManagerService extends IWindowManager.Stub
Slog.w(TAG_WM, "Attempted to add window with exiting application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
+ } else if (type == TYPE_APPLICATION_STARTING && atoken.startingWindow != null) {
+ Slog.w(TAG_WM, "Attempted to add starting window to token with already existing"
+ + " starting window");
+ return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
} else if (rootType == TYPE_INPUT_METHOD) {
if (token.windowType != TYPE_INPUT_METHOD) {
@@ -1360,6 +1383,10 @@ public class WindowManagerService extends IWindowManager.Stub
win.initAppOpsState();
+ final boolean suspended = mPmInternal.isPackageSuspended(win.getOwningPackage(),
+ UserHandle.getUserId(win.getOwningUid()));
+ win.setHiddenWhileSuspended(suspended);
+
final boolean hideSystemAlertWindows = !mHidingNonSystemOverlayWindows.isEmpty();
win.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows);
@@ -1619,6 +1646,8 @@ public class WindowManagerService extends IWindowManager.Stub
if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "postWindowRemoveCleanupLocked: " + win);
mWindowMap.remove(win.mClient.asBinder());
+ markForSeamlessRotation(win, false);
+
win.resetAppOpsState();
if (mCurrentFocus == null) {
@@ -1683,6 +1712,12 @@ public class WindowManagerService extends IWindowManager.Stub
dc.computeImeTarget(true /* updateImeTarget */);
}
+ private void updateHiddenWhileSuspendedState(ArraySet<String> packages, boolean suspended) {
+ synchronized (mWindowMap) {
+ mRoot.updateHiddenWhileSuspendedState(packages, suspended);
+ }
+ }
+
private void updateAppOpsState() {
synchronized(mWindowMap) {
mRoot.updateAppOpsState();
@@ -1800,10 +1835,9 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- public int relayoutWindow(Session session, IWindow client, int seq,
- LayoutParams attrs, int requestedWidth,
- int requestedHeight, int viewVisibility, int flags,
- Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
+ public int relayoutWindow(Session session, IWindow client, int seq, LayoutParams attrs,
+ int requestedWidth, int requestedHeight, int viewVisibility, int flags,
+ long frameNumber, Rect outFrame, Rect outOverscanInsets, Rect outContentInsets,
Rect outVisibleInsets, Rect outStableInsets, Rect outOutsets, Rect outBackdropFrame,
DisplayCutout.ParcelableWrapper outCutout, MergedConfiguration mergedConfiguration,
Surface outSurface) {
@@ -1830,6 +1864,7 @@ public class WindowManagerService extends IWindowManager.Stub
win.setRequestedSize(requestedWidth, requestedHeight);
}
+ win.setFrameNumber(frameNumber);
int attrChanges = 0;
int flagChanges = 0;
if (attrs != null) {
@@ -1899,7 +1934,15 @@ public class WindowManagerService extends IWindowManager.Stub
winAnimator.setOpaqueLocked(false);
}
- boolean imMayMove = (flagChanges & (FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE)) != 0;
+ final int oldVisibility = win.mViewVisibility;
+
+ // If the window is becoming visible, visibleOrAdding may change which may in turn
+ // change the IME target.
+ final boolean becameVisible =
+ (oldVisibility == View.INVISIBLE || oldVisibility == View.GONE)
+ && viewVisibility == View.VISIBLE;
+ boolean imMayMove = (flagChanges & (FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE)) != 0
+ || becameVisible;
final boolean isDefaultDisplay = win.isDefaultDisplay();
boolean focusMayChange = isDefaultDisplay && (win.mViewVisibility != viewVisibility
|| ((flagChanges & FLAG_NOT_FOCUSABLE) != 0)
@@ -1915,7 +1958,6 @@ public class WindowManagerService extends IWindowManager.Stub
win.mRelayoutCalled = true;
win.mInRelayout = true;
- final int oldVisibility = win.mViewVisibility;
win.mViewVisibility = viewVisibility;
if (DEBUG_SCREEN_ON) {
RuntimeException stack = new RuntimeException();
@@ -2658,7 +2700,7 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- public void initializeRecentsAnimation(
+ public void initializeRecentsAnimation(int targetActivityType,
IRecentsAnimationRunner recentsAnimationRunner,
RecentsAnimationController.RecentsAnimationCallbacks callbacks, int displayId,
SparseBooleanArray recentTaskIds) {
@@ -2666,7 +2708,7 @@ public class WindowManagerService extends IWindowManager.Stub
mRecentsAnimationController = new RecentsAnimationController(this,
recentsAnimationRunner, callbacks, displayId);
mAppTransition.updateBooster();
- mRecentsAnimationController.initialize(recentTaskIds);
+ mRecentsAnimationController.initialize(targetActivityType, recentTaskIds);
}
}
@@ -2687,20 +2729,26 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- public void cancelRecentsAnimation() {
+ /**
+ * Cancels any running recents animation. The caller should NOT hold the WM lock while calling
+ * this method, as it can call back into AM, and locking will be done in the animation
+ * controller itself.
+ */
+ public void cancelRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode,
+ String reason) {
// Note: Do not hold the WM lock, this will lock appropriately in the call which also
// calls through to AM/RecentsAnimation.onAnimationFinished()
if (mRecentsAnimationController != null) {
// This call will call through to cleanupAnimation() below after the animation is
// canceled
- mRecentsAnimationController.cancelAnimation();
+ mRecentsAnimationController.cancelAnimation(reorderMode, reason);
}
}
- public void cleanupRecentsAnimation(boolean moveHomeToTop) {
+ public void cleanupRecentsAnimation(@RecentsAnimationController.ReorderMode int reorderMode) {
synchronized (mWindowMap) {
if (mRecentsAnimationController != null) {
- mRecentsAnimationController.cleanupAnimation(moveHomeToTop);
+ mRecentsAnimationController.cleanupAnimation(reorderMode);
mRecentsAnimationController = null;
mAppTransition.updateBooster();
}
diff --git a/com/android/server/wm/WindowState.java b/com/android/server/wm/WindowState.java
index 54c2e9ba..da5bc73e 100644
--- a/com/android/server/wm/WindowState.java
+++ b/com/android/server/wm/WindowState.java
@@ -20,6 +20,8 @@ import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
import static android.app.AppOpsManager.OP_NONE;
+import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
+import static android.app.AppOpsManager.OP_TOAST_WINDOW;
import static android.os.PowerManager.DRAW_WAKE_LOCK;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -64,7 +66,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
-import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType;
@@ -178,6 +179,7 @@ import android.util.MergedConfiguration;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
+import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
@@ -266,6 +268,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
* animation is done.
*/
boolean mPolicyVisibilityAfterAnim = true;
+ // overlay window is hidden because the owning app is suspended
+ private boolean mHiddenWhileSuspended;
private boolean mAppOpVisibility = true;
boolean mPermanentlyHidden; // the window should never be shown again
// This is a non-system overlay window that is currently force hidden.
@@ -632,6 +636,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private PowerManagerWrapper mPowerManagerWrapper;
/**
+ * A frame number in which changes requested in this layout will be rendered.
+ */
+ private long mFrameNumber = -1;
+
+ /**
* Compares two window sub-layers and returns -1 if the first is lesser than the second in terms
* of z-order and 1 otherwise.
*/
@@ -1366,6 +1375,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// Window was not laid out for this display yet, so make sure mLayoutSeq does not match.
if (dc != null) {
mLayoutSeq = dc.mLayoutSeq - 1;
+ mInputWindowHandle.displayId = dc.getDisplayId();
}
}
@@ -1378,7 +1388,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
public int getDisplayId() {
final DisplayContent displayContent = getDisplayContent();
if (displayContent == null) {
- return -1;
+ return Display.INVALID_DISPLAY;
}
return displayContent.getDisplayId();
}
@@ -1996,6 +2006,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// Try starting an animation.
if (mWinAnimator.applyAnimationLocked(transit, false)) {
mAnimatingExit = true;
+
+ // mAnimatingExit affects canAffectSystemUiFlags(). Run layout such that
+ // any change from that is performed immediately.
+ setDisplayLayoutNeeded();
+ mService.requestTraversal();
}
//TODO (multidisplay): Magnification is supported only for the default display.
if (mService.mAccessibilityController != null && displayId == DEFAULT_DISPLAY) {
@@ -2482,6 +2497,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// to handle their windows being removed from under them.
return false;
}
+ if (mHiddenWhileSuspended) {
+ // Being hidden due to owner package being suspended.
+ return false;
+ }
if (mForceHideNonSystemOverlayWindow) {
// This is an alert window that is currently force hidden.
return false;
@@ -2578,6 +2597,22 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
}
+ void setHiddenWhileSuspended(boolean hide) {
+ if (mOwnerCanAddInternalSystemWindow
+ || (!isSystemAlertWindowType(mAttrs.type) && mAttrs.type != TYPE_TOAST)) {
+ return;
+ }
+ if (mHiddenWhileSuspended == hide) {
+ return;
+ }
+ mHiddenWhileSuspended = hide;
+ if (hide) {
+ hideLw(true, true);
+ } else {
+ showLw(true, true);
+ }
+ }
+
private void setAppOpVisibilityLw(boolean state) {
if (mAppOpVisibility != state) {
mAppOpVisibility = state;
@@ -3298,7 +3333,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
pw.println(Integer.toHexString(mSystemUiVisibility));
}
if (!mPolicyVisibility || !mPolicyVisibilityAfterAnim || !mAppOpVisibility
- || isParentWindowHidden()|| mPermanentlyHidden || mForceHideNonSystemOverlayWindow) {
+ || isParentWindowHidden()|| mPermanentlyHidden || mForceHideNonSystemOverlayWindow
+ || mHiddenWhileSuspended) {
pw.print(prefix); pw.print("mPolicyVisibility=");
pw.print(mPolicyVisibility);
pw.print(" mPolicyVisibilityAfterAnim=");
@@ -3307,6 +3343,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
pw.print(mAppOpVisibility);
pw.print(" parentHidden="); pw.print(isParentWindowHidden());
pw.print(" mPermanentlyHidden="); pw.print(mPermanentlyHidden);
+ pw.print(" mHiddenWhileSuspended="); pw.print(mHiddenWhileSuspended);
pw.print(" mForceHideNonSystemOverlayWindow="); pw.println(
mForceHideNonSystemOverlayWindow);
}
@@ -4623,7 +4660,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mLastSurfaceInsets.set(mAttrs.surfaceInsets);
t.deferTransactionUntil(mSurfaceControl,
mWinAnimator.mSurfaceController.mSurfaceControl.getHandle(),
- mAttrs.frameNumber);
+ getFrameNumber());
}
}
}
@@ -4739,6 +4776,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
return mService.mInputMethodTarget == this;
}
+ long getFrameNumber() {
+ return mFrameNumber;
+ }
+
+ void setFrameNumber(long frameNumber) {
+ mFrameNumber = frameNumber;
+ }
+
private final class MoveAnimationSpec implements AnimationSpec {
private final long mDuration;
diff --git a/com/android/server/wm/WindowStateAnimator.java b/com/android/server/wm/WindowStateAnimator.java
index e92d460f..ab5e24ac 100644
--- a/com/android/server/wm/WindowStateAnimator.java
+++ b/com/android/server/wm/WindowStateAnimator.java
@@ -67,7 +67,6 @@ import android.view.animation.AnimationUtils;
import com.android.server.policy.WindowManagerPolicy;
-import java.io.FileDescriptor;
import java.io.PrintWriter;
/**
@@ -217,6 +216,12 @@ class WindowStateAnimator {
int mXOffset = 0;
int mYOffset = 0;
+ /**
+ * A flag to determine if the WSA needs to offset its position to compensate for the stack's
+ * position update before the WSA surface has resized.
+ */
+ private boolean mOffsetPositionForStackResize;
+
private final Rect mTmpSize = new Rect();
private final SurfaceControl.Transaction mReparentTransaction = new SurfaceControl.Transaction();
@@ -230,6 +235,8 @@ class WindowStateAnimator {
// once per animation.
boolean mPipAnimationStarted = false;
+ private final Point mTmpPos = new Point();
+
WindowStateAnimator(final WindowState win) {
final WindowManagerService service = win.mService;
@@ -498,6 +505,8 @@ class WindowStateAnimator {
mSurfaceController = new WindowSurfaceController(mSession.mSurfaceSession,
attrs.getTitle().toString(), width, height, format, flags, this,
windowType, ownerUid);
+
+ setOffsetPositionForStackResize(false);
mSurfaceFormat = format;
w.setHasSurface(true);
@@ -859,7 +868,8 @@ class WindowStateAnimator {
// However, this would be unsafe, as the client may be in the middle
// of producing a frame at the old size, having just completed layout
// to find the surface size changed underneath it.
- if (!w.mRelayoutCalled || w.mInRelayout) {
+ final boolean relayout = !w.mRelayoutCalled || w.mInRelayout;
+ if (relayout) {
mSurfaceResized = mSurfaceController.setSizeInTransaction(
mTmpSize.width(), mTmpSize.height(), recoveringMemory);
} else {
@@ -996,7 +1006,38 @@ class WindowStateAnimator {
mPipAnimationStarted = false;
if (!w.mSeamlesslyRotated) {
- mSurfaceController.setPositionInTransaction(mXOffset, mYOffset, recoveringMemory);
+ // Used to offset the WSA when stack position changes before a resize.
+ int xOffset = mXOffset;
+ int yOffset = mYOffset;
+ if (mOffsetPositionForStackResize) {
+ if (relayout) {
+ // Once a relayout is called, reset the offset back to 0 and defer
+ // setting it until a new frame with the updated size. This ensures that
+ // the WS position is reset (so the stack position is shown) at the same
+ // time that the buffer size changes.
+ setOffsetPositionForStackResize(false);
+ mSurfaceController.deferTransactionUntil(mSurfaceController.getHandle(),
+ mWin.getFrameNumber());
+ } else {
+ final TaskStack stack = mWin.getStack();
+ mTmpPos.x = 0;
+ mTmpPos.y = 0;
+ if (stack != null) {
+ stack.getRelativePosition(mTmpPos);
+ }
+
+ xOffset = -mTmpPos.x;
+ yOffset = -mTmpPos.y;
+
+ // Crop also needs to be extended so the bottom isn't cut off when the WSA
+ // position is moved.
+ if (clipRect != null) {
+ clipRect.right += mTmpPos.x;
+ clipRect.bottom += mTmpPos.y;
+ }
+ }
+ }
+ mSurfaceController.setPositionInTransaction(xOffset, yOffset, recoveringMemory);
}
}
@@ -1499,4 +1540,8 @@ class WindowStateAnimator {
int getLayer() {
return mLastLayer;
}
+
+ void setOffsetPositionForStackResize(boolean offsetPositionForStackResize) {
+ mOffsetPositionForStackResize = offsetPositionForStackResize;
+ }
}
diff --git a/com/android/settingslib/TwoTargetPreference.java b/com/android/settingslib/TwoTargetPreference.java
index 8b39f60a..02b68d8c 100644
--- a/com/android/settingslib/TwoTargetPreference.java
+++ b/com/android/settingslib/TwoTargetPreference.java
@@ -16,6 +16,7 @@
package com.android.settingslib;
+import android.annotation.IntDef;
import android.content.Context;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
@@ -24,10 +25,24 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
public class TwoTargetPreference extends Preference {
- private boolean mUseSmallIcon;
+ @IntDef({ICON_SIZE_DEFAULT, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IconSize {
+ }
+
+ public static final int ICON_SIZE_DEFAULT = 0;
+ public static final int ICON_SIZE_MEDIUM = 1;
+ public static final int ICON_SIZE_SMALL = 2;
+
+ @IconSize
+ private int mIconSize;
private int mSmallIconSize;
+ private int mMediumIconSize;
public TwoTargetPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
@@ -54,22 +69,30 @@ public class TwoTargetPreference extends Preference {
setLayoutResource(R.layout.preference_two_target);
mSmallIconSize = context.getResources().getDimensionPixelSize(
R.dimen.two_target_pref_small_icon_size);
+ mMediumIconSize = context.getResources().getDimensionPixelSize(
+ R.dimen.two_target_pref_medium_icon_size);
final int secondTargetResId = getSecondTargetResId();
if (secondTargetResId != 0) {
setWidgetLayoutResource(secondTargetResId);
}
}
- public void setUseSmallIcon(boolean useSmallIcon) {
- mUseSmallIcon = useSmallIcon;
+ public void setIconSize(@IconSize int iconSize) {
+ mIconSize = iconSize;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- if (mUseSmallIcon) {
- ImageView icon = holder.itemView.findViewById(android.R.id.icon);
- icon.setLayoutParams(new LinearLayout.LayoutParams(mSmallIconSize, mSmallIconSize));
+ final ImageView icon = holder.itemView.findViewById(android.R.id.icon);
+ switch (mIconSize) {
+ case ICON_SIZE_SMALL:
+ icon.setLayoutParams(new LinearLayout.LayoutParams(mSmallIconSize, mSmallIconSize));
+ break;
+ case ICON_SIZE_MEDIUM:
+ icon.setLayoutParams(
+ new LinearLayout.LayoutParams(mMediumIconSize, mMediumIconSize));
+ break;
}
final View divider = holder.findViewById(R.id.two_target_divider);
final View widgetFrame = holder.findViewById(android.R.id.widget_frame);
diff --git a/com/android/settingslib/bluetooth/BluetoothCallback.java b/com/android/settingslib/bluetooth/BluetoothCallback.java
index 4a6df505..bab59f1f 100644
--- a/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -29,5 +29,5 @@ public interface BluetoothCallback {
void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState);
void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state);
void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile);
- void onProfileAudioStateChanged(int bluetoothProfile, int state);
+ void onAudioModeChanged();
}
diff --git a/com/android/settingslib/bluetooth/BluetoothEventManager.java b/com/android/settingslib/bluetooth/BluetoothEventManager.java
index b74b2cd0..06fe4de4 100644
--- a/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -27,6 +27,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.telephony.TelephonyManager;
import android.util.Log;
import com.android.settingslib.R;
@@ -119,6 +120,12 @@ public class BluetoothEventManager {
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
+ // Headset state changed broadcasts
+ addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
+ new AudioModeChangedHandler());
+ addHandler(TelephonyManager.ACTION_PHONE_STATE_CHANGED,
+ new AudioModeChangedHandler());
+
mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter, null, mReceiverHandler);
mContext.registerReceiver(mProfileBroadcastReceiver, mProfileIntentFilter, null, mReceiverHandler);
}
@@ -456,4 +463,25 @@ public class BluetoothEventManager {
}
}
}
+
+ private class AudioModeChangedHandler implements Handler {
+
+ @Override
+ public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+ final String action = intent.getAction();
+ if (action == null) {
+ Log.w(TAG, "AudioModeChangedHandler() action is null");
+ return;
+ }
+ dispatchAudioModeChanged();
+ }
+ }
+
+ private void dispatchAudioModeChanged() {
+ synchronized (mCallbacks) {
+ for (BluetoothCallback callback : mCallbacks) {
+ callback.onAudioModeChanged();
+ }
+ }
+ }
}
diff --git a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 6c068ff1..dc2eceac 100644
--- a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -631,7 +631,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
}
HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
if (hearingAidProfile != null) {
- mIsActiveDeviceHearingAid = hearingAidProfile.isActiveDevice(mDevice);
+ mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
}
}
diff --git a/com/android/settingslib/bluetooth/HearingAidProfile.java b/com/android/settingslib/bluetooth/HearingAidProfile.java
index 920500f9..6c5ecbf2 100644
--- a/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -136,13 +136,12 @@ public class HearingAidProfile implements LocalBluetoothProfile {
public boolean setActiveDevice(BluetoothDevice device) {
if (mService == null) return false;
- mService.setActiveDevice(device);
- return true;
+ return mService.setActiveDevice(device);
}
- public boolean isActiveDevice(BluetoothDevice device) {
- if (mService == null) return false;
- return mService.isActiveDevice(device);
+ public List<BluetoothDevice> getActiveDevices() {
+ if (mService == null) return new ArrayList<>();
+ return mService.getActiveDevices();
}
public boolean isPreferred(BluetoothDevice device) {
diff --git a/com/android/settingslib/bluetooth/HidDeviceProfile.java b/com/android/settingslib/bluetooth/HidDeviceProfile.java
new file mode 100644
index 00000000..941964a5
--- /dev/null
+++ b/com/android/settingslib/bluetooth/HidDeviceProfile.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHidDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * HidProfile handles Bluetooth HID profile.
+ */
+public class HidDeviceProfile implements LocalBluetoothProfile {
+ private static final String TAG = "HidDeviceProfile";
+ // Order of this profile in device profiles list
+ private static final int ORDINAL = 18;
+ // HID Device Profile is always preferred.
+ private static final int PREFERRED_VALUE = -1;
+ private static final boolean DEBUG = true;
+
+ private final LocalBluetoothAdapter mLocalAdapter;
+ private final CachedBluetoothDeviceManager mDeviceManager;
+ private final LocalBluetoothProfileManager mProfileManager;
+ static final String NAME = "HID DEVICE";
+
+ private BluetoothHidDevice mService;
+ private boolean mIsProfileReady;
+
+ HidDeviceProfile(Context context, LocalBluetoothAdapter adapter,
+ CachedBluetoothDeviceManager deviceManager,
+ LocalBluetoothProfileManager profileManager) {
+ mLocalAdapter = adapter;
+ mDeviceManager = deviceManager;
+ mProfileManager = profileManager;
+ adapter.getProfileProxy(context, new HidDeviceServiceListener(),
+ BluetoothProfile.HID_DEVICE);
+ }
+
+ // These callbacks run on the main thread.
+ private final class HidDeviceServiceListener
+ implements BluetoothProfile.ServiceListener {
+
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (DEBUG) {
+ Log.d(TAG,"Bluetooth service connected :-)");
+ }
+ mService = (BluetoothHidDevice) proxy;
+ // We just bound to the service, so refresh the UI for any connected HID devices.
+ List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+ for (BluetoothDevice nextDevice : deviceList) {
+ CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+ // we may add a new device here, but generally this should not happen
+ if (device == null) {
+ Log.w(TAG, "HidProfile found new device: " + nextDevice);
+ device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice);
+ }
+ Log.d(TAG, "Connection status changed: " + device);
+ device.onProfileStateChanged(HidDeviceProfile.this,
+ BluetoothProfile.STATE_CONNECTED);
+ device.refresh();
+ }
+ mIsProfileReady = true;
+ }
+
+ public void onServiceDisconnected(int profile) {
+ if (DEBUG) {
+ Log.d(TAG, "Bluetooth service disconnected");
+ }
+ mIsProfileReady = false;
+ }
+ }
+
+ @Override
+ public boolean isProfileReady() {
+ return mIsProfileReady;
+ }
+
+ @Override
+ public boolean isConnectable() {
+ return true;
+ }
+
+ @Override
+ public boolean isAutoConnectable() {
+ return false;
+ }
+
+ @Override
+ public boolean connect(BluetoothDevice device) {
+ return false;
+ }
+
+ @Override
+ public boolean disconnect(BluetoothDevice device) {
+ if (mService == null) {
+ return false;
+ }
+ return mService.disconnect(device);
+ }
+
+ @Override
+ public int getConnectionStatus(BluetoothDevice device) {
+ if (mService == null) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+
+ return !deviceList.isEmpty() && deviceList.contains(device)
+ ? mService.getConnectionState(device)
+ : BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ @Override
+ public boolean isPreferred(BluetoothDevice device) {
+ return getConnectionStatus(device) != BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ @Override
+ public int getPreferred(BluetoothDevice device) {
+ return PREFERRED_VALUE;
+ }
+
+ @Override
+ public void setPreferred(BluetoothDevice device, boolean preferred) {
+ // if set preferred to false, then disconnect to the current device
+ if (!preferred) {
+ mService.disconnect(device);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return NAME;
+ }
+
+ @Override
+ public int getOrdinal() {
+ return ORDINAL;
+ }
+
+ @Override
+ public int getNameResource(BluetoothDevice device) {
+ return R.string.bluetooth_profile_hid;
+ }
+
+ @Override
+ public int getSummaryResourceForDevice(BluetoothDevice device) {
+ final int state = getConnectionStatus(device);
+ switch (state) {
+ case BluetoothProfile.STATE_DISCONNECTED:
+ return R.string.bluetooth_hid_profile_summary_use_for;
+ case BluetoothProfile.STATE_CONNECTED:
+ return R.string.bluetooth_hid_profile_summary_connected;
+ default:
+ return Utils.getConnectionStateSummary(state);
+ }
+ }
+
+ @Override
+ public int getDrawableResource(BluetoothClass btClass) {
+ return R.drawable.ic_bt_misc_hid;
+ }
+
+ protected void finalize() {
+ if (DEBUG) {
+ Log.d(TAG, "finalize()");
+ }
+ if (mService != null) {
+ try {
+ BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HID_DEVICE,
+ mService);
+ mService = null;
+ } catch (Throwable t) {
+ Log.w(TAG, "Error cleaning up HID proxy", t);
+ }
+ }
+ }
+}
diff --git a/com/android/settingslib/bluetooth/HidProfile.java b/com/android/settingslib/bluetooth/HidProfile.java
index 213002fb..93c4017f 100644
--- a/com/android/settingslib/bluetooth/HidProfile.java
+++ b/com/android/settingslib/bluetooth/HidProfile.java
@@ -48,7 +48,7 @@ public class HidProfile implements LocalBluetoothProfile {
private static final int ORDINAL = 3;
// These callbacks run on the main thread.
- private final class InputDeviceServiceListener
+ private final class HidHostServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
@@ -86,7 +86,7 @@ public class HidProfile implements LocalBluetoothProfile {
mLocalAdapter = adapter;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
- adapter.getProfileProxy(context, new InputDeviceServiceListener(),
+ adapter.getProfileProxy(context, new HidHostServiceListener(),
BluetoothProfile.HID_HOST);
}
diff --git a/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 34a099cb..6413aab0 100644
--- a/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -22,6 +22,7 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothMapClient;
@@ -86,6 +87,7 @@ public class LocalBluetoothProfileManager {
private MapProfile mMapProfile;
private MapClientProfile mMapClientProfile;
private final HidProfile mHidProfile;
+ private HidDeviceProfile mHidDeviceProfile;
private OppProfile mOppProfile;
private final PanProfile mPanProfile;
private PbapClientProfile mPbapClientProfile;
@@ -123,7 +125,7 @@ public class LocalBluetoothProfileManager {
updateLocalProfiles(uuids);
}
- // Always add HID and PAN profiles
+ // Always add HID host, HID device, and PAN profiles
mHidProfile = new HidProfile(context, mLocalAdapter, mDeviceManager, this);
addProfile(mHidProfile, HidProfile.NAME,
BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
@@ -132,6 +134,10 @@ public class LocalBluetoothProfileManager {
addPanProfile(mPanProfile, PanProfile.NAME,
BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
+ mHidDeviceProfile = new HidDeviceProfile(context, mLocalAdapter, mDeviceManager, this);
+ addProfile(mHidDeviceProfile, HidDeviceProfile.NAME,
+ BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
+
if(DEBUG) Log.d(TAG, "Adding local MAP profile");
if (mUseMapClient) {
mMapClientProfile = new MapClientProfile(mContext, mLocalAdapter, mDeviceManager, this);
@@ -195,8 +201,10 @@ public class LocalBluetoothProfileManager {
if (DEBUG) Log.d(TAG, "Adding local HEADSET profile");
mHeadsetProfile = new HeadsetProfile(mContext, mLocalAdapter,
mDeviceManager, this);
- addProfile(mHeadsetProfile, HeadsetProfile.NAME,
- BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME,
+ BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
+ BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
+ BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
}
} else if (mHeadsetProfile != null) {
Log.w(TAG, "Warning: HEADSET profile was previously added but the UUID is now missing.");
@@ -208,8 +216,10 @@ public class LocalBluetoothProfileManager {
if(DEBUG) Log.d(TAG, "Adding local HfpClient profile");
mHfpClientProfile =
new HfpClientProfile(mContext, mLocalAdapter, mDeviceManager, this);
- addProfile(mHfpClientProfile, HfpClientProfile.NAME,
- BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
+ addHeadsetProfile(mHfpClientProfile, HfpClientProfile.NAME,
+ BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED,
+ BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED,
+ BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED);
}
} else if (mHfpClientProfile != null) {
Log.w(TAG,
@@ -277,6 +287,15 @@ public class LocalBluetoothProfileManager {
// There is no local SDP record for HID and Settings app doesn't control PBAP Server.
}
+ private void addHeadsetProfile(LocalBluetoothProfile profile, String profileName,
+ String stateChangedAction, String audioStateChangedAction, int audioDisconnectedState) {
+ BluetoothEventManager.Handler handler = new HeadsetStateChangeHandler(
+ profile, audioStateChangedAction, audioDisconnectedState);
+ mEventManager.addProfileHandler(stateChangedAction, handler);
+ mEventManager.addProfileHandler(audioStateChangedAction, handler);
+ mProfileNameMap.put(profileName, profile);
+ }
+
private final Collection<ServiceListener> mServiceListeners =
new ArrayList<ServiceListener>();
@@ -323,18 +342,47 @@ public class LocalBluetoothProfileManager {
cachedDevice = mDeviceManager.addDevice(mLocalAdapter,
LocalBluetoothProfileManager.this, device);
}
+ onReceiveInternal(intent, cachedDevice);
+ }
+
+ protected void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
if (newState == BluetoothProfile.STATE_DISCONNECTED &&
oldState == BluetoothProfile.STATE_CONNECTING) {
Log.i(TAG, "Failed to connect " + mProfile + " device");
}
-
cachedDevice.onProfileStateChanged(mProfile, newState);
cachedDevice.refresh();
}
}
+ /** Connectivity and audio state change handler for headset profiles. */
+ private class HeadsetStateChangeHandler extends StateChangedHandler {
+ private final String mAudioChangeAction;
+ private final int mAudioDisconnectedState;
+
+ HeadsetStateChangeHandler(LocalBluetoothProfile profile, String audioChangeAction,
+ int audioDisconnectedState) {
+ super(profile);
+ mAudioChangeAction = audioChangeAction;
+ mAudioDisconnectedState = audioDisconnectedState;
+ }
+
+ @Override
+ public void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
+ if (mAudioChangeAction.equals(intent.getAction())) {
+ int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
+ if (newState != mAudioDisconnectedState) {
+ cachedDevice.onProfileStateChanged(mProfile, BluetoothProfile.STATE_CONNECTED);
+ }
+ cachedDevice.refresh();
+ } else {
+ super.onReceiveInternal(intent, cachedDevice);
+ }
+ }
+ }
+
/** State change handler for NAP and PANU profiles. */
private class PanStateChangedHandler extends StateChangedHandler {
@@ -505,6 +553,12 @@ public class LocalBluetoothProfileManager {
removedProfiles.remove(mHidProfile);
}
+ if (mHidProfile != null && mHidDeviceProfile.getConnectionStatus(device)
+ != BluetoothProfile.STATE_DISCONNECTED) {
+ profiles.add(mHidDeviceProfile);
+ removedProfiles.remove(mHidDeviceProfile);
+ }
+
if(isPanNapConnected)
if(DEBUG) Log.d(TAG, "Valid PAN-NAP connection exists.");
if ((BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.NAP) &&
diff --git a/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 835ff07c..f7b16f8b 100644
--- a/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -148,15 +148,32 @@ public class BatterySaverUtils {
Secure.putInt(context.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
}
+ /**
+ * Don't show the automatic battery suggestion notification in the future.
+ */
public static void suppressAutoBatterySaver(Context context) {
Secure.putInt(context.getContentResolver(),
Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 1);
}
- public static void scheduleAutoBatterySaver(Context context, int level) {
+ /**
+ * Set the automatic battery saver trigger level to {@code level}.
+ */
+ public static void setAutoBatterySaverTriggerLevel(Context context, int level) {
+ if (level > 0) {
+ suppressAutoBatterySaver(context);
+ }
+ Global.putInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, level);
+ }
+
+ /**
+ * Set the automatic battery saver trigger level to {@code level}, but only when
+ * automatic battery saver isn't enabled yet.
+ */
+ public static void ensureAutoBatterySaver(Context context, int level) {
if (Global.getInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0)
== 0) {
- Global.putInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, level);
+ setAutoBatterySaverTriggerLevel(context, level);
}
}
}
diff --git a/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java b/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
index 70816782..06e2ee10 100644
--- a/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
+++ b/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java
@@ -16,6 +16,7 @@
package com.android.settingslib.fuelgauge;
+import android.content.pm.PackageManager;
import android.os.IDeviceIdleController;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -24,6 +25,8 @@ import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Log;
+import com.android.internal.util.ArrayUtils;
+
/**
* Handles getting/changing the whitelist for the exceptions to battery saving features.
*/
@@ -68,6 +71,19 @@ public class PowerWhitelistBackend {
return mSysWhitelistedAppsExceptIdle.contains(pkg);
}
+ public boolean isSysWhitelistedExceptIdle(String[] pkgs) {
+ if (ArrayUtils.isEmpty(pkgs)) {
+ return false;
+ }
+ for (String pkg : pkgs) {
+ if (isSysWhitelistedExceptIdle(pkg)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public void addApp(String pkg) {
try {
mDeviceIdleService.addPowerSaveWhitelistApp(pkg);
diff --git a/com/android/settingslib/users/UserManagerHelper.java b/com/android/settingslib/users/UserManagerHelper.java
index c4ca339b..113256ff 100644
--- a/com/android/settingslib/users/UserManagerHelper.java
+++ b/com/android/settingslib/users/UserManagerHelper.java
@@ -41,6 +41,7 @@ public final class UserManagerHelper {
private static final String TAG = "UserManagerHelper";
private final Context mContext;
private final UserManager mUserManager;
+ private final ActivityManager mActivityManager;
private OnUsersUpdateListener mUpdateListener;
private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
@Override
@@ -52,6 +53,7 @@ public final class UserManagerHelper {
public UserManagerHelper(Context context) {
mContext = context;
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
}
/**
@@ -72,30 +74,64 @@ public final class UserManagerHelper {
}
/**
- * Gets {@link UserInfo} for the current user.
+ * Gets UserInfo for the foreground user.
*
- * @return {@link UserInfo} for the current user.
+ * Concept of foreground user is relevant for the multi-user deployment. Foreground user
+ * corresponds to the currently "logged in" user.
+ *
+ * @return {@link UserInfo} for the foreground user.
+ */
+ public UserInfo getForegroundUserInfo() {
+ return mUserManager.getUserInfo(getForegroundUserId());
+ }
+
+ /**
+ * @return Id of the foreground user.
*/
- public UserInfo getCurrentUserInfo() {
- return mUserManager.getUserInfo(UserHandle.myUserId());
+ public int getForegroundUserId() {
+ return mActivityManager.getCurrentUser();
}
/**
- * Gets all the other users on the system that are not the current user.
+ * Gets UserInfo for the user running the caller process.
+ *
+ * Differentiation between foreground user and current process user is relevant for multi-user
+ * deployments.
*
- * @return List of {@code UserInfo} for each user that is not the current user.
+ * Some multi-user aware components (like SystemUI) might run as a singleton - one component
+ * for all users. Current process user is then always the same for that component, even when
+ * the foreground user changes.
+ *
+ * @return {@link UserInfo} for the user running the current process.
*/
- public List<UserInfo> getAllUsersExcludesCurrentUser() {
- List<UserInfo> others = getAllUsers();
+ public UserInfo getCurrentProcessUserInfo() {
+ return mUserManager.getUserInfo(getCurrentProcessUserId());
+ }
- for (Iterator<UserInfo> iterator = others.iterator(); iterator.hasNext(); ) {
- UserInfo userInfo = iterator.next();
- if (userInfo.id == UserHandle.myUserId()) {
- // Remove current user from the list.
- iterator.remove();
- }
- }
- return others;
+ /**
+ * @return Id for the user running the current process.
+ */
+ public int getCurrentProcessUserId() {
+ return UserHandle.myUserId();
+ }
+
+ /**
+ * Gets all the other users on the system that are not the user running the current process.
+ *
+ * @return List of {@code UserInfo} for each user that is not the user running the process.
+ */
+ public List<UserInfo> getAllUsersExcludesCurrentProcessUser() {
+ return getAllUsersExceptUser(getCurrentProcessUserId());
+ }
+
+ /**
+ * Gets all the existing users on the system that are not the currently running as the
+ * foreground user.
+ *
+ * @return List of {@code UserInfo} for each user that is not the foreground user.
+ */
+ public List<UserInfo> getAllUsersExcludesForegroundUser() {
+ return getAllUsersExceptUser(getForegroundUserId());
}
/**
@@ -104,12 +140,22 @@ public final class UserManagerHelper {
* @return List of {@code UserInfo} for each user that is not the system user.
*/
public List<UserInfo> getAllUsersExcludesSystemUser() {
+ return getAllUsersExceptUser(UserHandle.USER_SYSTEM);
+ }
+
+ /**
+ * Get all the users except the one with userId passed in.
+ *
+ * @param userId of the user not to be returned.
+ * @return All users other than user with userId.
+ */
+ public List<UserInfo> getAllUsersExceptUser(int userId) {
List<UserInfo> others = getAllUsers();
for (Iterator<UserInfo> iterator = others.iterator(); iterator.hasNext(); ) {
UserInfo userInfo = iterator.next();
- if (userIsSystemUser(userInfo)) {
- // Remove system user from the list.
+ if (userInfo.id == userId) {
+ // Remove user with userId from the list.
iterator.remove();
}
}
@@ -146,78 +192,115 @@ public final class UserManagerHelper {
}
/**
- * Checks whether passed in user is the user that's currently logged in.
+ * Checks whether passed in user is the foreground user.
+ *
+ * @param userInfo User to check.
+ * @return {@code true} if foreground user, {@code false} otherwise.
+ */
+ public boolean userIsForegroundUser(UserInfo userInfo) {
+ return getForegroundUserId() == userInfo.id;
+ }
+
+ /**
+ * Checks whether passed in user is the user that's running the current process.
*
* @param userInfo User to check.
- * @return {@code true} if current user, {@code false} otherwise.
+ * @return {@code true} if user running the process, {@code false} otherwise.
+ */
+ public boolean userIsRunningCurrentProcess(UserInfo userInfo) {
+ return getCurrentProcessUserId() == userInfo.id;
+ }
+
+ // Foreground user information accessors.
+
+ /**
+ * Checks if the foreground user is a guest user.
*/
- public boolean userIsCurrentUser(UserInfo userInfo) {
- return getCurrentUserInfo().id == userInfo.id;
+ public boolean foregroundUserIsGuestUser() {
+ return getForegroundUserInfo().isGuest();
}
- // Current user information accessors
+ /**
+ * Return whether the foreground user has a restriction.
+ *
+ * @param restriction Restriction to check. Should be a UserManager.* restriction.
+ * @return Whether that restriction exists for the foreground user.
+ */
+ public boolean foregroundUserHasUserRestriction(String restriction) {
+ return mUserManager.hasUserRestriction(restriction, getForegroundUserInfo().getUserHandle());
+ }
+
+ /**
+ * Checks if the foreground user can add new users.
+ */
+ public boolean foregroundUserCanAddUsers() {
+ return !foregroundUserHasUserRestriction(UserManager.DISALLOW_ADD_USER);
+ }
+
+ // Current process user information accessors
/**
- * Checks if the current user is a demo user.
+ * Checks if the calling app is running in a demo user.
*/
- public boolean isDemoUser() {
+ public boolean currentProcessRunningAsDemoUser() {
return mUserManager.isDemoUser();
}
/**
- * Checks if the current user is a guest user.
+ * Checks if the calling app is running as a guest user.
*/
- public boolean isGuestUser() {
+ public boolean currentProcessRunningAsGuestUser() {
return mUserManager.isGuestUser();
}
/**
- * Checks if the current user is the system user (User 0).
+ * Checks whether this process is running under the system user.
*/
- public boolean isSystemUser() {
+ public boolean currentProcessRunningAsSystemUser() {
return mUserManager.isSystemUser();
}
- // Current user restriction accessors
+ // Current process user restriction accessors
/**
- * Return whether the current user has a restriction.
+ * Return whether the user running the current process has a restriction.
*
* @param restriction Restriction to check. Should be a UserManager.* restriction.
- * @return Whether that restriction exists for the current user.
+ * @return Whether that restriction exists for the user running the process.
*/
- public boolean hasUserRestriction(String restriction) {
+ public boolean currentProcessHasUserRestriction(String restriction) {
return mUserManager.hasUserRestriction(restriction);
}
/**
- * Checks if the current user can add new users.
+ * Checks if the user running the current process can add new users.
*/
- public boolean canAddUsers() {
- return !hasUserRestriction(UserManager.DISALLOW_ADD_USER);
+ public boolean currentProcessCanAddUsers() {
+ return !currentProcessHasUserRestriction(UserManager.DISALLOW_ADD_USER);
}
/**
- * Checks if the current user can remove users.
+ * Checks if the user running the current process can remove users.
*/
- public boolean canRemoveUsers() {
- return !hasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
+ public boolean currentProcessCanRemoveUsers() {
+ return !currentProcessHasUserRestriction(UserManager.DISALLOW_REMOVE_USER);
}
/**
- * Checks if the current user is allowed to switch to another user.
+ * Checks if the user running the current process is allowed to switch to another user.
*/
- public boolean canSwitchUsers() {
- return !hasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
+ public boolean currentProcessCanSwitchUsers() {
+ return !currentProcessHasUserRestriction(UserManager.DISALLOW_USER_SWITCH);
}
/**
- * Checks if the current user can modify accounts. Demo and Guest users cannot modify accounts
- * even if the DISALLOW_MODIFY_ACCOUNTS restriction is not applied.
+ * Checks if the current process user can modify accounts. Demo and Guest users cannot modify
+ * accounts even if the DISALLOW_MODIFY_ACCOUNTS restriction is not applied.
*/
- public boolean canModifyAccounts() {
- return !hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS) && !isDemoUser()
- && !isGuestUser();
+ public boolean currentProcessCanModifyAccounts() {
+ return !currentProcessHasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)
+ && !currentProcessRunningAsDemoUser()
+ && !currentProcessRunningAsGuestUser();
}
// User actions
@@ -242,8 +325,8 @@ public final class UserManagerHelper {
/**
* Tries to remove the user that's passed in. System user cannot be removed.
- * If the user to be removed is current user, it switches to the system user first, and then
- * removes the user.
+ * If the user to be removed is user currently running the process,
+ * it switches to the system user first, and then removes the user.
*
* @param userInfo User to be removed
* @return {@code true} if user is successfully removed, {@code false} otherwise.
@@ -254,7 +337,7 @@ public final class UserManagerHelper {
return false;
}
- if (userInfo.id == getCurrentUserInfo().id) {
+ if (userInfo.id == getCurrentProcessUserId()) {
switchToUserId(UserHandle.USER_SYSTEM);
}
@@ -267,7 +350,7 @@ public final class UserManagerHelper {
* @param userInfo User to switch to.
*/
public void switchToUser(UserInfo userInfo) {
- if (userInfo.id == getCurrentUserInfo().id) {
+ if (userInfo.id == getForegroundUserId()) {
return;
}
@@ -276,15 +359,6 @@ public final class UserManagerHelper {
return;
}
- if (UserManager.isGuestUserEphemeral()) {
- // If switching from guest, we want to bring up the guest exit dialog instead of
- // switching
- UserInfo currUserInfo = getCurrentUserInfo();
- if (currUserInfo != null && currUserInfo.isGuest()) {
- return;
- }
- }
-
switchToUserId(userInfo.id);
}
@@ -348,6 +422,9 @@ public final class UserManagerHelper {
filter.addAction(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_INFO_CHANGED);
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ filter.addAction(Intent.ACTION_USER_STOPPED);
+ filter.addAction(Intent.ACTION_USER_UNLOCKED);
mContext.registerReceiverAsUser(mUserChangeReceiver, UserHandle.ALL, filter, null, null);
}
@@ -366,9 +443,7 @@ public final class UserManagerHelper {
private void switchToUserId(int id) {
try {
- final ActivityManager am = (ActivityManager)
- mContext.getSystemService(Context.ACTIVITY_SERVICE);
- am.switchUser(id);
+ mActivityManager.switchUser(id);
} catch (Exception e) {
Log.e(TAG, "Couldn't switch user.", e);
}
@@ -389,4 +464,3 @@ public final class UserManagerHelper {
void onUsersUpdate();
}
}
-
diff --git a/com/android/settingslib/utils/PowerUtil.java b/com/android/settingslib/utils/PowerUtil.java
index 8b3da394..de29030f 100644
--- a/com/android/settingslib/utils/PowerUtil.java
+++ b/com/android/settingslib/utils/PowerUtil.java
@@ -144,7 +144,8 @@ public class PowerUtil {
FIFTEEN_MINUTES_MILLIS);
// convert the time to a properly formatted string.
- DateFormat fmt = DateFormat.getTimeInstance(DateFormat.SHORT);
+ String skeleton = android.text.format.DateFormat.getTimeFormatString(context);
+ DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton);
Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs));
CharSequence timeString = fmt.format(date);
diff --git a/com/android/settingslib/wifi/WifiStatusTracker.java b/com/android/settingslib/wifi/WifiStatusTracker.java
index 9347674f..547cd9a6 100644
--- a/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -41,8 +41,9 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback {
private final WifiManager mWifiManager;
private final NetworkScoreManager mNetworkScoreManager;
private final ConnectivityManager mConnectivityManager;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
private final WifiNetworkScoreCache.CacheListener mCacheListener =
- new WifiNetworkScoreCache.CacheListener(new Handler(Looper.getMainLooper())) {
+ new WifiNetworkScoreCache.CacheListener(mHandler) {
@Override
public void networkCacheUpdated(List<ScoredNetwork> updatedNetworks) {
updateStatusLabel();
@@ -89,7 +90,8 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback {
mNetworkScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI,
mWifiNetworkScoreCache, NetworkScoreManager.CACHE_FILTER_CURRENT_NETWORK);
mWifiNetworkScoreCache.registerListener(mCacheListener);
- mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
+ mConnectivityManager.registerNetworkCallback(
+ mNetworkRequest, mNetworkCallback, mHandler);
} else {
mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI,
mWifiNetworkScoreCache);
diff --git a/com/android/settingslib/wifi/WifiTracker.java b/com/android/settingslib/wifi/WifiTracker.java
index a128b542..d8f08867 100644
--- a/com/android/settingslib/wifi/WifiTracker.java
+++ b/com/android/settingslib/wifi/WifiTracker.java
@@ -313,7 +313,8 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro
mContext.registerReceiver(mReceiver, mFilter, null /* permission */, mWorkHandler);
// NetworkCallback objects cannot be reused. http://b/20701525 .
mNetworkCallback = new WifiTrackerNetworkCallback();
- mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback);
+ mConnectivityManager.registerNetworkCallback(
+ mNetworkRequest, mNetworkCallback, mWorkHandler);
mRegistered = true;
}
}
@@ -788,7 +789,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro
// We don't send a NetworkInfo object along with this message, because even if we
// fetch one from ConnectivityManager, it might be older than the most recent
// NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast.
- mWorkHandler.post(() -> updateNetworkInfo(null));
+ updateNetworkInfo(null);
}
}
}
diff --git a/com/android/setupwizardlib/span/LinkSpan.java b/com/android/setupwizardlib/span/LinkSpan.java
index a5f04240..26a3d165 100644
--- a/com/android/setupwizardlib/span/LinkSpan.java
+++ b/com/android/setupwizardlib/span/LinkSpan.java
@@ -21,10 +21,13 @@ import android.content.ContextWrapper;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.Nullable;
+import android.text.Selection;
+import android.text.Spannable;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.View;
+import android.widget.TextView;
/**
* A clickable span that will listen for click events and send it back to the context. To use this
@@ -86,11 +89,19 @@ public class LinkSpan extends ClickableSpan {
public void onClick(View view) {
if (dispatchClick(view)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ // Prevent the touch event from bubbling up to the parent views.
view.cancelPendingInputEvents();
}
} else {
Log.w(TAG, "Dropping click event. No listener attached.");
}
+ if (view instanceof TextView) {
+ // Remove the highlight effect when the click happens by clearing the selection
+ CharSequence text = ((TextView) view).getText();
+ if (text instanceof Spannable) {
+ Selection.setSelection((Spannable) text, 0);
+ }
+ }
}
private boolean dispatchClick(View view) {
diff --git a/com/android/setupwizardlib/span/LinkSpanTest.java b/com/android/setupwizardlib/span/LinkSpanTest.java
index fe72e039..3aafa7db 100644
--- a/com/android/setupwizardlib/span/LinkSpanTest.java
+++ b/com/android/setupwizardlib/span/LinkSpanTest.java
@@ -16,11 +16,16 @@
package com.android.setupwizardlib.span;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertSame;
import static org.robolectric.RuntimeEnvironment.application;
import android.content.Context;
import android.content.ContextWrapper;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -32,7 +37,7 @@ import org.junit.runner.RunWith;
public class LinkSpanTest {
@Test
- public void testOnClick() {
+ public void onClick_shouldCallListenerOnContext() {
final TestContext context = new TestContext(application);
final TextView textView = new TextView(context);
final LinkSpan linkSpan = new LinkSpan("test_id");
@@ -43,7 +48,7 @@ public class LinkSpanTest {
}
@Test
- public void testNonImplementingContext() {
+ public void onClick_contextDoesNotImplementOnClickListener_shouldBeNoOp() {
final TextView textView = new TextView(application);
final LinkSpan linkSpan = new LinkSpan("test_id");
@@ -54,7 +59,7 @@ public class LinkSpanTest {
}
@Test
- public void testWrappedListener() {
+ public void onClick_contextWrapsOnClickListener_shouldCallWrappedListener() {
final TestContext context = new TestContext(application);
final Context wrapperContext = new ContextWrapper(context);
final TextView textView = new TextView(wrapperContext);
@@ -65,6 +70,27 @@ public class LinkSpanTest {
assertSame("Clicked LinkSpan should be passed to setup", linkSpan, context.clickedSpan);
}
+ @Test
+ public void onClick_shouldClearSelection() {
+ final TestContext context = new TestContext(application);
+ final TextView textView = new TextView(context);
+ textView.setMovementMethod(LinkMovementMethod.getInstance());
+ textView.setFocusable(true);
+ textView.setFocusableInTouchMode(true);
+ final LinkSpan linkSpan = new LinkSpan("test_id");
+
+ SpannableStringBuilder text = new SpannableStringBuilder("Lorem ipsum dolor sit");
+ textView.setText(text);
+ text.setSpan(linkSpan, /* start= */ 0, /* end= */ 5, /* flags= */ 0);
+ // Simulate the touch effect set by TextView when touched.
+ Selection.setSelection(text, /* start= */ 0, /* end= */ 5);
+
+ linkSpan.onClick(textView);
+
+ assertThat(Selection.getSelectionStart(textView.getText())).isEqualTo(0);
+ assertThat(Selection.getSelectionEnd(textView.getText())).isEqualTo(0);
+ }
+
@SuppressWarnings("deprecation")
private static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
diff --git a/com/android/setupwizardlib/view/RichTextView.java b/com/android/setupwizardlib/view/RichTextView.java
index aab3238b..fa68a68f 100644
--- a/com/android/setupwizardlib/view/RichTextView.java
+++ b/com/android/setupwizardlib/view/RichTextView.java
@@ -20,16 +20,18 @@ import android.content.Context;
import android.text.Annotation;
import android.text.SpannableString;
import android.text.Spanned;
-import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.TextAppearanceSpan;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.MotionEvent;
import android.widget.TextView;
import com.android.setupwizardlib.span.LinkSpan;
import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
import com.android.setupwizardlib.span.SpanHelper;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
/**
* An extension of TextView that automatically replaces the annotation tags as specified in
@@ -112,7 +114,7 @@ public class RichTextView extends TextView implements OnLinkClickListener {
// nullifying any return values of MovementMethod.onTouchEvent.
// To still allow propagating touch events to the parent when this view doesn't have
// links, we only set the movement method here if the text contains links.
- setMovementMethod(LinkMovementMethod.getInstance());
+ setMovementMethod(TouchableLinkMovementMethod.getInstance());
} else {
setMovementMethod(null);
}
@@ -137,6 +139,25 @@ public class RichTextView extends TextView implements OnLinkClickListener {
return false;
}
+ @Override
+ @SuppressWarnings("ClickableViewAccessibility") // super.onTouchEvent is called
+ public boolean onTouchEvent(MotionEvent event) {
+ // Since View#onTouchEvent always return true if the view is clickable (which is the case
+ // when a TextView has a movement method), override the implementation to allow the movement
+ // method, if it implements TouchableMovementMethod, to say that the touch is not handled,
+ // allowing the event to bubble up to the parent view.
+ boolean superResult = super.onTouchEvent(event);
+ MovementMethod movementMethod = getMovementMethod();
+ if (movementMethod instanceof TouchableMovementMethod) {
+ TouchableMovementMethod touchableMovementMethod =
+ (TouchableMovementMethod) movementMethod;
+ if (touchableMovementMethod.getLastTouchEvent() == event) {
+ return touchableMovementMethod.isLastTouchEventHandled();
+ }
+ }
+ return superResult;
+ }
+
public void setOnLinkClickListener(OnLinkClickListener listener) {
mOnLinkClickListener = listener;
}
diff --git a/com/android/setupwizardlib/test/RichTextViewTest.java b/com/android/setupwizardlib/view/RichTextViewTest.java
index 5f3eb9f6..f77de687 100644
--- a/com/android/setupwizardlib/test/RichTextViewTest.java
+++ b/com/android/setupwizardlib/view/RichTextViewTest.java
@@ -14,39 +14,45 @@
* limitations under the License.
*/
-package com.android.setupwizardlib.test;
+package com.android.setupwizardlib.view;
+
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.ContextWrapper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
import android.text.Annotation;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.TextAppearanceSpan;
+import android.view.MotionEvent;
+import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
import com.android.setupwizardlib.span.LinkSpan;
import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
-import com.android.setupwizardlib.view.RichTextView;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
import java.util.Arrays;
-@RunWith(AndroidJUnit4.class)
-@SmallTest
+@RunWith(SuwLibRobolectricTestRunner.class)
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
public class RichTextViewTest {
@Test
@@ -55,12 +61,14 @@ public class RichTextViewTest {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
- RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+ RichTextView textView = new RichTextView(application);
textView.setText(ssb);
final CharSequence text = textView.getText();
assertTrue("Text should be spanned", text instanceof Spanned);
+ assertThat(textView.getMovementMethod()).isInstanceOf(TouchableLinkMovementMethod.class);
+
Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class);
assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length);
@@ -77,7 +85,7 @@ public class RichTextViewTest {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
- RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+ RichTextView textView = new RichTextView(application);
textView.setText(ssb);
OnLinkClickListener listener = mock(OnLinkClickListener.class);
@@ -99,7 +107,7 @@ public class RichTextViewTest {
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
- TestContext context = spy(new TestContext(InstrumentationRegistry.getTargetContext()));
+ TestContext context = spy(new TestContext(application));
RichTextView textView = new RichTextView(context);
textView.setText(ssb);
@@ -111,12 +119,50 @@ public class RichTextViewTest {
}
@Test
+ public void onTouchEvent_clickOnLinks_shouldReturnTrue() {
+ Annotation link = new Annotation("link", "foobar");
+ SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
+ ssb.setSpan(link, 0, 2, 0 /* flags */);
+
+ RichTextView textView = new RichTextView(application);
+ textView.setText(ssb);
+
+ TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
+ textView.setMovementMethod(mockMovementMethod);
+
+ MotionEvent motionEvent =
+ MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
+ doReturn(true).when(mockMovementMethod).isLastTouchEventHandled();
+ assertThat(textView.onTouchEvent(motionEvent)).isTrue();
+ }
+
+ @Test
+ public void onTouchEvent_clickOutsideLinks_shouldReturnFalse() {
+ Annotation link = new Annotation("link", "foobar");
+ SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
+ ssb.setSpan(link, 0, 2, 0 /* flags */);
+
+ RichTextView textView = new RichTextView(application);
+ textView.setText(ssb);
+
+ TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
+ textView.setMovementMethod(mockMovementMethod);
+
+ MotionEvent motionEvent =
+ MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
+ doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
+ doReturn(false).when(mockMovementMethod).isLastTouchEventHandled();
+ assertThat(textView.onTouchEvent(motionEvent)).isFalse();
+ }
+
+ @Test
public void testTextStyle() {
Annotation link = new Annotation("textAppearance", "foobar");
SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
ssb.setSpan(link, 1, 2, 0 /* flags */);
- RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+ RichTextView textView = new RichTextView(application);
textView.setText(ssb);
final CharSequence text = textView.getText();
@@ -137,7 +183,7 @@ public class RichTextViewTest {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
spannableStringBuilder.setSpan(testLink, 0, 3, 0);
- RichTextView view = new RichTextView(InstrumentationRegistry.getContext());
+ RichTextView view = new RichTextView(application);
view.setText(spannableStringBuilder);
assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
@@ -147,7 +193,7 @@ public class RichTextViewTest {
@SuppressLint("SetTextI18n") // It's OK. This is just a test.
@Test
public void testTextContainingNoLinksAreNotFocusable() {
- RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+ RichTextView textView = new RichTextView(application);
textView.setText("Thou shall not be focusable!");
assertFalse("TextView should not be focusable since it does not contain any span",
@@ -160,16 +206,23 @@ public class RichTextViewTest {
@SuppressLint("SetTextI18n") // It's OK. This is just a test.
@Test
public void testRichTextViewFocusChangesWithTextChange() {
- RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+ RichTextView textView = new RichTextView(application);
textView.setText("Thou shall not be focusable!");
assertFalse(textView.isFocusable());
+ assertFalse(textView.isFocusableInTouchMode());
SpannableStringBuilder spannableStringBuilder =
new SpannableStringBuilder("I am focusable");
spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
textView.setText(spannableStringBuilder);
assertTrue(textView.isFocusable());
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ assertTrue(textView.isFocusableInTouchMode());
+ assertFalse(textView.getRevealOnFocusHint());
+ } else {
+ assertFalse(textView.isFocusableInTouchMode());
+ }
}
public static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
diff --git a/com/android/setupwizardlib/view/TouchableMovementMethod.java b/com/android/setupwizardlib/view/TouchableMovementMethod.java
new file mode 100644
index 00000000..10e91f46
--- /dev/null
+++ b/com/android/setupwizardlib/view/TouchableMovementMethod.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.setupwizardlib.view;
+
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * A movement method that tracks the last result of whether touch events are handled. This is
+ * used to patch the return value of {@link TextView#onTouchEvent} so that it consumes the touch
+ * events only when the movement method says the event is consumed.
+ */
+public interface TouchableMovementMethod {
+
+ /**
+ * @return The last touch event received in {@link MovementMethod#onTouchEvent}
+ */
+ MotionEvent getLastTouchEvent();
+
+ /**
+ * @return The return value of the last {@link MovementMethod#onTouchEvent}, or whether the
+ * last touch event should be considered handled by the text view
+ */
+ boolean isLastTouchEventHandled();
+
+ /**
+ * An extension of LinkMovementMethod that tracks whether the event is handled when it is
+ * touched.
+ */
+ class TouchableLinkMovementMethod extends LinkMovementMethod
+ implements TouchableMovementMethod {
+
+ public static TouchableLinkMovementMethod getInstance() {
+ return new TouchableLinkMovementMethod();
+ }
+
+ boolean mLastEventResult = false;
+ MotionEvent mLastEvent;
+
+ @Override
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ mLastEvent = event;
+ boolean result = super.onTouchEvent(widget, buffer, event);
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // Unfortunately, LinkMovementMethod extends ScrollMovementMethod, and it always
+ // consume the down event. So here we use the selection instead as a hint of whether
+ // the down event landed on a link.
+ mLastEventResult = Selection.getSelectionStart(buffer) != -1;
+ } else {
+ mLastEventResult = result;
+ }
+ return result;
+ }
+
+ @Override
+ public MotionEvent getLastTouchEvent() {
+ return mLastEvent;
+ }
+
+ @Override
+ public boolean isLastTouchEventHandled() {
+ return mLastEventResult;
+ }
+ }
+}
diff --git a/com/android/systemui/BatteryMeterView.java b/com/android/systemui/BatteryMeterView.java
index 1ae06d75..0683514f 100644
--- a/com/android/systemui/BatteryMeterView.java
+++ b/com/android/systemui/BatteryMeterView.java
@@ -81,6 +81,14 @@ public class BatteryMeterView extends LinearLayout implements
private float mDarkIntensity;
private int mUser;
+ /**
+ * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings.
+ */
+ private boolean mUseWallpaperTextColors;
+
+ private int mNonAdaptedForegroundColor;
+ private int mNonAdaptedBackgroundColor;
+
public BatteryMeterView(Context context) {
this(context, null, 0);
}
@@ -140,6 +148,29 @@ public class BatteryMeterView extends LinearLayout implements
updateShowPercent();
}
+ /**
+ * Sets whether the battery meter view uses the wallpaperTextColor. If we're not using it, we'll
+ * revert back to dark-mode-based/tinted colors.
+ *
+ * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for all
+ * components
+ */
+ public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) {
+ if (shouldUseWallpaperTextColor == mUseWallpaperTextColors) {
+ return;
+ }
+
+ mUseWallpaperTextColors = shouldUseWallpaperTextColor;
+
+ if (mUseWallpaperTextColors) {
+ updateColors(
+ Utils.getColorAttr(mContext, R.attr.wallpaperTextColor),
+ Utils.getColorAttr(mContext, R.attr.wallpaperTextColorSecondary));
+ } else {
+ updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor);
+ }
+ }
+
public void setColorsFromContext(Context context) {
if (context == null) {
return;
@@ -179,7 +210,8 @@ public class BatteryMeterView extends LinearLayout implements
getContext().getContentResolver().registerContentObserver(
Settings.System.getUriFor(SHOW_BATTERY_PERCENT), false, mSettingObserver, mUser);
updateShowPercent();
- Dependency.get(TunerService.class).addTunable(this, StatusBarIconController.ICON_BLACKLIST);
+ Dependency.get(TunerService.class)
+ .addTunable(this, StatusBarIconController.ICON_BLACKLIST);
Dependency.get(ConfigurationController.class).addCallback(this);
mUserTracker.startTracking();
}
@@ -273,19 +305,23 @@ public class BatteryMeterView extends LinearLayout implements
@Override
public void onDarkChanged(Rect area, float darkIntensity, int tint) {
mDarkIntensity = darkIntensity;
+
float intensity = DarkIconDispatcher.isInArea(area, this) ? darkIntensity : 0;
- int foreground = getColorForDarkIntensity(intensity, mLightModeFillColor,
- mDarkModeFillColor);
- int background = getColorForDarkIntensity(intensity, mLightModeBackgroundColor,
- mDarkModeBackgroundColor);
- mDrawable.setColors(foreground, background);
- setTextColor(foreground);
+ mNonAdaptedForegroundColor = getColorForDarkIntensity(
+ intensity, mLightModeFillColor, mDarkModeFillColor);
+ mNonAdaptedBackgroundColor = getColorForDarkIntensity(
+ intensity, mLightModeBackgroundColor,mDarkModeBackgroundColor);
+
+ if (!mUseWallpaperTextColors) {
+ updateColors(mNonAdaptedForegroundColor, mNonAdaptedBackgroundColor);
+ }
}
- public void setTextColor(int color) {
- mTextColor = color;
+ private void updateColors(int foregroundColor, int backgroundColor) {
+ mDrawable.setColors(foregroundColor, backgroundColor);
+ mTextColor = foregroundColor;
if (mBatteryPercentView != null) {
- mBatteryPercentView.setTextColor(color);
+ mBatteryPercentView.setTextColor(foregroundColor);
}
}
diff --git a/com/android/systemui/ForegroundServiceController.java b/com/android/systemui/ForegroundServiceController.java
index 5a2263cf..ae6ee2af 100644
--- a/com/android/systemui/ForegroundServiceController.java
+++ b/com/android/systemui/ForegroundServiceController.java
@@ -73,7 +73,7 @@ public interface ForegroundServiceController {
void onAppOpChanged(int code, int uid, String packageName, boolean active);
/**
- * Gets active app ops for this user and package.
+ * Gets active app ops for this user and package
*/
@Nullable ArraySet<Integer> getAppOps(int userId, String packageName);
}
diff --git a/com/android/systemui/ImageWallpaper.java b/com/android/systemui/ImageWallpaper.java
index a4f8d8c1..b8a57bfe 100644
--- a/com/android/systemui/ImageWallpaper.java
+++ b/com/android/systemui/ImageWallpaper.java
@@ -444,13 +444,7 @@ public class ImageWallpaper extends WallpaperService {
final Surface surface = getSurfaceHolder().getSurface();
surface.hwuiDestroy();
- mLoader = new AsyncTask<Void, Void, Bitmap>() {
- @Override
- protected Bitmap doInBackground(Void... params) {
- mWallpaperManager.forgetLoadedWallpaper();
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ mWallpaperManager.forgetLoadedWallpaper();
}
private void scheduleUnloadWallpaper() {
diff --git a/com/android/systemui/OverviewProxyService.java b/com/android/systemui/OverviewProxyService.java
index 8cff56df..b1020cfb 100644
--- a/com/android/systemui/OverviewProxyService.java
+++ b/com/android/systemui/OverviewProxyService.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Handler;
@@ -39,6 +40,7 @@ import com.android.systemui.recents.events.activity.DockedFirstAnimationFrameEve
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.GraphicBufferCompat;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.CallbackController;
@@ -50,6 +52,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP;
import static com.android.systemui.shared.system.NavigationBarCompat.InteractionType;
@@ -71,11 +74,13 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private final DeviceProvisionedController mDeviceProvisionedController
= Dependency.get(DeviceProvisionedController.class);
private final List<OverviewProxyListener> mConnectionCallbacks = new ArrayList<>();
+ private final Intent mQuickStepIntent;
private IOverviewProxy mOverviewProxy;
private int mConnectionBackoffAttempts;
private CharSequence mOnboardingText;
private @InteractionType int mInteractionFlags;
+ private boolean mIsEnabled;
private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() {
@@ -130,14 +135,23 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
});
}
} finally {
+ Prefs.putInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, mInteractionFlags);
Binder.restoreCallingIdentity(token);
}
}
};
- private final BroadcastReceiver mLauncherAddedReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver mLauncherStateChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
+ updateEnabledState();
+
+ // When launcher service is disabled, reset interaction flags because it is inactive
+ if (!isEnabled()) {
+ mInteractionFlags = 0;
+ Prefs.remove(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS);
+ }
+
// Reconnect immediately, instead of waiting for resume to arrive.
startConnectionToCurrentUser();
}
@@ -196,17 +210,21 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mConnectionBackoffAttempts = 0;
mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
com.android.internal.R.string.config_recentsComponentName));
+ mQuickStepIntent = new Intent(ACTION_QUICKSTEP)
+ .setPackage(mRecentsComponentName.getPackageName());
+ mInteractionFlags = Prefs.getInt(mContext, Prefs.Key.QUICK_STEP_INTERACTION_FLAGS, 0);
// Listen for the package update changes.
if (SystemServicesProxy.getInstance(context)
.isSystemUser(mDeviceProvisionedController.getCurrentUser())) {
+ updateEnabledState();
mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback);
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
filter.addDataSchemeSpecificPart(mRecentsComponentName.getPackageName(),
PatternMatcher.PATTERN_LITERAL);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
- mContext.registerReceiver(mLauncherAddedReceiver, filter);
+ mContext.registerReceiver(mLauncherStateChangedReceiver, filter);
}
}
@@ -222,7 +240,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
disconnectFromLauncherService();
// If user has not setup yet or already connected, do not try to connect
- if (!mDeviceProvisionedController.isCurrentUserSetup()) {
+ if (!mDeviceProvisionedController.isCurrentUserSetup() || !isEnabled()) {
return;
}
mHandler.removeCallbacks(mConnectionRunnable);
@@ -248,6 +266,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
public void addCallback(OverviewProxyListener listener) {
mConnectionCallbacks.add(listener);
listener.onConnectionChanged(mOverviewProxy != null);
+ listener.onInteractionFlagsChanged(mInteractionFlags);
}
@Override
@@ -256,7 +275,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
public boolean shouldShowSwipeUpUI() {
- return getProxy() != null && ((mInteractionFlags & FLAG_DISABLE_SWIPE_UP) == 0);
+ return isEnabled() && ((mInteractionFlags & FLAG_DISABLE_SWIPE_UP) == 0);
+ }
+
+ public boolean isEnabled() {
+ return mIsEnabled;
}
public IOverviewProxy getProxy() {
@@ -292,6 +315,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
}
+ private void updateEnabledState() {
+ mIsEnabled = mContext.getPackageManager().resolveServiceAsUser(mQuickStepIntent,
+ MATCH_DIRECT_BOOT_UNAWARE,
+ ActivityManagerWrapper.getInstance().getCurrentUserId()) != null;
+ }
+
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(TAG_OPS + " state:");
diff --git a/com/android/systemui/Prefs.java b/com/android/systemui/Prefs.java
index 2a271473..7f7a7692 100644
--- a/com/android/systemui/Prefs.java
+++ b/com/android/systemui/Prefs.java
@@ -54,7 +54,8 @@ public final class Prefs {
Key.HAS_SEEN_RECENTS_ONBOARDING,
Key.SEEN_RINGER_GUIDANCE_COUNT,
Key.QS_HAS_TURNED_OFF_MOBILE_DATA,
- Key.TOUCHED_RINGER_TOGGLE
+ Key.TOUCHED_RINGER_TOGGLE,
+ Key.QUICK_STEP_INTERACTION_FLAGS
})
public @interface Key {
@Deprecated
@@ -93,6 +94,7 @@ public final class Prefs {
String QS_TILE_SPECS_REVEALED = "QsTileSpecsRevealed";
String QS_HAS_TURNED_OFF_MOBILE_DATA = "QsHasTurnedOffMobileData";
String TOUCHED_RINGER_TOGGLE = "TouchedRingerToggle";
+ String QUICK_STEP_INTERACTION_FLAGS = "QuickStepInteractionFlags";
}
public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
diff --git a/com/android/systemui/ScreenDecorations.java b/com/android/systemui/ScreenDecorations.java
index a0fa69e6..e54b083e 100644
--- a/com/android/systemui/ScreenDecorations.java
+++ b/com/android/systemui/ScreenDecorations.java
@@ -14,6 +14,10 @@
package com.android.systemui;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -21,12 +25,14 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M
import static com.android.systemui.tuner.TunablePadding.FLAG_START;
import static com.android.systemui.tuner.TunablePadding.FLAG_END;
+import android.annotation.Dimension;
import android.app.Fragment;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
@@ -41,6 +47,7 @@ import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.LayoutInflater;
+import android.view.Surface;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
@@ -359,6 +366,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
if (!mBoundingPath.isEmpty()) {
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setAntiAlias(true);
canvas.drawPath(mBoundingPath, mPaint);
}
}
@@ -388,7 +396,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
if (hasCutout()) {
mBounds.set(mInfo.displayCutout.getBounds());
localBounds(mBoundingRect);
- mInfo.displayCutout.getBounds().getBoundaryPath(mBoundingPath);
+ updateBoundingPath();
invalidate();
newVisible = VISIBLE;
} else {
@@ -400,6 +408,44 @@ public class ScreenDecorations extends SystemUI implements Tunable {
}
}
+ private void updateBoundingPath() {
+ int lw = mInfo.logicalWidth;
+ int lh = mInfo.logicalHeight;
+
+ boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
+
+ int dw = flipped ? lh : lw;
+ int dh = flipped ? lw : lh;
+
+ mBoundingPath.set(DisplayCutout.pathFromResources(getResources(), lw, lh));
+ Matrix m = new Matrix();
+ transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
+ mBoundingPath.transform(m);
+ }
+
+ private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
+ @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
+ switch (rotation) {
+ case ROTATION_0:
+ out.reset();
+ break;
+ case ROTATION_90:
+ out.setRotate(270);
+ out.postTranslate(0, physicalWidth);
+ break;
+ case ROTATION_180:
+ out.setRotate(180);
+ out.postTranslate(physicalWidth, physicalHeight);
+ break;
+ case ROTATION_270:
+ out.setRotate(90);
+ out.postTranslate(physicalHeight, 0);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown rotation: " + rotation);
+ }
+ }
+
private boolean hasCutout() {
final DisplayCutout displayCutout = mInfo.displayCutout;
if (displayCutout == null) {
diff --git a/com/android/systemui/SystemUIFactory.java b/com/android/systemui/SystemUIFactory.java
index 039e7b5a..52d458ce 100644
--- a/com/android/systemui/SystemUIFactory.java
+++ b/com/android/systemui/SystemUIFactory.java
@@ -25,6 +25,7 @@ import android.view.ViewGroup;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.colorextraction.ColorExtractor.GradientColors;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.Dependency.DependencyProvider;
import com.android.systemui.classifier.FalsingManager;
@@ -41,9 +42,11 @@ import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.ScrimView;
+import com.android.systemui.statusbar.SmartReplyLogger;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.phone.LockIcon;
import com.android.systemui.statusbar.phone.LockscreenWallpaper;
@@ -100,12 +103,13 @@ public class SystemUIFactory {
dismissCallbackRegistry, FalsingManager.getInstance(context));
}
- public ScrimController createScrimController(LightBarController lightBarController,
- ScrimView scrimBehind, ScrimView scrimInFront, LockscreenWallpaper lockscreenWallpaper,
+ public ScrimController createScrimController(ScrimView scrimBehind, ScrimView scrimInFront,
+ LockscreenWallpaper lockscreenWallpaper, Consumer<Float> scrimBehindAlphaListener,
+ Consumer<GradientColors> scrimInFrontColorListener,
Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters,
AlarmManager alarmManager) {
- return new ScrimController(lightBarController, scrimBehind, scrimInFront,
- scrimVisibleListener, dozeParameters, alarmManager);
+ return new ScrimController(scrimBehind, scrimInFront, scrimBehindAlphaListener,
+ scrimInFrontColorListener, scrimVisibleListener, dozeParameters, alarmManager);
}
public NotificationIconAreaController createNotificationIconAreaController(Context context,
@@ -142,5 +146,7 @@ public class SystemUIFactory {
providers.put(NotificationViewHierarchyManager.class,
() -> new NotificationViewHierarchyManager(context));
providers.put(NotificationEntryManager.class, () -> new NotificationEntryManager(context));
+ providers.put(KeyguardDismissUtil.class, KeyguardDismissUtil::new);
+ providers.put(SmartReplyLogger.class, () -> new SmartReplyLogger(context));
}
}
diff --git a/com/android/systemui/classifier/AnglesClassifier.java b/com/android/systemui/classifier/AnglesClassifier.java
index e18ac74d..cdf4ba7d 100644
--- a/com/android/systemui/classifier/AnglesClassifier.java
+++ b/com/android/systemui/classifier/AnglesClassifier.java
@@ -16,6 +16,9 @@
package com.android.systemui.classifier;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.Log;
import android.view.MotionEvent;
import java.util.ArrayList;
@@ -49,13 +52,18 @@ import java.util.List;
public class AnglesClassifier extends StrokeClassifier {
private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
+ public static final boolean VERBOSE = SystemProperties.getBoolean("debug.falsing_log.ang",
+ Build.IS_DEBUGGABLE);
+
+ private static String TAG = "ANG";
+
public AnglesClassifier(ClassifierData classifierData) {
mClassifierData = classifierData;
}
@Override
public String getTag() {
- return "ANG";
+ return TAG;
}
@Override
@@ -170,18 +178,31 @@ public class AnglesClassifier extends StrokeClassifier {
public float getAnglesVariance() {
float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ if (VERBOSE) {
+ FalsingLog.i(TAG, "getAnglesVariance: (first pass) " + anglesVariance);
+ FalsingLog.i(TAG, " - mFirstLength=" + mFirstLength);
+ FalsingLog.i(TAG, " - mLength=" + mLength);
+ }
if (mFirstLength < mLength / 2f) {
anglesVariance = Math.min(anglesVariance, mFirstAngleVariance
+ getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
+ if (VERBOSE) FalsingLog.i(TAG, "getAnglesVariance: (second pass) " + anglesVariance);
}
return anglesVariance;
}
public float getAnglesPercentage() {
if (mAnglesCount == 0.0f) {
+ if (VERBOSE) FalsingLog.i(TAG, "getAnglesPercentage: count==0, result=1");
return 1.0f;
}
- return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+ final float result = (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+ if (VERBOSE) {
+ FalsingLog.i(TAG, "getAnglesPercentage: left=" + mLeftAngles + " right="
+ + mRightAngles + " straight=" + mStraightAngles + " count=" + mAnglesCount
+ + " result=" + result);
+ }
+ return result;
}
}
-} \ No newline at end of file
+}
diff --git a/com/android/systemui/classifier/AnglesVarianceEvaluator.java b/com/android/systemui/classifier/AnglesVarianceEvaluator.java
index 6883dd0a..9ffe783f 100644
--- a/com/android/systemui/classifier/AnglesVarianceEvaluator.java
+++ b/com/android/systemui/classifier/AnglesVarianceEvaluator.java
@@ -18,14 +18,11 @@ package com.android.systemui.classifier;
public class AnglesVarianceEvaluator {
public static float evaluate(float value, int type) {
- final boolean secureUnlock = type == Classifier.BOUNCER_UNLOCK;
float evaluation = 0.0f;
- if (value > 0.05) evaluation++;
- if (value > 0.10) evaluation++;
if (value > 0.20) evaluation++;
- if (value > 0.40 && !secureUnlock) evaluation++;
- if (value > 0.80 && !secureUnlock) evaluation++;
- if (value > 1.50 && !secureUnlock) evaluation++;
+ if (value > 0.40) evaluation++;
+ if (value > 0.80) evaluation++;
+ if (value > 1.50) evaluation++;
return evaluation;
}
}
diff --git a/com/android/systemui/classifier/SpeedAnglesClassifier.java b/com/android/systemui/classifier/SpeedAnglesClassifier.java
index 6df72b15..66f0cf68 100644
--- a/com/android/systemui/classifier/SpeedAnglesClassifier.java
+++ b/com/android/systemui/classifier/SpeedAnglesClassifier.java
@@ -16,6 +16,8 @@
package com.android.systemui.classifier;
+import android.os.Build;
+import android.os.SystemProperties;
import android.view.MotionEvent;
import java.util.ArrayList;
@@ -34,6 +36,10 @@ import java.util.List;
* should be in this interval.
*/
public class SpeedAnglesClassifier extends StrokeClassifier {
+ public static final boolean VERBOSE = SystemProperties.getBoolean("debug.falsing_log.spd_ang",
+ Build.IS_DEBUGGABLE);
+ public static final String TAG = "SPD_ANG";
+
private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
public SpeedAnglesClassifier(ClassifierData classifierData) {
@@ -42,7 +48,7 @@ public class SpeedAnglesClassifier extends StrokeClassifier {
@Override
public String getTag() {
- return "SPD_ANG";
+ return TAG;
}
@Override
@@ -135,14 +141,24 @@ public class SpeedAnglesClassifier extends StrokeClassifier {
}
public float getAnglesVariance() {
- return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+ final float v = mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+ if (VERBOSE) {
+ FalsingLog.i(TAG, "getAnglesVariance: sum^2=" + mSumSquares
+ + " count=" + mCount + " result=" + v);
+ }
+ return v;
}
public float getAnglesPercentage() {
if (mAnglesCount == 0.0f) {
return 1.0f;
}
- return (mAcceleratingAngles) / mAnglesCount;
+ final float v = (mAcceleratingAngles) / mAnglesCount;
+ if (VERBOSE) {
+ FalsingLog.i(TAG, "getAnglesPercentage: angles=" + mAcceleratingAngles
+ + " count=" + mAnglesCount + " result=" + v);
+ }
+ return v;
}
}
} \ No newline at end of file
diff --git a/com/android/systemui/doze/DozeUi.java b/com/android/systemui/doze/DozeUi.java
index 778e6309..c3907649 100644
--- a/com/android/systemui/doze/DozeUi.java
+++ b/com/android/systemui/doze/DozeUi.java
@@ -109,7 +109,11 @@ public class DozeUi implements DozeMachine.Part {
switch (newState) {
case DOZE_AOD:
if (oldState == DOZE_AOD_PAUSED) {
+ // Whenever turning on the display, it's necessary to push a new frame.
+ // The display buffers will be empty and need to be filled.
mHost.dozeTimeTick();
+ // The first frame may arrive when the display isn't ready yet.
+ mHandler.postDelayed(mHost::dozeTimeTick, 100);
}
scheduleTimeTick();
break;
diff --git a/com/android/systemui/fingerprint/FingerprintDialogImpl.java b/com/android/systemui/fingerprint/FingerprintDialogImpl.java
index 3577c0fa..a81043e2 100644
--- a/com/android/systemui/fingerprint/FingerprintDialogImpl.java
+++ b/com/android/systemui/fingerprint/FingerprintDialogImpl.java
@@ -18,8 +18,8 @@ package com.android.systemui.fingerprint;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.hardware.biometrics.BiometricDialog;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.BiometricPrompt;
+import android.hardware.biometrics.IBiometricPromptReceiver;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@@ -48,7 +48,7 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
private FingerprintDialogView mDialogView;
private WindowManager mWindowManager;
- private IBiometricDialogReceiver mReceiver;
+ private IBiometricPromptReceiver mReceiver;
private boolean mDialogShowing;
private Handler mHandler = new Handler() {
@@ -97,7 +97,7 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
}
@Override
- public void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) {
+ public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) {
if (DEBUG) Log.d(TAG, "showFingerprintDialog");
// Remove these messages as they are part of the previous client
mHandler.removeMessages(MSG_FINGERPRINT_ERROR);
@@ -134,12 +134,15 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
}
private void handleShowDialog(SomeArgs args) {
- if (DEBUG) Log.d(TAG, "handleShowDialog");
- if (mDialogShowing) {
+ if (DEBUG) Log.d(TAG, "handleShowDialog, isAnimatingAway: "
+ + mDialogView.isAnimatingAway());
+ if (mDialogView.isAnimatingAway()) {
+ mDialogView.forceRemove();
+ } else if (mDialogShowing) {
Log.w(TAG, "Dialog already showing");
return;
}
- mReceiver = (IBiometricDialogReceiver) args.arg2;
+ mReceiver = (IBiometricPromptReceiver) args.arg2;
mDialogView.setBundle((Bundle)args.arg1);
mWindowManager.addView(mDialogView, mDialogView.getLayoutParams());
mDialogShowing = true;
@@ -168,7 +171,7 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
}
private void handleHideDialog(boolean userCanceled) {
- if (DEBUG) Log.d(TAG, "handleHideDialog");
+ if (DEBUG) Log.d(TAG, "handleHideDialog, userCanceled: " + userCanceled);
if (!mDialogShowing) {
// This can happen if there's a race and we get called from both
// onAuthenticated and onError, etc.
@@ -177,7 +180,7 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
}
if (userCanceled) {
try {
- mReceiver.onDialogDismissed(BiometricDialog.DISMISSED_REASON_USER_CANCEL);
+ mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when hiding dialog", e);
}
@@ -193,7 +196,7 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
return;
}
try {
- mReceiver.onDialogDismissed(BiometricDialog.DISMISSED_REASON_NEGATIVE);
+ mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_NEGATIVE);
} catch (RemoteException e) {
Log.e(TAG, "Remote exception when handling negative button", e);
}
@@ -206,7 +209,7 @@ public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Call
return;
}
try {
- mReceiver.onDialogDismissed(BiometricDialog.DISMISSED_REASON_POSITIVE);
+ mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_POSITIVE);
} catch (RemoteException e) {
Log.e(TAG, "Remote exception when handling positive button", e);
}
diff --git a/com/android/systemui/fingerprint/FingerprintDialogView.java b/com/android/systemui/fingerprint/FingerprintDialogView.java
index 95258b01..8013a9e2 100644
--- a/com/android/systemui/fingerprint/FingerprintDialogView.java
+++ b/com/android/systemui/fingerprint/FingerprintDialogView.java
@@ -17,17 +17,16 @@
package com.android.systemui.fingerprint;
import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
-import android.hardware.biometrics.BiometricDialog;
+import android.hardware.biometrics.BiometricPrompt;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
@@ -44,7 +43,6 @@ import android.widget.TextView;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
-import com.android.systemui.util.leak.RotationUtils;
/**
* This class loads the view for the system-provided dialog. The view consists of:
@@ -67,7 +65,7 @@ public class FingerprintDialogView extends LinearLayout {
private final Interpolator mLinearOutSlowIn;
private final WindowManager mWindowManager;
private final float mAnimationTranslationOffset;
- private final int mErrorTextColor;
+ private final int mErrorColor;
private final int mTextColor;
private final int mFingerprintColor;
@@ -77,9 +75,29 @@ public class FingerprintDialogView extends LinearLayout {
private Bundle mBundle;
private final LinearLayout mDialog;
private int mLastState;
+ private boolean mAnimatingAway;
+ private boolean mWasForceRemoved;
private final float mDisplayWidth;
+ private final Runnable mShowAnimationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mLayout.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION_SHOW)
+ .setInterpolator(mLinearOutSlowIn)
+ .withLayer()
+ .start();
+ mDialog.animate()
+ .translationY(0)
+ .setDuration(ANIMATION_DURATION_SHOW)
+ .setInterpolator(mLinearOutSlowIn)
+ .withLayer()
+ .start();
+ }
+ };
+
public FingerprintDialogView(Context context, Handler handler) {
super(context);
mHandler = handler;
@@ -87,8 +105,8 @@ public class FingerprintDialogView extends LinearLayout {
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mAnimationTranslationOffset = getResources()
.getDimension(R.dimen.fingerprint_dialog_animation_translation_offset);
- mErrorTextColor = Color.parseColor(
- getResources().getString(R.color.fingerprint_dialog_error_message_color));
+ mErrorColor = Color.parseColor(
+ getResources().getString(R.color.fingerprint_dialog_error_color));
mTextColor = Color.parseColor(
getResources().getString(R.color.fingerprint_dialog_text_light_color));
mFingerprintColor = Color.parseColor(
@@ -163,29 +181,29 @@ public class FingerprintDialogView extends LinearLayout {
mLastState = STATE_NONE;
updateFingerprintIcon(STATE_FINGERPRINT);
- title.setText(mBundle.getCharSequence(BiometricDialog.KEY_TITLE));
+ title.setText(mBundle.getCharSequence(BiometricPrompt.KEY_TITLE));
title.setSelected(true);
- final CharSequence subtitleText = mBundle.getCharSequence(BiometricDialog.KEY_SUBTITLE);
- if (subtitleText == null) {
+ final CharSequence subtitleText = mBundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
+ if (TextUtils.isEmpty(subtitleText)) {
subtitle.setVisibility(View.GONE);
} else {
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(subtitleText);
}
- final CharSequence descriptionText = mBundle.getCharSequence(BiometricDialog.KEY_DESCRIPTION);
- if (descriptionText == null) {
- subtitle.setVisibility(View.VISIBLE);
+ final CharSequence descriptionText = mBundle.getCharSequence(BiometricPrompt.KEY_DESCRIPTION);
+ if (TextUtils.isEmpty(descriptionText)) {
description.setVisibility(View.GONE);
} else {
- description.setText(mBundle.getCharSequence(BiometricDialog.KEY_DESCRIPTION));
+ description.setVisibility(View.VISIBLE);
+ description.setText(descriptionText);
}
- negative.setText(mBundle.getCharSequence(BiometricDialog.KEY_NEGATIVE_TEXT));
+ negative.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT));
final CharSequence positiveText =
- mBundle.getCharSequence(BiometricDialog.KEY_POSITIVE_TEXT);
+ mBundle.getCharSequence(BiometricPrompt.KEY_POSITIVE_TEXT);
positive.setText(positiveText); // needs to be set for marquee to work
if (positiveText != null) {
positive.setVisibility(View.VISIBLE);
@@ -193,26 +211,20 @@ public class FingerprintDialogView extends LinearLayout {
positive.setVisibility(View.GONE);
}
- // Dim the background and slide the dialog up
- mDialog.setTranslationY(mAnimationTranslationOffset);
- mLayout.setAlpha(0f);
- postOnAnimation(new Runnable() {
- @Override
- public void run() {
- mLayout.animate()
- .alpha(1f)
- .setDuration(ANIMATION_DURATION_SHOW)
- .setInterpolator(mLinearOutSlowIn)
- .withLayer()
- .start();
- mDialog.animate()
- .translationY(0)
- .setDuration(ANIMATION_DURATION_SHOW)
- .setInterpolator(mLinearOutSlowIn)
- .withLayer()
- .start();
- }
- });
+ if (!mWasForceRemoved) {
+ // Dim the background and slide the dialog up
+ mDialog.setTranslationY(mAnimationTranslationOffset);
+ mLayout.setAlpha(0f);
+ postOnAnimation(mShowAnimationRunnable);
+ } else {
+ // Show the dialog immediately
+ mLayout.animate().cancel();
+ mDialog.animate().cancel();
+ mDialog.setAlpha(1.0f);
+ mDialog.setTranslationY(0);
+ mLayout.setAlpha(1.0f);
+ }
+ mWasForceRemoved = false;
}
private void setDismissesDialog(View v) {
@@ -225,10 +237,13 @@ public class FingerprintDialogView extends LinearLayout {
}
public void startDismiss() {
+ mAnimatingAway = true;
+
final Runnable endActionRunnable = new Runnable() {
@Override
public void run() {
mWindowManager.removeView(FingerprintDialogView.this);
+ mAnimatingAway = false;
}
};
@@ -252,6 +267,23 @@ public class FingerprintDialogView extends LinearLayout {
});
}
+ /**
+ * Force remove the window, cancelling any animation that's happening. This should only be
+ * called if we want to quickly show the dialog again (e.g. on rotation). Calling this method
+ * will cause the dialog to show without an animation the next time it's attached.
+ */
+ public void forceRemove() {
+ mLayout.animate().cancel();
+ mDialog.animate().cancel();
+ mWindowManager.removeView(FingerprintDialogView.this);
+ mAnimatingAway = false;
+ mWasForceRemoved = true;
+ }
+
+ public boolean isAnimatingAway() {
+ return mAnimatingAway;
+ }
+
public void setBundle(Bundle bundle) {
mBundle = bundle;
}
@@ -268,10 +300,10 @@ public class FingerprintDialogView extends LinearLayout {
mHandler.removeMessages(FingerprintDialogImpl.MSG_CLEAR_MESSAGE);
updateFingerprintIcon(STATE_FINGERPRINT_ERROR);
mErrorText.setText(message);
- mErrorText.setTextColor(mErrorTextColor);
+ mErrorText.setTextColor(mErrorColor);
mErrorText.setContentDescription(message);
mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_CLEAR_MESSAGE),
- BiometricDialog.HIDE_DIALOG_DELAY);
+ BiometricPrompt.HIDE_DIALOG_DELAY);
}
public void showHelpMessage(String message) {
@@ -281,21 +313,17 @@ public class FingerprintDialogView extends LinearLayout {
public void showErrorMessage(String error) {
showTemporaryMessage(error);
mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG,
- false /* userCanceled */), BiometricDialog.HIDE_DIALOG_DELAY);
+ false /* userCanceled */), BiometricPrompt.HIDE_DIALOG_DELAY);
}
private void updateFingerprintIcon(int newState) {
- Drawable icon = getAnimationResForTransition(mLastState, newState);
+ Drawable icon = getAnimationForTransition(mLastState, newState);
if (icon == null) {
Log.e(TAG, "Animation not found");
return;
}
- if (newState == STATE_FINGERPRINT) {
- icon.setColorFilter(mFingerprintColor, PorterDuff.Mode.SRC_IN);
- }
-
final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable
? (AnimatedVectorDrawable) icon
: null;
@@ -303,7 +331,7 @@ public class FingerprintDialogView extends LinearLayout {
final ImageView fingerprint_icon = mLayout.findViewById(R.id.fingerprint_icon);
fingerprint_icon.setImageDrawable(icon);
- if (animation != null) {
+ if (animation != null && shouldAnimateForTransition(mLastState, newState)) {
animation.forceAnimationOnUI();
animation.start();
}
@@ -311,17 +339,33 @@ public class FingerprintDialogView extends LinearLayout {
mLastState = newState;
}
- private Drawable getAnimationResForTransition(int oldState, int newState) {
+ private boolean shouldAnimateForTransition(int oldState, int newState) {
+ if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
+ return false;
+ } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
+ return true;
+ } else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
+ return true;
+ } else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
+ // TODO(b/77328470): add animation when fingerprint is authenticated
+ return false;
+ }
+ return false;
+ }
+
+ private Drawable getAnimationForTransition(int oldState, int newState) {
int iconRes;
if (oldState == STATE_NONE && newState == STATE_FINGERPRINT) {
- iconRes = R.drawable.lockscreen_fingerprint_draw_on_animation;
+ iconRes = R.drawable.fingerprint_dialog_fp_to_error;
} else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_ERROR) {
- iconRes = R.drawable.lockscreen_fingerprint_fp_to_error_state_animation;
+ iconRes = R.drawable.fingerprint_dialog_fp_to_error;
} else if (oldState == STATE_FINGERPRINT_ERROR && newState == STATE_FINGERPRINT) {
- iconRes = R.drawable.lockscreen_fingerprint_error_state_to_fp_animation;
+ iconRes = R.drawable.fingerprint_dialog_error_to_fp;
} else if (oldState == STATE_FINGERPRINT && newState == STATE_FINGERPRINT_AUTHENTICATED) {
- iconRes = R.drawable.lockscreen_fingerprint_draw_off_animation;
- } else {
+ // TODO(b/77328470): add animation when fingerprint is authenticated
+ iconRes = R.drawable.fingerprint_dialog_error_to_fp;
+ }
+ else {
return null;
}
return mContext.getDrawable(iconRes);
diff --git a/com/android/systemui/globalactions/GlobalActionsDialog.java b/com/android/systemui/globalactions/GlobalActionsDialog.java
index e171b53d..a7975d7e 100644
--- a/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -107,6 +107,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
static public final String SYSTEM_DIALOG_REASON_KEY = "reason";
static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
+ static public final String SYSTEM_DIALOG_REASON_DREAM = "dream";
private static final String TAG = "GlobalActionsDialog";
@@ -376,17 +377,13 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
mAdapter = new MyAdapter();
- OnItemLongClickListener onItemLongClickListener = new OnItemLongClickListener() {
- @Override
- public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
- long id) {
- final Action action = mAdapter.getItem(position);
- if (action instanceof LongPressAction) {
- mDialog.dismiss();
- return ((LongPressAction) action).onLongPress();
- }
- return false;
+ OnItemLongClickListener onItemLongClickListener = (parent, view, position, id) -> {
+ final Action action = mAdapter.getItem(position);
+ if (action instanceof LongPressAction) {
+ mDialog.dismiss();
+ return ((LongPressAction) action).onLongPress();
}
+ return false;
};
ActionsDialog dialog = new ActionsDialog(mContext, this, mAdapter, onItemLongClickListener);
dialog.setCanceledOnTouchOutside(false); // Handled by the custom class.
@@ -1236,7 +1233,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
|| Intent.ACTION_SCREEN_OFF.equals(action)) {
String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
if (!SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) {
- mHandler.sendEmptyMessage(MESSAGE_DISMISS);
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISMISS, reason));
}
} else if (TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED.equals(action)) {
// Airplane mode can be changed after ECM exits if airplane toggle button
@@ -1287,7 +1284,11 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
switch (msg.what) {
case MESSAGE_DISMISS:
if (mDialog != null) {
- mDialog.dismiss();
+ if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) {
+ mDialog.dismissImmediately();
+ } else {
+ mDialog.dismiss();
+ }
mDialog = null;
}
break;
@@ -1468,6 +1469,10 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
.start();
}
+ void dismissImmediately() {
+ super.dismiss();
+ }
+
private float getAnimTranslation() {
return getContext().getResources().getDimension(
com.android.systemui.R.dimen.global_actions_panel_width) / 2;
diff --git a/com/android/systemui/keyboard/KeyboardUI.java b/com/android/systemui/keyboard/KeyboardUI.java
index 94641054..ebd15f55 100644
--- a/com/android/systemui/keyboard/KeyboardUI.java
+++ b/com/android/systemui/keyboard/KeyboardUI.java
@@ -613,7 +613,7 @@ public class KeyboardUI extends SystemUI implements InputManager.OnTabletModeCha
int bluetoothProfile) { }
@Override
- public void onProfileAudioStateChanged(int bluetoothProfile, int state) { }
+ public void onAudioModeChanged() { }
}
private final class BluetoothErrorListener implements Utils.ErrorListener {
diff --git a/com/android/systemui/keyguard/KeyguardViewMediator.java b/com/android/systemui/keyguard/KeyguardViewMediator.java
index d6e59c77..5993c396 100644
--- a/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -145,6 +145,8 @@ public class KeyguardViewMediator extends SystemUI {
private static final String DELAYED_LOCK_PROFILE_ACTION =
"com.android.internal.policy.impl.PhoneWindowManager.DELAYED_LOCK";
+ private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF";
+
// used for handler messages
private static final int SHOW = 1;
private static final int HIDE = 2;
@@ -357,7 +359,12 @@ public class KeyguardViewMediator extends SystemUI {
// ActivityManagerService) will not reconstruct the keyguard if it is already showing.
synchronized (KeyguardViewMediator.this) {
resetKeyguardDonePendingLocked();
- resetStateLocked();
+ if (mLockPatternUtils.isLockScreenDisabled(userId)) {
+ // If we switching to a user that has keyguard disabled, dismiss keyguard.
+ dismiss(null /* callback */, null /* message */);
+ } else {
+ resetStateLocked();
+ }
adjustStatusBarLocked();
}
}
@@ -688,11 +695,15 @@ public class KeyguardViewMediator extends SystemUI {
mShowKeyguardWakeLock.setReferenceCounted(false);
IntentFilter filter = new IntentFilter();
- filter.addAction(DELAYED_KEYGUARD_ACTION);
- filter.addAction(DELAYED_LOCK_PROFILE_ACTION);
filter.addAction(Intent.ACTION_SHUTDOWN);
mContext.registerReceiver(mBroadcastReceiver, filter);
+ final IntentFilter delayedActionFilter = new IntentFilter();
+ delayedActionFilter.addAction(DELAYED_KEYGUARD_ACTION);
+ delayedActionFilter.addAction(DELAYED_LOCK_PROFILE_ACTION);
+ mContext.registerReceiver(mDelayedLockBroadcastReceiver, delayedActionFilter,
+ SYSTEMUI_PERMISSION, null /* scheduler */);
+
mKeyguardDisplayManager = new KeyguardDisplayManager(mContext, mViewMediatorCallback);
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
@@ -1184,6 +1195,10 @@ public class KeyguardViewMediator extends SystemUI {
Trace.endSection();
}
+ public boolean isHiding() {
+ return mHiding;
+ }
+
/**
* Handles SET_OCCLUDED message sent by setOccluded()
*/
@@ -1456,7 +1471,10 @@ public class KeyguardViewMediator extends SystemUI {
}
}
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ /**
+ * This broadcast receiver should be registered with the SystemUI permission.
+ */
+ private final BroadcastReceiver mDelayedLockBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DELAYED_KEYGUARD_ACTION.equals(intent.getAction())) {
@@ -1478,7 +1496,14 @@ public class KeyguardViewMediator extends SystemUI {
}
}
}
- } else if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
+ }
+ }
+ };
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
synchronized (KeyguardViewMediator.this){
mShuttingDown = true;
}
diff --git a/com/android/systemui/pip/phone/PipTouchHandler.java b/com/android/systemui/pip/phone/PipTouchHandler.java
index a0bdcd00..1805f96c 100644
--- a/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -295,6 +295,26 @@ public class PipTouchHandler {
final Rect toAdjustedBounds = mMenuState == MENU_STATE_FULL
? expandedAdjustedBounds
: normalAdjustedBounds;
+ final Rect toMovementBounds = mMenuState == MENU_STATE_FULL
+ ? expandedMovementBounds
+ : normalMovementBounds;
+
+ // If the PIP window needs to shift to right above shelf/IME and it's already above
+ // that, don't move the PIP window.
+ if (toAdjustedBounds.bottom < mMovementBounds.bottom
+ && animatingBounds.top < toAdjustedBounds.bottom) {
+ return;
+ }
+
+ // If the PIP window needs to shift down due to dismissal of shelf/IME but it's way
+ // above the position as if shelf/IME shows, don't move the PIP window.
+ int movementBoundsAdjustment = toMovementBounds.bottom - mMovementBounds.bottom;
+ int offsetAdjustment = fromImeAdjustment ? mImeOffset : mShelfHeight;
+ if (toAdjustedBounds.bottom >= mMovementBounds.bottom
+ && animatingBounds.top
+ < toAdjustedBounds.bottom - movementBoundsAdjustment - offsetAdjustment) {
+ return;
+ }
animateToOffset(animatingBounds, toAdjustedBounds);
}
@@ -320,10 +340,6 @@ public class PipTouchHandler {
private void animateToOffset(Rect animatingBounds, Rect toAdjustedBounds) {
final Rect bounds = new Rect(animatingBounds);
- if (toAdjustedBounds.bottom < mMovementBounds.bottom
- && bounds.top < toAdjustedBounds.bottom) {
- return;
- }
bounds.offset(0, toAdjustedBounds.bottom - bounds.top);
// In landscape mode, PIP window can go offset while launching IME. We want to align the
// the top of the PIP window with the top of the movement bounds in that case.
diff --git a/com/android/systemui/power/PowerNotificationWarnings.java b/com/android/systemui/power/PowerNotificationWarnings.java
index 49b00ce1..40ce69b8 100644
--- a/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/com/android/systemui/power/PowerNotificationWarnings.java
@@ -229,10 +229,11 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
// Bump the notification when the bucket dropped.
.setWhen(mWarningTriggerTimeMs)
.setShowWhen(false)
- .setContentTitle(title)
.setContentText(contentText)
+ .setContentTitle(title)
.setOnlyAlertOnce(true)
.setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
+ .setStyle(new Notification.BigTextStyle().bigText(contentText))
.setVisibility(Notification.VISIBILITY_PUBLIC);
if (hasBatterySettings()) {
nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
@@ -483,16 +484,24 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
d.setMessage(mContext.getString(R.string.auto_saver_enabled_text,
getLowBatteryAutoTriggerDefaultLevel()));
- // Negative == "got it". Just close the dialog. Battery saver has already been enabled.
- d.setNegativeButton(R.string.auto_saver_okay_action, null);
- d.setPositiveButton(R.string.open_saver_setting_action, (dialog, which) ->
- mContext.startActivity(actionBatterySaverSetting));
+ // "Got it". Just close the dialog. Automatic battery has been enabled already.
+ d.setPositiveButton(R.string.auto_saver_okay_action,
+ (dialog, which) -> onAutoSaverEnabledConfirmationClosed());
+
+ // "Settings" -> Opens the battery saver settings activity.
+ d.setNeutralButton(R.string.open_saver_setting_action, (dialog, which) -> {
+ mContext.startActivity(actionBatterySaverSetting);
+ onAutoSaverEnabledConfirmationClosed();
+ });
d.setShowForAllUsers(true);
- d.setOnDismissListener((dialog) -> mSaverEnabledConfirmation = null);
+ d.setOnDismissListener((dialog) -> onAutoSaverEnabledConfirmationClosed());
d.show();
mSaverEnabledConfirmation = d;
}
+ private void onAutoSaverEnabledConfirmationClosed() {
+ mSaverEnabledConfirmation = null;
+ }
private void setSaverMode(boolean mode, boolean needFirstTimeWarning) {
BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning);
@@ -505,7 +514,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
autoTriggerThreshold = 15;
}
- BatterySaverUtils.scheduleAutoBatterySaver(mContext, autoTriggerThreshold);
+ BatterySaverUtils.ensureAutoBatterySaver(mContext, autoTriggerThreshold);
showAutoSaverEnabledConfirmation();
}
diff --git a/com/android/systemui/qs/QSContainerImpl.java b/com/android/systemui/qs/QSContainerImpl.java
index bfbfbf6f..a9455f23 100644
--- a/com/android/systemui/qs/QSContainerImpl.java
+++ b/com/android/systemui/qs/QSContainerImpl.java
@@ -16,19 +16,19 @@
package com.android.systemui.qs;
+import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
+
import android.content.Context;
import android.content.res.Configuration;
-import android.graphics.Canvas;
-import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
-import com.android.settingslib.Utils;
import com.android.systemui.R;
+import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.qs.customize.QSCustomizer;
-import com.android.systemui.statusbar.ExpandableOutlineView;
+import com.android.systemui.statusbar.CommandQueue;
/**
* Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
@@ -44,8 +44,13 @@ public class QSContainerImpl extends FrameLayout {
protected float mQsExpansion;
private QSCustomizer mQSCustomizer;
private View mQSFooter;
+
private View mBackground;
+ private View mBackgroundGradient;
+ private View mStatusBarBackground;
+
private int mSideMargins;
+ private boolean mQsDisabled;
public QSContainerImpl(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -60,6 +65,8 @@ public class QSContainerImpl extends FrameLayout {
mQSCustomizer = findViewById(R.id.qs_customize);
mQSFooter = findViewById(R.id.qs_footer);
mBackground = findViewById(R.id.quick_settings_background);
+ mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
+ mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
mSideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
setClickable(true);
@@ -68,6 +75,23 @@ public class QSContainerImpl extends FrameLayout {
}
@Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ // Hide the backgrounds when in landscape mode.
+ if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ mBackgroundGradient.setVisibility(View.INVISIBLE);
+ mStatusBarBackground.setVisibility(View.INVISIBLE);
+ } else {
+ mBackgroundGradient.setVisibility(View.VISIBLE);
+ mStatusBarBackground.setVisibility(View.VISIBLE);
+ }
+
+ updateResources();
+ mSizePoint.set(0, 0); // Will be retrieved on next measure pass.
+ }
+
+ @Override
public boolean performClick() {
// Want to receive clicks so missing QQS tiles doesn't cause collapse, but
// don't want to do anything with them.
@@ -76,6 +100,16 @@ public class QSContainerImpl extends FrameLayout {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mQsDisabled) {
+ // Only show the status bar contents in QQS header when QS is disabled.
+ mHeader.measure(widthMeasureSpec, heightMeasureSpec);
+ LayoutParams layoutParams = (LayoutParams) mHeader.getLayoutParams();
+ int height = layoutParams.topMargin + layoutParams.bottomMargin
+ + mHeader.getMeasuredHeight();
+ super.onMeasure(
+ widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ return;
+ }
// Since we control our own bottom, be whatever size we want.
// Otherwise the QSPanel ends up with 0 height when the window is only the
// size of the status bar.
@@ -90,9 +124,8 @@ public class QSContainerImpl extends FrameLayout {
// QSCustomizer will always be the height of the screen, but do this after
// other measuring to avoid changing the height of the QS.
- getDisplay().getRealSize(mSizePoint);
mQSCustomizer.measure(widthMeasureSpec,
- MeasureSpec.makeMeasureSpec(mSizePoint.y, MeasureSpec.EXACTLY));
+ MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
}
@Override
@@ -101,6 +134,23 @@ public class QSContainerImpl extends FrameLayout {
updateExpansion();
}
+ public void disable(int state1, int state2, boolean animate) {
+ final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
+ if (disabled == mQsDisabled) return;
+ mQsDisabled = disabled;
+ mBackgroundGradient.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+ mQSPanel.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+ mQSFooter.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+ }
+
+ private void updateResources() {
+ LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams();
+ layoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.quick_qs_offset_height);
+
+ mQSPanel.setLayoutParams(layoutParams);
+ }
+
/**
* Overrides the height of this view (post-layout), so that the content is clipped to that
* height and the background is set to that height.
@@ -147,4 +197,11 @@ public class QSContainerImpl extends FrameLayout {
lp.rightMargin = mSideMargins;
lp.leftMargin = mSideMargins;
}
+
+ private int getDisplayHeight() {
+ if (mSizePoint.y == 0) {
+ getDisplay().getRealSize(mSizePoint);
+ }
+ return mSizePoint.y;
+ }
}
diff --git a/com/android/systemui/qs/QSFooter.java b/com/android/systemui/qs/QSFooter.java
index 3f3cea2e..6c7eda7c 100644
--- a/com/android/systemui/qs/QSFooter.java
+++ b/com/android/systemui/qs/QSFooter.java
@@ -69,4 +69,6 @@ public interface QSFooter {
*/
@Nullable
View getExpandView();
+
+ default void disable(int state1, int state2, boolean animate) {}
}
diff --git a/com/android/systemui/qs/QSFooterImpl.java b/com/android/systemui/qs/QSFooterImpl.java
index 0fa65974..28dd26f4 100644
--- a/com/android/systemui/qs/QSFooterImpl.java
+++ b/com/android/systemui/qs/QSFooterImpl.java
@@ -47,10 +47,8 @@ import com.android.settingslib.graph.SignalDrawable;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.R.dimen;
-import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.TouchAnimator.Builder;
-import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.MultiUserSwitch;
import com.android.systemui.statusbar.phone.SettingsButton;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -62,8 +60,7 @@ import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChange
import com.android.systemui.tuner.TunerService;
public class QSFooterImpl extends FrameLayout implements QSFooter,
- OnClickListener, OnUserInfoChangedListener, EmergencyListener,
- SignalCallback, CommandQueue.Callbacks {
+ OnClickListener, OnUserInfoChangedListener, EmergencyListener, SignalCallback {
private ActivityStarter mActivityStarter;
private UserInfoController mUserInfoController;
@@ -211,16 +208,9 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
}
@Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
- }
-
- @Override
@VisibleForTesting
public void onDetachedFromWindow() {
setListening(false);
- SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
super.onDetachedFromWindow();
}
@@ -393,7 +383,7 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
if (TextUtils.equals(mInfo.typeContentDescription,
mContext.getString(R.string.data_connection_no_internet))
|| TextUtils.equals(mInfo.typeContentDescription,
- mContext.getString(R.string.cell_data_off))) {
+ mContext.getString(R.string.cell_data_off_content_description))) {
contentDescription.append(mInfo.typeContentDescription);
}
mMobileSignal.setContentDescription(contentDescription);
diff --git a/com/android/systemui/qs/QSFragment.java b/com/android/systemui/qs/QSFragment.java
index 018a6356..cb068e3b 100644
--- a/com/android/systemui/qs/QSFragment.java
+++ b/com/android/systemui/qs/QSFragment.java
@@ -14,9 +14,12 @@
package com.android.systemui.qs;
+import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Fragment;
+import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
@@ -35,12 +38,14 @@ import android.widget.FrameLayout.LayoutParams;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.R.id;
+import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
import com.android.systemui.statusbar.stack.StackStateAnimator;
-public class QSFragment extends Fragment implements QS {
+public class QSFragment extends Fragment implements QS, CommandQueue.Callbacks {
private static final String TAG = "QS";
private static final boolean DEBUG = false;
private static final String EXTRA_EXPANDED = "expanded";
@@ -65,6 +70,7 @@ public class QSFragment extends Fragment implements QS {
private int mLayoutDirection;
private QSFooter mFooter;
private float mLastQSExpansion = -1;
+ private boolean mQsDisabled;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -176,6 +182,17 @@ public class QSFragment extends Fragment implements QS {
}
}
+ @Override
+ public void disable(int state1, int state2, boolean animate) {
+ final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
+ if (disabled == mQsDisabled) return;
+ mQsDisabled = disabled;
+ mContainer.disable(state1, state2, animate);
+ mHeader.disable(state1, state2, animate);
+ mFooter.disable(state1, state2, animate);
+ updateQsState();
+ }
+
private void updateQsState() {
final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
|| mHeaderAnimating;
@@ -189,6 +206,9 @@ public class QSFragment extends Fragment implements QS {
mFooter.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
? View.VISIBLE
: View.INVISIBLE);
+ if (mQsDisabled) {
+ mFooter.setVisibility(View.GONE);
+ }
mFooter.setExpanded((mKeyguardShowing && !mHeaderAnimating)
|| (mQsExpanded && !mStackScrollerOverscrolling));
mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
@@ -258,6 +278,12 @@ public class QSFragment extends Fragment implements QS {
mHeader.setListening(listening);
mFooter.setListening(listening);
mQSPanel.setListening(mListening && mQsExpanded);
+ if (listening) {
+ SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+ } else {
+ SysUiServiceProvider.getComponent(getContext(), CommandQueue.class)
+ .removeCallbacks(this);
+ }
}
@Override
diff --git a/com/android/systemui/qs/QuickStatusBarHeader.java b/com/android/systemui/qs/QuickStatusBarHeader.java
index df65d1fb..e2af90d6 100644
--- a/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -26,6 +26,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
import android.media.AudioManager;
@@ -54,8 +55,10 @@ import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
+import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.DarkIconDispatcher;
import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.statusbar.policy.DateView;
import com.android.systemui.statusbar.policy.NextAlarmController;
import java.util.Locale;
@@ -65,7 +68,7 @@ import java.util.Locale;
* battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner
* contents.
*/
-public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue.Callbacks,
+public class QuickStatusBarHeader extends RelativeLayout implements
View.OnClickListener, NextAlarmController.NextAlarmChangeCallback {
private static final String TAG = "QuickStatusBarHeader";
private static final boolean DEBUG = false;
@@ -90,6 +93,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
private TouchAnimator mStatusIconsAlphaAnimator;
private TouchAnimator mHeaderTextContainerAlphaAnimator;
+ private View mSystemIconsView;
private View mQuickQsStatusIcons;
private View mDate;
private View mHeaderTextContainerView;
@@ -107,6 +111,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
private View mStatusSeparator;
private ImageView mRingerModeIcon;
private TextView mRingerModeTextView;
+ private BatteryMeterView mBatteryMeterView;
+ private Clock mClockView;
+ private DateView mDateView;
private NextAlarmController mAlarmController;
/** Counts how many times the long press tooltip has been shown to the user. */
@@ -138,6 +145,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
mDate = findViewById(R.id.date);
mDate.setOnClickListener(this);
+ mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
mIconManager = new TintedIconManager(findViewById(R.id.statusIcons));
@@ -164,19 +172,21 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
// Set the correct tint for the status icons so they contrast
mIconManager.setTint(fillColor);
- BatteryMeterView battery = findViewById(R.id.battery);
- battery.setForceShowPercent(true);
+ mBatteryMeterView = findViewById(R.id.battery);
+ mBatteryMeterView.setForceShowPercent(true);
+ mClockView = findViewById(R.id.clock);
+ mDateView = findViewById(R.id.date);
}
private void updateStatusText() {
boolean ringerVisible = false;
if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_vibrate);
- mRingerModeTextView.setText(R.string.volume_ringer_status_vibrate);
+ mRingerModeTextView.setText(R.string.qs_status_phone_vibrate);
ringerVisible = true;
} else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) {
mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_silent);
- mRingerModeTextView.setText(R.string.volume_ringer_status_silent);
+ mRingerModeTextView.setText(R.string.qs_status_phone_muted);
ringerVisible = true;
}
mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE);
@@ -212,6 +222,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateResources();
+
+ // Update color schemes in landscape to use wallpaperTextColor
+ boolean shouldUseWallpaperTextColor =
+ newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
+ mBatteryMeterView.useWallpaperTextColor(shouldUseWallpaperTextColor);
+ mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor);
+ mDateView.useWallpaperTextColor(shouldUseWallpaperTextColor);
}
@Override
@@ -221,11 +238,22 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
}
private void updateResources() {
- // Update height, especially due to landscape mode restricting space.
+ Resources resources = mContext.getResources();
+
+ // Update height for a few views, especially due to landscape mode restricting space.
mHeaderTextContainerView.getLayoutParams().height =
- mContext.getResources().getDimensionPixelSize(R.dimen.qs_header_tooltip_height);
+ resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height);
mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams());
+ mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.quick_qs_offset_height);
+ mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams());
+
+ getLayoutParams().height = resources.getDimensionPixelSize(mQsDisabled
+ ? com.android.internal.R.dimen.quick_qs_offset_height
+ : com.android.internal.R.dimen.quick_qs_total_height);
+ setLayoutParams(getLayoutParams());
+
updateStatusIconAlphaAnimator();
updateHeaderTextContainerAlphaAnimator();
}
@@ -293,20 +321,18 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
TOOLTIP_NOT_YET_SHOWN_COUNT);
}
- @Override
public void disable(int state1, int state2, boolean animate) {
final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
if (disabled == mQsDisabled) return;
mQsDisabled = disabled;
mHeaderQsPanel.setDisabledByPolicy(disabled);
- final int rawHeight = (int) getResources().getDimension(
- com.android.internal.R.dimen.quick_qs_total_height);
- getLayoutParams().height = disabled ? (rawHeight - mHeaderQsPanel.getHeight()) : rawHeight;
+ mHeaderTextContainerView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+ mQuickQsStatusIcons.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
+ updateResources();
}
@Override
public void onAttachedToWindow() {
- SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
requestApplyInsets();
}
@@ -327,7 +353,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
@VisibleForTesting
public void onDetachedFromWindow() {
setListening(false);
- SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
super.onDetachedFromWindow();
}
@@ -497,9 +522,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
mHeaderQsPanel.setHost(host, null /* No customization in header */);
// Use SystemUI context to get battery meter colors, and let it use the default tint (white)
- BatteryMeterView battery = findViewById(R.id.battery);
- battery.setColorsFromContext(mHost.getContext());
- battery.onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
+ mBatteryMeterView.setColorsFromContext(mHost.getContext());
+ mBatteryMeterView.onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
}
public void setCallback(Callback qsPanelCallback) {
diff --git a/com/android/systemui/qs/car/CarQSFooter.java b/com/android/systemui/qs/car/CarQSFooter.java
index 23d3ebbb..24b5a340 100644
--- a/com/android/systemui/qs/car/CarQSFooter.java
+++ b/com/android/systemui/qs/car/CarQSFooter.java
@@ -29,7 +29,6 @@ import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSFooter;
import com.android.systemui.qs.QSPanel;
-import com.android.systemui.statusbar.car.UserGridView;
import com.android.systemui.statusbar.phone.MultiUserSwitch;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.UserInfoController;
diff --git a/com/android/systemui/qs/car/CarQSFragment.java b/com/android/systemui/qs/car/CarQSFragment.java
index 0ee6d1fb..da21aa50 100644
--- a/com/android/systemui/qs/car/CarQSFragment.java
+++ b/com/android/systemui/qs/car/CarQSFragment.java
@@ -20,21 +20,20 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Fragment;
+import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.GridLayoutManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.QSFooter;
-import com.android.systemui.statusbar.car.PageIndicator;
-import com.android.systemui.statusbar.car.UserGridView;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.car.UserGridRecyclerView;
import java.util.ArrayList;
import java.util.List;
@@ -45,14 +44,12 @@ import java.util.List;
* status bar, and a static row with access to the user switcher and settings.
*/
public class CarQSFragment extends Fragment implements QS {
- private ViewGroup mPanel;
private View mHeader;
private View mUserSwitcherContainer;
private CarQSFooter mFooter;
private View mFooterUserName;
private View mFooterExpandIcon;
- private UserGridView mUserGridView;
- private PageIndicator mPageIndicator;
+ private UserGridRecyclerView mUserGridView;
private AnimatorSet mAnimatorSet;
private UserSwitchCallback mUserSwitchCallback;
@@ -65,7 +62,6 @@ public class CarQSFragment extends Fragment implements QS {
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- mPanel = (ViewGroup) view;
mHeader = view.findViewById(R.id.header);
mFooter = view.findViewById(R.id.qs_footer);
mFooterUserName = mFooter.findViewById(R.id.user_name);
@@ -75,16 +71,15 @@ public class CarQSFragment extends Fragment implements QS {
updateUserSwitcherHeight(0);
- mUserGridView = view.findViewById(R.id.user_grid);
- mUserGridView.init(null, Dependency.get(UserSwitcherController.class),
- false /* overrideAlpha */);
-
- mPageIndicator = view.findViewById(R.id.user_switcher_page_indicator);
- mPageIndicator.setupWithViewPager(mUserGridView);
+ Context context = getContext();
+ mUserGridView = mUserSwitcherContainer.findViewById(R.id.user_grid);
+ GridLayoutManager layoutManager = new GridLayoutManager(context,
+ context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col));
+ mUserGridView.setLayoutManager(layoutManager);
+ mUserGridView.buildAdapter();
mUserSwitchCallback = new UserSwitchCallback();
mFooter.setUserSwitchCallback(mUserSwitchCallback);
- mUserGridView.setUserSwitchCallback(mUserSwitchCallback);
}
@Override
@@ -111,13 +106,11 @@ public class CarQSFragment extends Fragment implements QS {
@Override
public void setHeaderListening(boolean listening) {
mFooter.setListening(listening);
- mUserGridView.setListening(listening);
}
@Override
public void setListening(boolean listening) {
mFooter.setListening(listening);
- mUserGridView.setListening(listening);
}
@Override
@@ -219,24 +212,6 @@ public class CarQSFragment extends Fragment implements QS {
mShowing = false;
animateHeightChange(false /* opening */);
}
-
- public void resetShowing() {
- if (mShowing) {
- for (int i = 0; i < mUserGridView.getChildCount(); i++) {
- ViewGroup podContainer = (ViewGroup) mUserGridView.getChildAt(i);
- // Need to bring the last child to the front to maintain the order in the pod
- // container. Why? ¯\_(ツ)_/¯
- if (podContainer.getChildCount() > 0) {
- podContainer.getChildAt(podContainer.getChildCount() - 1).bringToFront();
- }
- // The alpha values are default to 0, so if the pods have been refreshed, they
- // need to be set to 1 when showing.
- for (int j = 0; j < podContainer.getChildCount(); j++) {
- podContainer.getChildAt(j).setAlpha(1f);
- }
- }
- }
- }
}
private void updateUserSwitcherHeight(int height) {
@@ -260,27 +235,6 @@ public class CarQSFragment extends Fragment implements QS {
});
allAnimators.add(heightAnimator);
- // The user grid contains pod containers that each contain a number of pods. Animate
- // all pods to avoid any discrepancy/race conditions with possible changes during the
- // animation.
- int cascadeDelay = getResources().getInteger(
- R.integer.car_user_switcher_anim_cascade_delay_ms);
- for (int i = 0; i < mUserGridView.getChildCount(); i++) {
- ViewGroup podContainer = (ViewGroup) mUserGridView.getChildAt(i);
- for (int j = 0; j < podContainer.getChildCount(); j++) {
- View pod = podContainer.getChildAt(j);
- Animator podAnimator = AnimatorInflater.loadAnimator(getContext(),
- opening ? R.anim.car_user_switcher_open_pod_animation
- : R.anim.car_user_switcher_close_pod_animation);
- // Add the cascading delay between pods
- if (opening) {
- podAnimator.setStartDelay(podAnimator.getStartDelay() + j * cascadeDelay);
- }
- podAnimator.setTarget(pod);
- allAnimators.add(podAnimator);
- }
- }
-
Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(),
opening ? R.anim.car_user_switcher_open_name_animation
: R.anim.car_user_switcher_close_name_animation);
@@ -293,12 +247,6 @@ public class CarQSFragment extends Fragment implements QS {
iconAnimator.setTarget(mFooterExpandIcon);
allAnimators.add(iconAnimator);
- Animator pageAnimator = AnimatorInflater.loadAnimator(getContext(),
- opening ? R.anim.car_user_switcher_open_pages_animation
- : R.anim.car_user_switcher_close_pages_animation);
- pageAnimator.setTarget(mPageIndicator);
- allAnimators.add(pageAnimator);
-
mAnimatorSet = new AnimatorSet();
mAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
diff --git a/com/android/systemui/qs/customize/CustomizeTileView.java b/com/android/systemui/qs/customize/CustomizeTileView.java
index eb95866a..20e3cee6 100644
--- a/com/android/systemui/qs/customize/CustomizeTileView.java
+++ b/com/android/systemui/qs/customize/CustomizeTileView.java
@@ -44,4 +44,9 @@ public class CustomizeTileView extends QSTileView {
public TextView getAppLabel() {
return mSecondLine;
}
+
+ @Override
+ protected boolean animationsEnabled() {
+ return false;
+ }
}
diff --git a/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index 0f83078e..e7e756f8 100644
--- a/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -16,6 +16,8 @@ package com.android.systemui.qs.tileimpl;
import static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -127,7 +129,6 @@ public class QSIconViewImpl extends QSIconView {
}
protected void setIcon(ImageView iv, QSTile.State state) {
- updateIcon(iv, state);
if (state.disabledByPolicy) {
iv.setColorFilter(getContext().getColor(R.color.qs_tile_disabled_color));
} else {
@@ -137,7 +138,7 @@ public class QSIconViewImpl extends QSIconView {
int color = getColor(state.state);
mState = state.state;
if (iv.isShown() && mTint != 0) {
- animateGrayScale(mTint, color, iv);
+ animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state));
mTint = color;
} else {
if (iv instanceof AlphaControlledSlashImageView) {
@@ -147,7 +148,10 @@ public class QSIconViewImpl extends QSIconView {
setTint(iv, color);
}
mTint = color;
+ updateIcon(iv, state);
}
+ } else {
+ updateIcon(iv, state);
}
}
@@ -155,12 +159,13 @@ public class QSIconViewImpl extends QSIconView {
return getColorForState(getContext(), state);
}
- public static void animateGrayScale(int fromColor, int toColor, ImageView iv) {
+ private void animateGrayScale(int fromColor, int toColor, ImageView iv,
+ final Runnable endRunnable) {
if (iv instanceof AlphaControlledSlashImageView) {
((AlphaControlledSlashImageView)iv)
.setFinalImageTintList(ColorStateList.valueOf(toColor));
}
- if (ValueAnimator.areAnimatorsEnabled()) {
+ if (mAnimationEnabled && ValueAnimator.areAnimatorsEnabled()) {
final float fromAlpha = Color.alpha(fromColor);
final float toAlpha = Color.alpha(toColor);
final float fromChannel = Color.red(fromColor);
@@ -175,10 +180,16 @@ public class QSIconViewImpl extends QSIconView {
setTint(iv, Color.argb(alpha, channel, channel, channel));
});
-
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ endRunnable.run();
+ }
+ });
anim.start();
} else {
setTint(iv, toColor);
+ endRunnable.run();
}
}
diff --git a/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/com/android/systemui/qs/tileimpl/QSTileBaseView.java
index 09d928fd..cc60f874 100644
--- a/com/android/systemui/qs/tileimpl/QSTileBaseView.java
+++ b/com/android/systemui/qs/tileimpl/QSTileBaseView.java
@@ -179,7 +179,7 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
protected void handleStateChanged(QSTile.State state) {
int circleColor = getCircleColor(state.state);
if (circleColor != mCircleColor) {
- if (mBg.isShown()) {
+ if (mBg.isShown() && animationsEnabled()) {
ValueAnimator animator = ValueAnimator.ofArgb(mCircleColor, circleColor)
.setDuration(QS_ANIM_LENGTH);
animator.addUpdateListener(animation -> mBg.setImageTintList(ColorStateList.valueOf(
@@ -205,6 +205,10 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
}
}
+ protected boolean animationsEnabled() {
+ return true;
+ }
+
private int getCircleColor(int state) {
switch (state) {
case Tile.STATE_ACTIVE:
diff --git a/com/android/systemui/qs/tiles/CellularTile.java b/com/android/systemui/qs/tiles/CellularTile.java
index 2abe9d92..d6182c43 100644
--- a/com/android/systemui/qs/tiles/CellularTile.java
+++ b/com/android/systemui/qs/tiles/CellularTile.java
@@ -108,20 +108,21 @@ public class CellularTile extends QSTileImpl<SignalState> {
}
if (mDataController.isMobileDataEnabled()) {
if (mKeyguardMonitor.isSecure() && !mKeyguardMonitor.canSkipBouncer()) {
- mActivityStarter.postQSRunnableDismissingKeyguard(this::showDisableDialog);
+ mActivityStarter.postQSRunnableDismissingKeyguard(this::maybeShowDisableDialog);
} else {
- if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) {
- mDataController.setMobileDataEnabled(false);
- } else {
- mUiHandler.post(this::showDisableDialog);
- }
+ mUiHandler.post(this::maybeShowDisableDialog);
}
} else {
mDataController.setMobileDataEnabled(true);
}
}
- private void showDisableDialog() {
+ private void maybeShowDisableDialog() {
+ if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) {
+ // Directly turn off mobile data if the user has seen the dialog before.
+ mDataController.setMobileDataEnabled(false);
+ return;
+ }
mHost.collapsePanels();
String carrierName = mController.getMobileDataNetworkName();
if (TextUtils.isEmpty(carrierName)) {
@@ -194,7 +195,18 @@ public class CellularTile extends QSTileImpl<SignalState> {
state.state = Tile.STATE_INACTIVE;
state.secondaryLabel = r.getString(R.string.cell_data_off);
}
- state.contentDescription = state.label + ", " + state.secondaryLabel;
+
+
+ // TODO(b/77881974): Instead of switching out the description via a string check for
+ // we need to have two strings provided by the MobileIconGroup.
+ final CharSequence contentDescriptionSuffix;
+ if (state.state == Tile.STATE_INACTIVE) {
+ contentDescriptionSuffix = r.getString(R.string.cell_data_off_content_description);
+ } else {
+ contentDescriptionSuffix = state.secondaryLabel;
+ }
+
+ state.contentDescription = state.label + ", " + contentDescriptionSuffix;
}
private CharSequence getMobileDataDescription(CallbackInfo cb) {
diff --git a/com/android/systemui/qs/tiles/DndTile.java b/com/android/systemui/qs/tiles/DndTile.java
index 7dcf5c0c..16c2a75f 100644
--- a/com/android/systemui/qs/tiles/DndTile.java
+++ b/com/android/systemui/qs/tiles/DndTile.java
@@ -143,26 +143,41 @@ public class DndTile extends QSTileImpl<BooleanState> {
public void showDetail(boolean show) {
int zenDuration = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ZEN_DURATION, 0);
- switch (zenDuration) {
- case Settings.Global.ZEN_DURATION_PROMPT:
- mUiHandler.post(() -> {
- Dialog mDialog = new EnableZenModeDialog(mContext).createDialog();
- mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
- SystemUIDialog.setShowForAllUsers(mDialog, true);
- SystemUIDialog.registerDismissListener(mDialog);
- SystemUIDialog.setWindowOnTop(mDialog);
- mUiHandler.post(() -> mDialog.show());
- mHost.collapsePanels();
- });
- break;
- case Settings.Global.ZEN_DURATION_FOREVER:
- mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
- break;
- default:
- Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,
- ActivityManager.getCurrentUser(), true).id;
- mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
- conditionId, TAG);
+ boolean showOnboarding = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0;
+ if (showOnboarding) {
+ // don't show on-boarding again or notification ever
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Global.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
+ // turn on DND
+ mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
+ // show on-boarding screen
+ Intent intent = new Intent(Settings.ZEN_MODE_ONBOARDING);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(intent, 0);
+ } else {
+ switch (zenDuration) {
+ case Settings.Global.ZEN_DURATION_PROMPT:
+ mUiHandler.post(() -> {
+ Dialog mDialog = new EnableZenModeDialog(mContext).createDialog();
+ mDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+ SystemUIDialog.setShowForAllUsers(mDialog, true);
+ SystemUIDialog.registerDismissListener(mDialog);
+ SystemUIDialog.setWindowOnTop(mDialog);
+ mUiHandler.post(() -> mDialog.show());
+ mHost.collapsePanels();
+ });
+ break;
+ case Settings.Global.ZEN_DURATION_FOREVER:
+ mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
+ break;
+ default:
+ Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,
+ ActivityManager.getCurrentUser(), true).id;
+ mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ conditionId, TAG);
+ }
}
}
@@ -209,7 +224,7 @@ public class DndTile extends QSTileImpl<BooleanState> {
state.slash.isSlashed = !state.value;
state.label = getTileLabel();
state.secondaryLabel = ZenModeConfig.getDescription(mContext,zen != Global.ZEN_MODE_OFF,
- mController.getConfig());
+ mController.getConfig(), false);
state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on);
checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_ADJUST_VOLUME);
switch (zen) {
diff --git a/com/android/systemui/qs/tiles/WifiTile.java b/com/android/systemui/qs/tiles/WifiTile.java
index 8a1e4dad..d8f7b71d 100644
--- a/com/android/systemui/qs/tiles/WifiTile.java
+++ b/com/android/systemui/qs/tiles/WifiTile.java
@@ -41,6 +41,7 @@ import com.android.systemui.qs.AlphaControlledSignalTileView;
import com.android.systemui.qs.QSDetailItems;
import com.android.systemui.qs.QSDetailItems.Item;
import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.tileimpl.QSIconViewImpl;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.NetworkController.AccessPointController;
@@ -60,6 +61,7 @@ public class WifiTile extends QSTileImpl<SignalState> {
protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback();
private final ActivityStarter mActivityStarter;
+ private boolean mExpectDisabled;
public WifiTile(QSHost host) {
super(host);
@@ -120,6 +122,15 @@ public class WifiTile extends QSTileImpl<SignalState> {
// Immediately enter transient state when turning on wifi.
refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
mController.setWifiEnabled(!wifiEnabled);
+ mExpectDisabled = wifiEnabled;
+ if (mExpectDisabled) {
+ mHandler.postDelayed(() -> {
+ if (mExpectDisabled) {
+ mExpectDisabled = false;
+ refreshState();
+ }
+ }, QSIconViewImpl.QS_ANIM_LENGTH);
+ }
}
@Override
@@ -143,11 +154,13 @@ public class WifiTile extends QSTileImpl<SignalState> {
@Override
protected void handleUpdateState(SignalState state, Object arg) {
if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg);
- final CallbackInfo cb;
- if (arg != null && arg instanceof CallbackInfo) {
- cb = (CallbackInfo) arg;
- } else {
- cb = mSignalCallback.mInfo;
+ final CallbackInfo cb = mSignalCallback.mInfo;
+ if (mExpectDisabled) {
+ if (cb.enabled) {
+ return; // Ignore updates until disabled event occurs.
+ } else {
+ mExpectDisabled = false;
+ }
}
boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) && (cb.ssid != null);
@@ -288,7 +301,7 @@ public class WifiTile extends QSTileImpl<SignalState> {
if (isShowingDetail()) {
mDetailAdapter.updateItems();
}
- refreshState(mInfo);
+ refreshState();
}
}
diff --git a/com/android/systemui/recents/Recents.java b/com/android/systemui/recents/Recents.java
index 0f85c5b3..8bb3c023 100644
--- a/com/android/systemui/recents/Recents.java
+++ b/com/android/systemui/recents/Recents.java
@@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS;
import android.app.ActivityManager;
+import android.app.trust.TrustManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
@@ -51,6 +52,7 @@ import com.android.systemui.EventLogTags;
import com.android.systemui.OverviewProxyService;
import com.android.systemui.R;
import com.android.systemui.RecentsComponent;
+import com.android.systemui.SystemUIApplication;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.SystemUI;
import com.android.systemui.recents.events.EventBus;
@@ -70,6 +72,7 @@ import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.StatusBar;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -107,6 +110,7 @@ public class Recents extends SystemUI
private Handler mHandler;
private RecentsImpl mImpl;
+ private TrustManager mTrustManager;
private int mDraggingInRecentsCurrentUser;
// Only For system user, this is the callbacks instance we return to each secondary user
@@ -235,6 +239,8 @@ public class Recents extends SystemUI
registerWithSystemUser();
}
putComponent(Recents.class, this);
+
+ mTrustManager = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
}
@Override
@@ -342,12 +348,28 @@ public class Recents extends SystemUI
// If connected to launcher service, let it handle the toggle logic
IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
if (overviewProxy != null) {
- try {
- overviewProxy.onOverviewToggle();
- return;
- } catch (RemoteException e) {
- Log.e(TAG, "Cannot send toggle recents through proxy service.", e);
+ final Runnable toggleRecents = () -> {
+ try {
+ if (mOverviewProxyService.getProxy() != null) {
+ mOverviewProxyService.getProxy().onOverviewToggle();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot send toggle recents through proxy service.", e);
+ }
+ };
+ // Preload only if device for current user is unlocked
+ final StatusBar statusBar = getComponent(StatusBar.class);
+ if (statusBar != null && statusBar.isKeyguardShowing()) {
+ statusBar.executeRunnableDismissingKeyguard(() -> {
+ // Flush trustmanager before checking device locked per user
+ mTrustManager.reportKeyguardShowingChanged();
+ mHandler.post(toggleRecents);
+ }, null, true /* dismissShade */, false /* afterKeyguardGone */,
+ true /* deferred */);
+ } else {
+ toggleRecents.run();
}
+ return;
}
int growTarget = getComponent(Divider.class).getView().growsRecents();
diff --git a/com/android/systemui/recents/RecentsImpl.java b/com/android/systemui/recents/RecentsImpl.java
index 19da3dba..6fcb1c11 100644
--- a/com/android/systemui/recents/RecentsImpl.java
+++ b/com/android/systemui/recents/RecentsImpl.java
@@ -126,7 +126,7 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
@Override
public void onTaskStackChangedBackground() {
// Skip background preloading recents in SystemUI if the overview services is bound
- if (Dependency.get(OverviewProxyService.class).getProxy() != null) {
+ if (Dependency.get(OverviewProxyService.class).isEnabled()) {
return;
}
@@ -300,7 +300,7 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
public void onBootCompleted() {
// Skip preloading tasks if we are already bound to the service
- if (Dependency.get(OverviewProxyService.class).getProxy() != null) {
+ if (Dependency.get(OverviewProxyService.class).isEnabled()) {
return;
}
diff --git a/com/android/systemui/recents/RecentsOnboarding.java b/com/android/systemui/recents/RecentsOnboarding.java
index 75bc9558..0d8aed4a 100644
--- a/com/android/systemui/recents/RecentsOnboarding.java
+++ b/com/android/systemui/recents/RecentsOnboarding.java
@@ -21,19 +21,19 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import android.annotation.TargetApi;
import android.app.ActivityManager;
-import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.RippleDrawable;
+import android.graphics.drawable.ShapeDrawable;
import android.os.Build;
import android.os.SystemProperties;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
+import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -62,6 +62,7 @@ public class RecentsOnboarding {
private static final String TAG = "RecentsOnboarding";
private static final boolean RESET_PREFS_FOR_DEBUG = false;
+ private static final boolean ONBOARDING_ENABLED = false;
private static final long SHOW_DELAY_MS = 500;
private static final long SHOW_HIDE_DURATION_MS = 300;
// Don't show the onboarding until the user has launched this number of apps.
@@ -76,17 +77,13 @@ public class RecentsOnboarding {
private final View mLayout;
private final TextView mTextView;
private final ImageView mDismissView;
- private final ColorDrawable mBackgroundDrawable;
- private final int mDarkBackgroundColor;
- private final int mLightBackgroundColor;
- private final int mDarkContentColor;
- private final int mLightContentColor;
- private final RippleDrawable mDarkRipple;
- private final RippleDrawable mLightRipple;
+ private final View mArrowView;
+ private final int mOnboardingToastColor;
+ private final int mOnboardingToastArrowRadius;
+ private int mNavBarHeight;
private boolean mTaskListenerRegistered;
private boolean mLayoutAttachedToWindow;
- private boolean mBackgroundIsLight;
private int mLastTaskId;
private boolean mHasDismissed;
private int mNumAppsLaunchedSinceDismiss;
@@ -159,24 +156,30 @@ public class RecentsOnboarding {
mLayout = LayoutInflater.from(mContext).inflate(R.layout.recents_onboarding, null);
mTextView = mLayout.findViewById(R.id.onboarding_text);
mDismissView = mLayout.findViewById(R.id.dismiss);
- mDarkBackgroundColor = res.getColor(android.R.color.background_dark);
- mLightBackgroundColor = res.getColor(android.R.color.background_light);
- mDarkContentColor = res.getColor(R.color.primary_text_default_material_light);
- mLightContentColor = res.getColor(R.color.primary_text_default_material_dark);
- mDarkRipple = new RippleDrawable(res.getColorStateList(R.color.ripple_material_light),
- null, null);
- mLightRipple = new RippleDrawable(res.getColorStateList(R.color.ripple_material_dark),
- null, null);
- mBackgroundDrawable = new ColorDrawable(mDarkBackgroundColor);
+ mArrowView = mLayout.findViewById(R.id.arrow);
+
+ TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+ mOnboardingToastColor = res.getColor(typedValue.resourceId);
+ mOnboardingToastArrowRadius = res.getDimensionPixelSize(
+ R.dimen.recents_onboarding_toast_arrow_corner_radius);
mLayout.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
- mLayout.setBackground(mBackgroundDrawable);
mDismissView.setOnClickListener(v -> {
hide(true);
mHasDismissed = true;
mNumAppsLaunchedSinceDismiss = 0;
});
+ ViewGroup.LayoutParams arrowLp = mArrowView.getLayoutParams();
+ ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+ arrowLp.width, arrowLp.height, false));
+ Paint arrowPaint = arrowDrawable.getPaint();
+ arrowPaint.setColor(mOnboardingToastColor);
+ // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+ arrowPaint.setPathEffect(new CornerPathEffect(mOnboardingToastArrowRadius));
+ mArrowView.setBackground(arrowDrawable);
+
if (RESET_PREFS_FOR_DEBUG) {
Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false);
Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0);
@@ -184,6 +187,9 @@ public class RecentsOnboarding {
}
public void onConnectedToLauncher() {
+ if (!ONBOARDING_ENABLED) {
+ return;
+ }
boolean alreadySeenRecentsOnboarding = Prefs.getBoolean(mContext,
Prefs.Key.HAS_SEEN_RECENTS_ONBOARDING, false);
if (!mTaskListenerRegistered && !alreadySeenRecentsOnboarding) {
@@ -231,6 +237,7 @@ public class RecentsOnboarding {
int orientation = mContext.getResources().getConfiguration().orientation;
if (!mLayoutAttachedToWindow && orientation == Configuration.ORIENTATION_PORTRAIT) {
mLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+
mWindowManager.addView(mLayout, getWindowLayoutParams());
int layoutHeight = mLayout.getHeight();
if (layoutHeight == 0) {
@@ -278,29 +285,18 @@ public class RecentsOnboarding {
}
}
- public void setContentDarkIntensity(float contentDarkIntensity) {
- boolean backgroundIsLight = contentDarkIntensity > 0.5f;
- if (backgroundIsLight != mBackgroundIsLight) {
- mBackgroundIsLight = backgroundIsLight;
- mBackgroundDrawable.setColor(mBackgroundIsLight
- ? mLightBackgroundColor : mDarkBackgroundColor);
- int contentColor = mBackgroundIsLight ? mDarkContentColor : mLightContentColor;
- mTextView.setTextColor(contentColor);
- mTextView.getCompoundDrawables()[3].setColorFilter(contentColor,
- PorterDuff.Mode.SRC_IN);
- mDismissView.setColorFilter(contentColor);
- mDismissView.setBackground(mBackgroundIsLight ? mDarkRipple : mLightRipple);
- }
+ public void setNavBarHeight(int navBarHeight) {
+ mNavBarHeight = navBarHeight;
}
private WindowManager.LayoutParams getWindowLayoutParams() {
- int flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
+ int flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
- WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG,
+ 0, -mNavBarHeight / 2,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
flags,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
diff --git a/com/android/systemui/recents/ScreenPinningRequest.java b/com/android/systemui/recents/ScreenPinningRequest.java
index 3dd6e353..bfbba7c1 100644
--- a/com/android/systemui/recents/ScreenPinningRequest.java
+++ b/com/android/systemui/recents/ScreenPinningRequest.java
@@ -107,7 +107,7 @@ public class ScreenPinningRequest implements View.OnClickListener {
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
diff --git a/com/android/systemui/recents/TriangleShape.java b/com/android/systemui/recents/TriangleShape.java
new file mode 100644
index 00000000..de85c0f9
--- /dev/null
+++ b/com/android/systemui/recents/TriangleShape.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.systemui.recents;
+
+import android.graphics.Outline;
+import android.graphics.Path;
+import android.graphics.drawable.shapes.PathShape;
+import android.support.annotation.NonNull;
+
+/**
+ * Wrapper around {@link android.graphics.drawable.shapes.PathShape}
+ * that creates a shape with a triangular path (pointing up or down).
+ */
+public class TriangleShape extends PathShape {
+ private Path mTriangularPath;
+
+ public TriangleShape(Path path, float stdWidth, float stdHeight) {
+ super(path, stdWidth, stdHeight);
+ mTriangularPath = path;
+ }
+
+ public static TriangleShape create(float width, float height, boolean isPointingUp) {
+ Path triangularPath = new Path();
+ if (isPointingUp) {
+ triangularPath.moveTo(0, height);
+ triangularPath.lineTo(width, height);
+ triangularPath.lineTo(width / 2, 0);
+ triangularPath.close();
+ } else {
+ triangularPath.moveTo(0, 0);
+ triangularPath.lineTo(width / 2, height);
+ triangularPath.lineTo(width, 0);
+ triangularPath.close();
+ }
+ return new TriangleShape(triangularPath, width, height);
+ }
+
+ @Override
+ public void getOutline(@NonNull Outline outline) {
+ outline.setConvexPath(mTriangularPath);
+ }
+}
diff --git a/com/android/systemui/shared/recents/model/IconLoader.java b/com/android/systemui/shared/recents/model/IconLoader.java
index 20d14188..78b1b261 100644
--- a/com/android/systemui/shared/recents/model/IconLoader.java
+++ b/com/android/systemui/shared/recents/model/IconLoader.java
@@ -15,10 +15,13 @@
*/
package com.android.systemui.shared.recents.model;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
+
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -108,10 +111,12 @@ public abstract class IconLoader {
}
if (desc.getIconResource() != 0) {
try {
- Context packageContext = mContext.createPackageContextAsUser(
- taskKey.getPackageName(), 0, UserHandle.of(userId));
- return createBadgedDrawable(packageContext.getDrawable(desc.getIconResource()),
- userId, desc);
+ PackageManager pm = mContext.getPackageManager();
+ ApplicationInfo appInfo = pm.getApplicationInfo(taskKey.getPackageName(),
+ MATCH_ANY_USER);
+ Resources res = pm.getResourcesForApplication(appInfo);
+ return createBadgedDrawable(res.getDrawable(desc.getIconResource(), null), userId,
+ desc);
} catch (Resources.NotFoundException|PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not find icon drawable from resource", e);
}
diff --git a/com/android/systemui/shared/system/ActivityManagerWrapper.java b/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 1aad27f9..ca5b0347 100644
--- a/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -44,8 +44,10 @@ import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
@@ -53,6 +55,8 @@ import android.view.IRecentsAnimationController;
import android.view.IRecentsAnimationRunner;
import android.view.RemoteAnimationTarget;
+
+import com.android.internal.app.IVoiceInteractionManagerService;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -67,6 +71,9 @@ public class ActivityManagerWrapper {
private static final ActivityManagerWrapper sInstance = new ActivityManagerWrapper();
+ // Should match the values in PhoneWindowManager
+ public static final String CLOSE_SYSTEM_WINDOWS_REASON_RECENTS = "recentapps";
+
private final PackageManager mPackageManager;
private final BackgroundExecutor mBackgroundExecutor;
private final TaskStackChangeListeners mTaskStackChangeListeners;
@@ -258,9 +265,9 @@ public class ActivityManagerWrapper {
/**
* Cancels the remote recents animation started from {@link #startRecentsActivity}.
*/
- public void cancelRecentsAnimation() {
+ public void cancelRecentsAnimation(boolean restoreHomeStackPosition) {
try {
- ActivityManager.getService().cancelRecentsAnimation();
+ ActivityManager.getService().cancelRecentsAnimation(restoreHomeStackPosition);
} catch (RemoteException e) {
Log.e(TAG, "Failed to cancel recents animation", e);
}
@@ -432,4 +439,21 @@ public class ActivityManagerWrapper {
return false;
}
}
+
+ /**
+ * Shows a voice session identified by {@code token}
+ * @return true if the session was shown, false otherwise
+ */
+ public boolean showVoiceSession(IBinder token, Bundle args, int flags) {
+ IVoiceInteractionManagerService service = IVoiceInteractionManagerService.Stub.asInterface(
+ ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+ if (service == null) {
+ return false;
+ }
+ try {
+ return service.showSessionFromSession(token, args, flags);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
}
diff --git a/com/android/systemui/shared/system/NavigationBarCompat.java b/com/android/systemui/shared/system/NavigationBarCompat.java
index 79c1cb17..bff0d9b8 100644
--- a/com/android/systemui/shared/system/NavigationBarCompat.java
+++ b/com/android/systemui/shared/system/NavigationBarCompat.java
@@ -17,10 +17,27 @@
package com.android.systemui.shared.system;
import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import sun.misc.Resource;
+
public class NavigationBarCompat {
+ /**
+ * Touch slopes and thresholds for quick step operations. Drag slop is the point where the
+ * home button press/long press over are ignored and will start to drag when exceeded and the
+ * touch slop is when the respected operation will occur when exceeded. Touch slop must be
+ * larger than the drag slop.
+ */
+ public static final int QUICK_STEP_DRAG_SLOP_PX = convertDpToPixel(10);
+ public static final int QUICK_SCRUB_DRAG_SLOP_PX = convertDpToPixel(20);
+ public static final int QUICK_STEP_TOUCH_SLOP_PX = convertDpToPixel(40);
+ public static final int QUICK_SCRUB_TOUCH_SLOP_PX = convertDpToPixel(35);
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({HIT_TARGET_NONE, HIT_TARGET_BACK, HIT_TARGET_HOME, HIT_TARGET_OVERVIEW})
public @interface HitTarget{}
@@ -42,7 +59,6 @@ public class NavigationBarCompat {
* Interaction type: whether the gesture to swipe up from the navigation bar will trigger
* launcher to show overview
*/
-
public static final int FLAG_DISABLE_SWIPE_UP = 0x1;
/**
* Interaction type: enable quick scrub interaction on the home button
@@ -58,4 +74,8 @@ public class NavigationBarCompat {
* Interaction type: show/hide the back button while this service is connected to launcher
*/
public static final int FLAG_HIDE_BACK_BUTTON = 0x8;
+
+ private static int convertDpToPixel(float dp){
+ return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
+ }
}
diff --git a/com/android/systemui/shared/system/PackageManagerWrapper.java b/com/android/systemui/shared/system/PackageManagerWrapper.java
index 6fa7db3f..32e4bbf4 100644
--- a/com/android/systemui/shared/system/PackageManagerWrapper.java
+++ b/com/android/systemui/shared/system/PackageManagerWrapper.java
@@ -18,23 +18,24 @@ package com.android.systemui.shared.system;
import android.app.AppGlobals;
import android.content.ComponentName;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.RemoteException;
-import java.util.ArrayList;
import java.util.List;
public class PackageManagerWrapper {
- private static final String TAG = "PackageManagerWrapper";
-
private static final PackageManagerWrapper sInstance = new PackageManagerWrapper();
private static final IPackageManager mIPackageManager = AppGlobals.getPackageManager();
+ public static final String ACTION_PREFERRED_ACTIVITY_CHANGED =
+ Intent.ACTION_PREFERRED_ACTIVITY_CHANGED;
+
public static PackageManagerWrapper getInstance() {
return sInstance;
}
@@ -53,40 +54,15 @@ public class PackageManagerWrapper {
}
/**
- * @return true if the packageName belongs to the current preferred home app on the device.
- *
- * If will also return false if there are multiple home apps and the user has not picked any
- * preferred home, in which case the user would see a disambiguation screen on going to home.
+ * Report the set of 'Home' activity candidates, plus (if any) which of them
+ * is the current "always use this one" setting.
*/
- public boolean isDefaultHomeActivity(String packageName) {
- List<ResolveInfo> allHomeCandidates = new ArrayList<>();
- ComponentName home;
+ public ComponentName getHomeActivities(List<ResolveInfo> allHomeCandidates) {
try {
- home = mIPackageManager.getHomeActivities(allHomeCandidates);
+ return mIPackageManager.getHomeActivities(allHomeCandidates);
} catch (RemoteException e) {
e.printStackTrace();
- return false;
- }
-
- if (home != null && packageName.equals(home.getPackageName())) {
- return true;
- }
-
- // Find the launcher with the highest priority and return that component if there are no
- // other home activity with the same priority.
- int lastPriority = Integer.MIN_VALUE;
- ComponentName lastComponent = null;
- final int size = allHomeCandidates.size();
- for (int i = 0; i < size; i++) {
- final ResolveInfo ri = allHomeCandidates.get(i);
- if (ri.priority > lastPriority) {
- lastComponent = ri.activityInfo.getComponentName();
- lastPriority = ri.priority;
- } else if (ri.priority == lastPriority) {
- // Two components found with same priority.
- lastComponent = null;
- }
+ return null;
}
- return lastComponent != null && packageName.equals(lastComponent.getPackageName());
}
}
diff --git a/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 5fa6c79a..80e226d9 100644
--- a/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -61,6 +61,14 @@ public class RecentsAnimationControllerCompat {
}
}
+ public void setSplitScreenMinimized(boolean minimized) {
+ try {
+ mAnimationController.setSplitScreenMinimized(minimized);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to set minimize dock", e);
+ }
+ }
+
public void finish(boolean toHome) {
try {
mAnimationController.finish(toHome);
diff --git a/android/security/keystore/InternalRecoveryServiceException.java b/com/android/systemui/shared/system/ThreadedRendererCompat.java
index 40076f73..bf88a291 100644
--- a/android/security/keystore/InternalRecoveryServiceException.java
+++ b/com/android/systemui/shared/system/ThreadedRendererCompat.java
@@ -11,21 +11,23 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
-package android.security.keystore;
+package com.android.systemui.shared.system;
+
+import android.view.ThreadedRenderer;
/**
- * @deprecated Use {@link android.security.keystore.recovery.InternalRecoveryServiceException}.
- * @hide
+ * @see ThreadedRenderer
*/
-public class InternalRecoveryServiceException extends RecoveryControllerException {
- public InternalRecoveryServiceException(String msg) {
- super(msg);
- }
+public class ThreadedRendererCompat {
+
+ public static int EGL_CONTEXT_PRIORITY_HIGH_IMG = 0x3101;
+ public static int EGL_CONTEXT_PRIORITY_MEDIUM_IMG = 0x3102;
+ public static int EGL_CONTEXT_PRIORITY_LOW_IMG = 0x3103;
- public InternalRecoveryServiceException(String message, Throwable cause) {
- super(message, cause);
+ public static void setContextPriority(int priority) {
+ ThreadedRenderer.setContextPriority(priority);
}
}
diff --git a/com/android/systemui/shared/system/TransactionCompat.java b/com/android/systemui/shared/system/TransactionCompat.java
index c82c5191..9975c413 100644
--- a/com/android/systemui/shared/system/TransactionCompat.java
+++ b/com/android/systemui/shared/system/TransactionCompat.java
@@ -101,6 +101,11 @@ public class TransactionCompat {
return this;
}
+ public TransactionCompat setEarlyWakeup() {
+ mTransaction.setEarlyWakeup();
+ return this;
+ }
+
public TransactionCompat setColor(SurfaceControlCompat surfaceControl, float[] color) {
mTransaction.setColor(surfaceControl.mSurfaceControl, color);
return this;
diff --git a/com/android/systemui/shared/system/WindowCallbacksCompat.java b/com/android/systemui/shared/system/WindowCallbacksCompat.java
new file mode 100644
index 00000000..b2b140e4
--- /dev/null
+++ b/com/android/systemui/shared/system/WindowCallbacksCompat.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.systemui.shared.system;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.DisplayListCanvas;
+import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.WindowCallbacks;
+
+public class WindowCallbacksCompat {
+
+ private final WindowCallbacks mWindowCallbacks = new WindowCallbacks() {
+ @Override
+ public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+ Rect stableInsets) {
+ WindowCallbacksCompat.this.onWindowSizeIsChanging(newBounds, fullscreen, systemInsets,
+ stableInsets);
+ }
+
+ @Override
+ public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen,
+ Rect systemInsets, Rect stableInsets, int resizeMode) {
+ WindowCallbacksCompat.this.onWindowDragResizeStart(initialBounds, fullscreen,
+ systemInsets, stableInsets, resizeMode);
+ }
+
+ @Override
+ public void onWindowDragResizeEnd() {
+ WindowCallbacksCompat.this.onWindowDragResizeEnd();
+ }
+
+ @Override
+ public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
+ return WindowCallbacksCompat.this.onContentDrawn(offsetX, offsetY, sizeX, sizeY);
+ }
+
+ @Override
+ public void onRequestDraw(boolean reportNextDraw) {
+ WindowCallbacksCompat.this.onRequestDraw(reportNextDraw);
+ }
+
+ @Override
+ public void onPostDraw(DisplayListCanvas canvas) {
+ WindowCallbacksCompat.this.onPostDraw(canvas);
+ }
+ };
+
+ private final View mView;
+
+ public WindowCallbacksCompat(View view) {
+ mView = view;
+ }
+
+ public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets,
+ Rect stableInsets) { }
+
+ public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets,
+ Rect stableInsets, int resizeMode) { }
+
+ public void onWindowDragResizeEnd() { }
+
+ public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
+ return false;
+ }
+
+ public void onRequestDraw(boolean reportNextDraw) {
+ if (reportNextDraw) {
+ reportDrawFinish();
+ }
+ }
+
+ public void onPostDraw(Canvas canvas) { }
+
+ public void reportDrawFinish() {
+ mView.getViewRootImpl().reportDrawFinish();
+ }
+
+ public boolean attach() {
+ ViewRootImpl root = mView.getViewRootImpl();
+ if (root != null) {
+ root.addWindowCallbacks(mWindowCallbacks);
+ root.requestInvalidateRootRenderNode();
+ return true;
+ }
+ return false;
+ }
+
+ public void detach() {
+ ViewRootImpl root = mView.getViewRootImpl();
+ if (root != null) {
+ root.removeWindowCallbacks(mWindowCallbacks);
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/AppOpsListener.java b/com/android/systemui/statusbar/AppOpsListener.java
index 2ec78cfe..019c6807 100644
--- a/com/android/systemui/statusbar/AppOpsListener.java
+++ b/com/android/systemui/statusbar/AppOpsListener.java
@@ -62,7 +62,7 @@ public class AppOpsListener implements AppOpsManager.OnOpActiveChangedListener {
public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
mFsc.onAppOpChanged(code, uid, packageName, active);
mPresenter.getHandler().post(() -> {
- mEntryManager.updateNotificationsForAppOps(code, uid, packageName, active);
+ mEntryManager.updateNotificationsForAppOp(code, uid, packageName, active);
});
}
}
diff --git a/com/android/systemui/statusbar/CommandQueue.java b/com/android/systemui/statusbar/CommandQueue.java
index 65037f99..6fd0aa63 100644
--- a/com/android/systemui/statusbar/CommandQueue.java
+++ b/com/android/systemui/statusbar/CommandQueue.java
@@ -18,7 +18,7 @@ package com.android.systemui.statusbar;
import android.content.ComponentName;
import android.graphics.Rect;
-import android.hardware.biometrics.IBiometricDialogReceiver;
+import android.hardware.biometrics.IBiometricPromptReceiver;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -160,7 +160,7 @@ public class CommandQueue extends IStatusBar.Stub {
default void onRotationProposal(int rotation, boolean isValid) { }
- default void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) { }
+ default void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) { }
default void onFingerprintAuthenticated() { }
default void onFingerprintHelp(String message) { }
default void onFingerprintError(String error) { }
@@ -513,7 +513,7 @@ public class CommandQueue extends IStatusBar.Stub {
}
@Override
- public void showFingerprintDialog(Bundle bundle, IBiometricDialogReceiver receiver) {
+ public void showFingerprintDialog(Bundle bundle, IBiometricPromptReceiver receiver) {
synchronized (mLock) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = bundle;
@@ -759,7 +759,7 @@ public class CommandQueue extends IStatusBar.Stub {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).showFingerprintDialog(
(Bundle)((SomeArgs)msg.obj).arg1,
- (IBiometricDialogReceiver)((SomeArgs)msg.obj).arg2);
+ (IBiometricPromptReceiver)((SomeArgs)msg.obj).arg2);
}
break;
case MSG_FINGERPRINT_AUTHENTICATED:
diff --git a/com/android/systemui/statusbar/ExpandableNotificationRow.java b/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 3ece2f95..87e6608a 100644
--- a/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -35,6 +35,7 @@ import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
@@ -100,13 +101,17 @@ import java.util.function.Consumer;
public class ExpandableNotificationRow extends ActivatableNotificationView
implements PluginListener<NotificationMenuRowPlugin> {
+ private static final boolean DEBUG = false;
private static final int DEFAULT_DIVIDER_ALPHA = 0x29;
private static final int COLORED_DIVIDER_ALPHA = 0x7B;
private static final int MENU_VIEW_INDEX = 0;
private static final String TAG = "ExpandableNotifRow";
+ /**
+ * Listener for when {@link ExpandableNotificationRow} is laid out.
+ */
public interface LayoutListener {
- public void onLayout();
+ void onLayout();
}
private LayoutListener mLayoutListener;
@@ -174,8 +179,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private NotificationGuts mGuts;
private NotificationData.Entry mEntry;
private StatusBarNotification mStatusBarNotification;
- private PackageManager mCachedPackageManager;
- private PackageInfo mCachedPackageInfo;
+ /**
+ * Whether or not this row represents a system notification. Note that if this is {@code null},
+ * that means we were either unable to retrieve the info or have yet to retrieve the info.
+ */
+ private Boolean mIsSystemNotification;
private String mAppName;
private boolean mIsHeadsUp;
private boolean mLastChronometerRunning = true;
@@ -271,7 +279,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
public Float get(ExpandableNotificationRow object) {
return object.getTranslation();
}
- };
+ };
private OnClickListener mOnClickListener;
private boolean mHeadsupDisappearRunning;
private View mChildAfterViewWhenDismissed;
@@ -292,6 +300,33 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private int mNotificationColorAmbient;
private NotificationViewState mNotificationViewState;
+ private SystemNotificationAsyncTask mSystemNotificationAsyncTask =
+ new SystemNotificationAsyncTask();
+
+ /**
+ * Returns whether the given {@code statusBarNotification} is a system notification.
+ * <b>Note</b>, this should be run in the background thread if possible as it makes multiple IPC
+ * calls.
+ */
+ private static Boolean isSystemNotification(
+ Context context, StatusBarNotification statusBarNotification) {
+ PackageManager packageManager = StatusBar.getPackageManagerForUser(
+ context, statusBarNotification.getUser().getIdentifier());
+ Boolean isSystemNotification = null;
+
+ try {
+ PackageInfo packageInfo = packageManager.getPackageInfo(
+ statusBarNotification.getPackageName(), PackageManager.GET_SIGNATURES);
+
+ isSystemNotification =
+ com.android.settingslib.Utils.isSystemPackage(
+ context.getResources(), packageManager, packageInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "cacheIsSystemNotification: Could not find package info");
+ }
+ return isSystemNotification;
+ }
+
@Override
public boolean isGroupExpansionChanging() {
if (isChildInGroup()) {
@@ -383,45 +418,43 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mStatusBarNotification = entry.notification;
mNotificationInflater.inflateNotificationViews();
- perhapsCachePackageInfo();
+ cacheIsSystemNotification();
}
/**
- * Caches the package manager and info objects which are expensive to obtain.
+ * Caches whether or not this row contains a system notification. Note, this is only cached
+ * once per notification as the packageInfo can't technically change for a notification row.
*/
- private void perhapsCachePackageInfo() {
- if (mCachedPackageInfo == null) {
- mCachedPackageManager = StatusBar.getPackageManagerForUser(
- mContext, mStatusBarNotification.getUser().getIdentifier());
- try {
- mCachedPackageInfo = mCachedPackageManager.getPackageInfo(
- mStatusBarNotification.getPackageName(), PackageManager.GET_SIGNATURES);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "perhapsCachePackageInfo: Could not find package info");
+ private void cacheIsSystemNotification() {
+ if (mIsSystemNotification == null) {
+ if (mSystemNotificationAsyncTask.getStatus() == AsyncTask.Status.PENDING) {
+ // Run async task once, only if it hasn't already been executed. Note this is
+ // executed in serial - no need to parallelize this small task.
+ mSystemNotificationAsyncTask.execute();
}
}
}
/**
- * Returns whether this row is considered non-blockable (e.g. it's a non-blockable system notif,
- * covers multiple channels, or is in a whitelist).
+ * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif
+ * or is in a whitelist).
*/
public boolean getIsNonblockable() {
- boolean isNonblockable;
-
- isNonblockable = Dependency.get(NotificationBlockingHelperManager.class)
+ boolean isNonblockable = Dependency.get(NotificationBlockingHelperManager.class)
.isNonblockablePackage(mStatusBarNotification.getPackageName());
- // Only bother with going through the children if the row is still blockable based on the
- // number of unique channels.
- if (!isNonblockable) {
- isNonblockable = getNumUniqueChannels() > 1;
+ // If the SystemNotifAsyncTask hasn't finished running or retrieved a value, we'll try once
+ // again, but in-place on the main thread this time. This should rarely ever get called.
+ if (mIsSystemNotification == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving isSystemNotification on main thread");
+ }
+ mSystemNotificationAsyncTask.cancel(true /* mayInterruptIfRunning */);
+ mIsSystemNotification = isSystemNotification(mContext, mStatusBarNotification);
}
- // Only bother with IPC if the package is still blockable.
- if (!isNonblockable && mCachedPackageManager != null && mCachedPackageInfo != null) {
- if (com.android.settingslib.Utils.isSystemPackage(
- mContext.getResources(), mCachedPackageManager, mCachedPackageInfo)) {
+ if (!isNonblockable && mIsSystemNotification != null) {
+ if (mIsSystemNotification) {
if (mEntry.channel != null
&& !mEntry.channel.isBlockableSystem()) {
isNonblockable = true;
@@ -2828,4 +2861,21 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
*/
boolean onClick(View v, int x, int y, MenuItem item);
}
+
+ /**
+ * Background task for executing IPCs to check if the notification is a system notification. The
+ * output is used for both the blocking helper and the notification info.
+ */
+ private class SystemNotificationAsyncTask extends AsyncTask<Void, Void, Boolean> {
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ return isSystemNotification(mContext, mStatusBarNotification);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mIsSystemNotification = result;
+ }
+ }
}
diff --git a/com/android/systemui/statusbar/NotificationContentView.java b/com/android/systemui/statusbar/NotificationContentView.java
index b81e9af4..4256cd63 100644
--- a/com/android/systemui/statusbar/NotificationContentView.java
+++ b/com/android/systemui/statusbar/NotificationContentView.java
@@ -26,6 +26,7 @@ import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.MotionEvent;
import android.view.NotificationHeaderView;
import android.view.View;
import android.view.ViewGroup;
@@ -79,7 +80,7 @@ public class NotificationContentView extends FrameLayout {
private RemoteInputView mHeadsUpRemoteInput;
private SmartReplyConstants mSmartReplyConstants;
- private SmartReplyView mExpandedSmartReplyView;
+ private SmartReplyLogger mSmartReplyLogger;
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
@@ -152,6 +153,7 @@ public class NotificationContentView extends FrameLayout {
super(context, attrs);
mHybridGroupManager = new HybridGroupManager(getContext(), this);
mSmartReplyConstants = Dependency.get(SmartReplyConstants.class);
+ mSmartReplyLogger = Dependency.get(SmartReplyLogger.class);
initView();
}
@@ -1242,7 +1244,7 @@ public class NotificationContentView extends FrameLayout {
}
applyRemoteInput(entry, hasRemoteInput);
- applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices);
+ applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry);
}
private void applyRemoteInput(NotificationData.Entry entry, boolean hasRemoteInput) {
@@ -1343,13 +1345,21 @@ public class NotificationContentView extends FrameLayout {
return null;
}
- private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent) {
- mExpandedSmartReplyView = mExpandedChild == null ?
- null : applySmartReplyView(mExpandedChild, remoteInput, pendingIntent);
+ private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent,
+ NotificationData.Entry entry) {
+ if (mExpandedChild != null) {
+ SmartReplyView view =
+ applySmartReplyView(mExpandedChild, remoteInput, pendingIntent, entry);
+ if (view != null && remoteInput != null && remoteInput.getChoices() != null
+ && remoteInput.getChoices().length > 0) {
+ mSmartReplyLogger.smartRepliesAdded(entry, remoteInput.getChoices().length);
+ }
+ }
}
private SmartReplyView applySmartReplyView(
- View view, RemoteInput remoteInput, PendingIntent pendingIntent) {
+ View view, RemoteInput remoteInput, PendingIntent pendingIntent,
+ NotificationData.Entry entry) {
View smartReplyContainerCandidate = view.findViewById(
com.android.internal.R.id.smart_reply_container);
if (!(smartReplyContainerCandidate instanceof LinearLayout)) {
@@ -1371,7 +1381,8 @@ public class NotificationContentView extends FrameLayout {
}
}
if (smartReplyView != null) {
- smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent);
+ smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent,
+ mSmartReplyLogger, entry);
smartReplyContainer.setVisibility(View.VISIBLE);
}
return smartReplyView;
@@ -1631,6 +1642,42 @@ public class NotificationContentView extends FrameLayout {
return null;
}
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ float y = ev.getY();
+ // We still want to distribute touch events to the remote input even if it's outside the
+ // view boundary. We're therefore manually dispatching these events to the remote view
+ RemoteInputView riv = getRemoteInputForView(getViewForVisibleType(mVisibleType));
+ if (riv != null && riv.getVisibility() == VISIBLE) {
+ int inputStart = mUnrestrictedContentHeight - riv.getHeight();
+ if (y <= mUnrestrictedContentHeight && y >= inputStart) {
+ ev.offsetLocation(0, -inputStart);
+ return riv.dispatchTouchEvent(ev);
+ }
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ /**
+ * Overridden to make sure touches to the reply action bar actually go through to this view
+ */
+ @Override
+ public boolean pointInView(float localX, float localY, float slop) {
+ float top = mClipTopAmount;
+ float bottom = mUnrestrictedContentHeight;
+ return localX >= -slop && localY >= top - slop && localX < ((mRight - mLeft) + slop) &&
+ localY < (bottom + slop);
+ }
+
+ private RemoteInputView getRemoteInputForView(View child) {
+ if (child == mExpandedChild) {
+ return mExpandedRemoteInput;
+ } else if (child == mHeadsUpChild) {
+ return mHeadsUpRemoteInput;
+ }
+ return null;
+ }
+
public int getExpandHeight() {
int viewType = VISIBLE_TYPE_EXPANDED;
if (mExpandedChild == null) {
diff --git a/com/android/systemui/statusbar/NotificationData.java b/com/android/systemui/statusbar/NotificationData.java
index 4b6ab64a..ab46b39a 100644
--- a/com/android/systemui/statusbar/NotificationData.java
+++ b/com/android/systemui/statusbar/NotificationData.java
@@ -383,8 +383,6 @@ public class NotificationData {
}
mGroupManager.onEntryAdded(entry);
- updateAppOps(entry);
-
updateRankingAndSort(mRankingMap);
}
@@ -403,25 +401,14 @@ public class NotificationData {
updateRankingAndSort(ranking);
}
- private void updateAppOps(Entry entry) {
- final int uid = entry.notification.getUid();
- final String pkg = entry.notification.getPackageName();
- ArraySet<Integer> activeOps = mFsc.getAppOps(entry.notification.getUserId(), pkg);
- if (activeOps != null) {
- int N = activeOps.size();
- for (int i = 0; i < N; i++) {
- updateAppOp(activeOps.valueAt(i), uid, pkg, true);
- }
- }
- }
-
- public void updateAppOp(int appOp, int uid, String pkg, boolean showIcon) {
+ public void updateAppOp(int appOp, int uid, String pkg, String key, boolean showIcon) {
synchronized (mEntries) {
final int N = mEntries.size();
for (int i = 0; i < N; i++) {
Entry entry = mEntries.valueAt(i);
if (uid == entry.notification.getUid()
- && pkg.equals(entry.notification.getPackageName())) {
+ && pkg.equals(entry.notification.getPackageName())
+ && key.equals(entry.key)) {
if (showIcon) {
entry.mActiveAppOps.add(appOp);
} else {
diff --git a/com/android/systemui/statusbar/NotificationEntryManager.java b/com/android/systemui/statusbar/NotificationEntryManager.java
index 45df4505..849cfdd9 100644
--- a/com/android/systemui/statusbar/NotificationEntryManager.java
+++ b/com/android/systemui/statusbar/NotificationEntryManager.java
@@ -43,6 +43,7 @@ import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.NotificationMessagingUtil;
@@ -665,6 +666,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
}
// Add the expanded view and icon.
mNotificationData.add(entry);
+ tagForeground(entry.notification);
updateNotifications();
}
@@ -726,6 +728,19 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
mPendingNotifications.put(key, shadeEntry);
}
+ @VisibleForTesting
+ protected void tagForeground(StatusBarNotification notification) {
+ ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps(
+ notification.getUserId(), notification.getPackageName());
+ if (activeOps != null) {
+ int N = activeOps.size();
+ for (int i = 0; i < N; i++) {
+ updateNotificationsForAppOp(activeOps.valueAt(i), notification.getUid(),
+ notification.getPackageName(), true);
+ }
+ }
+ }
+
@Override
public void addNotification(StatusBarNotification notification,
NotificationListenerService.RankingMap ranking) {
@@ -736,10 +751,11 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
}
}
- public void updateNotificationsForAppOps(int appOp, int uid, String pkg, boolean showIcon) {
- if (mForegroundServiceController.getStandardLayoutKey(
- UserHandle.getUserId(uid), pkg) != null) {
- mNotificationData.updateAppOp(appOp, uid, pkg, showIcon);
+ public void updateNotificationsForAppOp(int appOp, int uid, String pkg, boolean showIcon) {
+ String foregroundKey = mForegroundServiceController.getStandardLayoutKey(
+ UserHandle.getUserId(uid), pkg);
+ if (foregroundKey != null) {
+ mNotificationData.updateAppOp(appOp, uid, pkg, foregroundKey, showIcon);
updateNotifications();
}
}
diff --git a/com/android/systemui/statusbar/NotificationInfo.java b/com/android/systemui/statusbar/NotificationInfo.java
index a93be00b..81dd9e8c 100644
--- a/com/android/systemui/statusbar/NotificationInfo.java
+++ b/com/android/systemui/statusbar/NotificationInfo.java
@@ -23,6 +23,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -34,10 +35,12 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
+import android.os.Handler;
import android.os.RemoteException;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
@@ -63,10 +66,10 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private INotificationManager mINotificationManager;
private PackageManager mPm;
- private String mPkg;
+ private String mPackageName;
private String mAppName;
private int mAppUid;
- private int mNumNotificationChannels;
+ private int mNumUniqueChannelsInRow;
private NotificationChannel mSingleNotificationChannel;
private int mStartingUserImportance;
private int mChosenImportance;
@@ -87,7 +90,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private OnClickListener mOnKeepShowing = this::closeControls;
- private OnClickListener mOnStopMinNotifications = v -> {
+ private OnClickListener mOnStopOrMinimizeNotifications = v -> {
swapContent(false);
};
@@ -120,16 +123,16 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
final INotificationManager iNotificationManager,
final String pkg,
final NotificationChannel notificationChannel,
- final int numChannels,
+ final int numUniqueChannelsInRow,
final StatusBarNotification sbn,
final CheckSaveListener checkSaveListener,
final OnSettingsClickListener onSettingsClick,
final OnAppSettingsClickListener onAppSettingsClick,
boolean isNonblockable)
throws RemoteException {
- bindNotification(pm, iNotificationManager, pkg, notificationChannel, numChannels, sbn,
- checkSaveListener, onSettingsClick, onAppSettingsClick, isNonblockable,
- false /* isBlockingHelper */,
+ bindNotification(pm, iNotificationManager, pkg, notificationChannel,
+ numUniqueChannelsInRow, sbn, checkSaveListener, onSettingsClick,
+ onAppSettingsClick, isNonblockable, false /* isBlockingHelper */,
false /* isUserSentimentNegative */);
}
@@ -138,7 +141,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
INotificationManager iNotificationManager,
String pkg,
NotificationChannel notificationChannel,
- int numChannels,
+ int numUniqueChannelsInRow,
StatusBarNotification sbn,
CheckSaveListener checkSaveListener,
OnSettingsClickListener onSettingsClick,
@@ -148,12 +151,12 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
boolean isUserSentimentNegative)
throws RemoteException {
mINotificationManager = iNotificationManager;
- mPkg = pkg;
- mNumNotificationChannels = numChannels;
+ mPackageName = pkg;
+ mNumUniqueChannelsInRow = numUniqueChannelsInRow;
mSbn = sbn;
mPm = pm;
mAppSettingsClickListener = onAppSettingsClick;
- mAppName = mPkg;
+ mAppName = mPackageName;
mCheckSaveListener = checkSaveListener;
mOnSettingsClickListener = onSettingsClick;
mSingleNotificationChannel = notificationChannel;
@@ -167,11 +170,11 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
pkg, mAppUid, false /* includeDeleted */);
- if (mNumNotificationChannels == 0) {
+ if (mNumUniqueChannelsInRow == 0) {
throw new IllegalArgumentException("bindNotification requires at least one channel");
} else {
// Special behavior for the Default channel if no other channels have been defined.
- mIsSingleDefaultChannel = mNumNotificationChannels == 1
+ mIsSingleDefaultChannel = mNumUniqueChannelsInRow == 1
&& mSingleNotificationChannel.getId().equals(
NotificationChannel.DEFAULT_CHANNEL_ID)
&& numTotalChannels == 1;
@@ -187,7 +190,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
Drawable pkgicon = null;
ApplicationInfo info;
try {
- info = mPm.getApplicationInfo(mPkg,
+ info = mPm.getApplicationInfo(
+ mPackageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE
@@ -208,7 +212,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
final NotificationChannelGroup notificationChannelGroup =
mINotificationManager.getNotificationChannelGroupForPackage(
- mSingleNotificationChannel.getGroup(), mPkg, mAppUid);
+ mSingleNotificationChannel.getGroup(), mPackageName, mAppUid);
if (notificationChannelGroup != null) {
groupName = notificationChannelGroup.getName();
}
@@ -232,7 +236,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
settingsButton.setOnClickListener(
(View view) -> {
mOnSettingsClickListener.onClick(view,
- mNumNotificationChannels > 1 ? null : mSingleNotificationChannel,
+ mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel,
appUidF);
});
} else {
@@ -248,7 +252,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
} else {
if (mNegativeUserSentiment) {
blockPrompt.setText(R.string.inline_blocking_helper);
- } else if (mIsSingleDefaultChannel || mNumNotificationChannels > 1) {
+ } else if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
blockPrompt.setText(R.string.inline_keep_showing_app);
} else {
blockPrompt.setText(R.string.inline_keep_showing);
@@ -258,7 +262,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private void bindName() {
final TextView channelName = findViewById(R.id.channel_name);
- if (mIsSingleDefaultChannel || mNumNotificationChannels > 1) {
+ if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
channelName.setVisibility(View.GONE);
} else {
channelName.setText(mSingleNotificationChannel.getName());
@@ -270,19 +274,26 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
}
private void saveImportance() {
- if (mIsNonblockable) {
- return;
+ if (!mIsNonblockable) {
+ if (mCheckSaveListener != null) {
+ mCheckSaveListener.checkSave(this::updateImportance, mSbn);
+ } else {
+ updateImportance();
+ }
}
+ }
+
+ /**
+ * Commits the updated importance values on the background thread.
+ */
+ private void updateImportance() {
MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
mChosenImportance - mStartingUserImportance);
- mSingleNotificationChannel.setImportance(mChosenImportance);
- mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
- try {
- mINotificationManager.updateNotificationChannelForPackage(
- mPkg, mAppUid, mSingleNotificationChannel);
- } catch (RemoteException e) {
- // :(
- }
+
+ Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
+ bgHandler.post(new UpdateImportanceRunnable(mINotificationManager, mPackageName, mAppUid,
+ mNumUniqueChannelsInRow == 1 ? mSingleNotificationChannel : null,
+ mStartingUserImportance, mChosenImportance));
}
private void bindButtons() {
@@ -292,9 +303,9 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
View minimize = findViewById(R.id.minimize);
findViewById(R.id.undo).setOnClickListener(mOnUndo);
- block.setOnClickListener(mOnStopMinNotifications);
+ block.setOnClickListener(mOnStopOrMinimizeNotifications);
keep.setOnClickListener(mOnKeepShowing);
- minimize.setOnClickListener(mOnStopMinNotifications);
+ minimize.setOnClickListener(mOnStopOrMinimizeNotifications);
if (mIsNonblockable) {
keep.setText(R.string.notification_done);
@@ -308,15 +319,15 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
minimize.setVisibility(GONE);
}
- // Set up app settings link
+ // Set up app settings link (i.e. Customize)
TextView settingsLinkView = findViewById(R.id.app_settings);
- Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
+ Intent settingsIntent = getAppSettingsIntent(mPm, mPackageName, mSingleNotificationChannel,
mSbn.getId(), mSbn.getTag());
- if (settingsIntent != null
+ if (!mIsForBlockingHelper
+ && settingsIntent != null
&& !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
settingsLinkView.setVisibility(VISIBLE);
- settingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
- mSbn.getNotification().getSettingsText()));
+ settingsLinkView.setText(mContext.getString(R.string.notification_app_settings));
settingsLinkView.setOnClickListener((View view) -> {
mAppSettingsClickListener.onClick(view, settingsIntent);
});
@@ -415,12 +426,25 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
return intent;
}
+ /**
+ * Closes the controls and commits the updated importance values (indirectly). If this view is
+ * being used to show the blocking helper, this will immediately dismiss the blocking helper and
+ * commit the updated importance.
+ *
+ * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
+ * user does not have the ability to undo the action anymore. See {@link #swapContent(boolean)}
+ * for where undo is handled.
+ */
@VisibleForTesting
void closeControls(View v) {
if (mIsForBlockingHelper) {
NotificationBlockingHelperManager manager =
Dependency.get(NotificationBlockingHelperManager.class);
manager.dismissCurrentBlockingHelper();
+
+ // Since this won't get a callback via gutsContainer.closeControls, save the new
+ // importance values immediately.
+ saveImportance();
} else {
int[] parentLoc = new int[2];
int[] targetLoc = new int[2];
@@ -454,11 +478,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
// Save regardless of the importance so we can lock the importance field if the user wants
// to keep getting notifications
if (save) {
- if (mCheckSaveListener != null) {
- mCheckSaveListener.checkSave(this::saveImportance, mSbn);
- } else {
- saveImportance();
- }
+ saveImportance();
}
return false;
}
@@ -467,4 +487,48 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
public int getActualHeight() {
return getHeight();
}
+
+ /**
+ * Runnable to either update the given channel (with a new importance value) or, if no channel
+ * is provided, update notifications enabled state for the package.
+ */
+ private static class UpdateImportanceRunnable implements Runnable {
+ private final INotificationManager mINotificationManager;
+ private final String mPackageName;
+ private final int mAppUid;
+ private final @Nullable NotificationChannel mChannelToUpdate;
+ private final int mCurrentImportance;
+ private final int mNewImportance;
+
+
+ public UpdateImportanceRunnable(INotificationManager notificationManager,
+ String packageName, int appUid, @Nullable NotificationChannel channelToUpdate,
+ int currentImportance, int newImportance) {
+ mINotificationManager = notificationManager;
+ mPackageName = packageName;
+ mAppUid = appUid;
+ mChannelToUpdate = channelToUpdate;
+ mCurrentImportance = currentImportance;
+ mNewImportance = newImportance;
+ }
+
+ @Override
+ public void run() {
+ try {
+ if (mChannelToUpdate != null) {
+ mChannelToUpdate.setImportance(mNewImportance);
+ mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+ mINotificationManager.updateNotificationChannelForPackage(
+ mPackageName, mAppUid, mChannelToUpdate);
+ } else {
+ // For notifications with more than one channel, update notification enabled
+ // state. If the importance was lowered, we disable notifications.
+ mINotificationManager.setNotificationsEnabledForPackage(
+ mPackageName, mAppUid, mNewImportance >= mCurrentImportance);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to update notification importance", e);
+ }
+ }
+ }
}
diff --git a/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index ccabb79e..e24bf676 100644
--- a/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -47,7 +47,6 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.List;
/**
* Handles keeping track of the current user, profiles, and various things related to hiding
@@ -352,7 +351,8 @@ public class NotificationLockscreenUserManager implements Dumpable {
final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
mContext.getContentResolver(),
Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0, userHandle);
- final boolean allowedByDpm = adminAllowsUnredactedNotifications(userHandle);
+ final boolean allowedByDpm = adminAllowsKeyguardFeature(userHandle,
+ DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS);
final boolean allowed = allowedByUser && allowedByDpm;
mUsersAllowingPrivateNotifications.append(userHandle, allowed);
return allowed;
@@ -361,13 +361,13 @@ public class NotificationLockscreenUserManager implements Dumpable {
return mUsersAllowingPrivateNotifications.get(userHandle);
}
- private boolean adminAllowsUnredactedNotifications(int userHandle) {
+ private boolean adminAllowsKeyguardFeature(int userHandle, int feature) {
if (userHandle == UserHandle.USER_ALL) {
return true;
}
- final int dpmFlags = mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */,
- userHandle);
- return (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0;
+ final int dpmFlags =
+ mDevicePolicyManager.getKeyguardDisabledFeatures(null /* admin */, userHandle);
+ return (dpmFlags & feature) == 0;
}
/**
@@ -389,14 +389,17 @@ public class NotificationLockscreenUserManager implements Dumpable {
* "public" (secure & locked) mode?
*/
private boolean userAllowsNotificationsInPublic(int userHandle) {
- if (isCurrentProfile(userHandle)) {
+ if (isCurrentProfile(userHandle) && userHandle != mCurrentUserId) {
return true;
}
if (mUsersAllowingNotifications.indexOfKey(userHandle) < 0) {
- final boolean allowed = 0 != Settings.Secure.getIntForUser(
+ final boolean allowedByUser = 0 != Settings.Secure.getIntForUser(
mContext.getContentResolver(),
Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0, userHandle);
+ final boolean allowedByDpm = adminAllowsKeyguardFeature(userHandle,
+ DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS);
+ final boolean allowed = allowedByUser && allowedByDpm;
mUsersAllowingNotifications.append(userHandle, allowed);
return allowed;
}
@@ -428,7 +431,6 @@ public class NotificationLockscreenUserManager implements Dumpable {
Notification.VISIBILITY_PRIVATE;
}
-
private void updateCurrentProfilesCache() {
synchronized (mCurrentProfiles) {
mCurrentProfiles.clear();
diff --git a/com/android/systemui/statusbar/NotificationMediaManager.java b/com/android/systemui/statusbar/NotificationMediaManager.java
index 852239a2..abc261e6 100644
--- a/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -172,6 +172,14 @@ public class NotificationMediaManager implements Dumpable {
}
}
+ if (mediaNotification != null) {
+ mMediaNotificationKey = mediaNotification.notification.getKey();
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
+ + mMediaNotificationKey + " controller=" + mMediaController);
+ }
+ }
+
if (controller != null && !sameSessions(mMediaController, controller)) {
// We have a new media session
clearCurrentMediaNotification();
@@ -183,13 +191,6 @@ public class NotificationMediaManager implements Dumpable {
+ mMediaMetadata);
}
- if (mediaNotification != null) {
- mMediaNotificationKey = mediaNotification.notification.getKey();
- if (DEBUG_MEDIA) {
- Log.v(TAG, "DEBUG_MEDIA: Found new media notification: key="
- + mMediaNotificationKey + " controller=" + mMediaController);
- }
- }
metaDataChanged = true;
}
}
diff --git a/com/android/systemui/statusbar/NotificationShelf.java b/com/android/systemui/statusbar/NotificationShelf.java
index 0112661c..6364f5b6 100644
--- a/com/android/systemui/statusbar/NotificationShelf.java
+++ b/com/android/systemui/statusbar/NotificationShelf.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar;
import static com.android.systemui.statusbar.phone.NotificationIconContainer.IconState.NO_VALUE;
-import static com.android.systemui.statusbar.phone.NotificationIconContainer.OVERFLOW_EARLY_AMOUNT;
import android.content.Context;
import android.content.res.Configuration;
@@ -26,6 +25,7 @@ import android.graphics.Rect;
import android.os.SystemProperties;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.MathUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -58,6 +58,8 @@ public class NotificationShelf extends ActivatableNotificationView implements
= SystemProperties.getBoolean("debug.icon_scroll_animations", true);
private static final int TAG_CONTINUOUS_CLIPPING = R.id.continuous_clipping_tag;
private static final String TAG = "NotificationShelf";
+ private static final long SHELF_IN_TRANSLATION_DURATION = 220;
+
private ViewInvertHelper mViewInvertHelper;
private boolean mDark;
private NotificationIconContainer mShelfIcons;
@@ -65,6 +67,7 @@ public class NotificationShelf extends ActivatableNotificationView implements
private int[] mTmp = new int[2];
private boolean mHideBackground;
private int mIconAppearTopPadding;
+ private int mShelfAppearTranslation;
private int mStatusBarHeight;
private int mStatusBarPaddingStart;
private AmbientState mAmbientState;
@@ -120,6 +123,7 @@ public class NotificationShelf extends ActivatableNotificationView implements
mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
mStatusBarPaddingStart = res.getDimensionPixelOffset(R.dimen.status_bar_padding_start);
mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
+ mShelfAppearTranslation = res.getDimensionPixelSize(R.dimen.shelf_appear_translation);
ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -151,6 +155,18 @@ public class NotificationShelf extends ActivatableNotificationView implements
updateInteractiveness();
}
+ public void fadeInTranslating() {
+ float translation = mShelfIcons.getTranslationY();
+ mShelfIcons.setTranslationY(translation + mShelfAppearTranslation);
+ mShelfIcons.setAlpha(0);
+ mShelfIcons.animate()
+ .alpha(1)
+ .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+ .translationY(translation)
+ .setDuration(SHELF_IN_TRANSLATION_DURATION)
+ .start();
+ }
+
@Override
protected View getContentView() {
return mShelfIcons;
@@ -175,12 +191,14 @@ public class NotificationShelf extends ActivatableNotificationView implements
float viewEnd = lastViewState.yTranslation + lastViewState.height;
mShelfState.copyFrom(lastViewState);
mShelfState.height = getIntrinsicHeight();
- mShelfState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
+
+ float awakenTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
getFullyClosedTranslation());
+ float darkTranslation = mAmbientState.getDarkTopPadding();
+ float yRatio = mAmbientState.hasPulsingNotifications() ?
+ 0 : mAmbientState.getDarkAmount();
+ mShelfState.yTranslation = MathUtils.lerp(awakenTranslation, darkTranslation, yRatio);
mShelfState.zTranslation = ambientState.getBaseZHeight();
- if (mAmbientState.isDark() && !mAmbientState.hasPulsingNotifications()) {
- mShelfState.yTranslation = mAmbientState.getDarkTopPadding();
- }
float openedAmount = (mShelfState.yTranslation - getFullyClosedTranslation())
/ (getIntrinsicHeight() * 2);
openedAmount = Math.min(1.0f, openedAmount);
@@ -555,7 +573,9 @@ public class NotificationShelf extends ActivatableNotificationView implements
iconState.translateContent = false;
}
float transitionAmount;
- if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
+ if (mAmbientState.getDarkAmount() > 0 && !row.isInShelf()) {
+ transitionAmount = mAmbientState.isFullyDark() ? 1 : 0;
+ } else if (isLastChild || !USE_ANIMATIONS_WHEN_OPENING || iconState.useFullTransitionAmount
|| iconState.useLinearTransitionAmount) {
transitionAmount = iconTransitionAmount;
} else {
diff --git a/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index fd3a9d5e..1637849d 100644
--- a/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -323,8 +323,7 @@ public class NotificationViewHierarchyManager {
boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry
.notification);
if (suppressedSummary
- || (mLockscreenUserManager.isLockscreenPublicMode(userId)
- && !mLockscreenUserManager.shouldShowLockscreenNotifications())
+ || mLockscreenUserManager.shouldHideNotifications(userId)
|| (isLocked && !showOnKeyguard)) {
entry.row.setVisibility(View.GONE);
} else {
diff --git a/com/android/systemui/statusbar/SmartReplyLogger.java b/com/android/systemui/statusbar/SmartReplyLogger.java
new file mode 100644
index 00000000..75dd77d8
--- /dev/null
+++ b/com/android/systemui/statusbar/SmartReplyLogger.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.systemui.statusbar;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import com.android.internal.statusbar.IStatusBarService;
+
+/**
+ * Handles reporting when smart replies are added to a notification
+ * and clicked upon.
+ */
+public class SmartReplyLogger {
+ protected IStatusBarService mBarService;
+
+ public SmartReplyLogger(Context context) {
+ mBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+ }
+
+ public void smartReplySent(NotificationData.Entry entry, int replyIndex) {
+ try {
+ mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
+ replyIndex);
+ } catch (RemoteException e) {
+ // Nothing to do, system going down
+ }
+ }
+
+ public void smartRepliesAdded(final NotificationData.Entry entry, int replyCount) {
+ try {
+ mBarService.onNotificationSmartRepliesAdded(entry.notification.getKey(),
+ replyCount);
+ } catch (RemoteException e) {
+ // Nothing to do, system going down
+ }
+ }
+}
diff --git a/com/android/systemui/statusbar/StatusBarMobileView.java b/com/android/systemui/statusbar/StatusBarMobileView.java
index b7620f30..51b42395 100644
--- a/com/android/systemui/statusbar/StatusBarMobileView.java
+++ b/com/android/systemui/statusbar/StatusBarMobileView.java
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar;
import static com.android.systemui.statusbar.policy.DarkIconDispatcher.getTint;
+import static com.android.systemui.statusbar.policy.DarkIconDispatcher.isInArea;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -141,12 +142,14 @@ public class StatusBarMobileView extends AlphaOptimizedLinearLayout implements D
if (mState.strengthId != state.strengthId) {
mMobileDrawable.setLevel(state.strengthId);
}
- if (mState.typeId != state.typeId && state.typeId != 0) {
- mMobileType.setContentDescription(state.typeContentDescription);
- mMobileType.setImageResource(state.typeId);
- mMobileType.setVisibility(View.VISIBLE);
- } else {
- mMobileType.setVisibility(View.GONE);
+ if (mState.typeId != state.typeId) {
+ if (state.typeId != 0) {
+ mMobileType.setContentDescription(state.typeContentDescription);
+ mMobileType.setImageResource(state.typeId);
+ mMobileType.setVisibility(View.VISIBLE);
+ } else {
+ mMobileType.setVisibility(View.GONE);
+ }
}
mMobileRoaming.setVisibility(state.roaming ? View.VISIBLE : View.GONE);
@@ -161,6 +164,9 @@ public class StatusBarMobileView extends AlphaOptimizedLinearLayout implements D
@Override
public void onDarkChanged(Rect area, float darkIntensity, int tint) {
+ if (!isInArea(area, this)) {
+ return;
+ }
mMobileDrawable.setDarkIntensity(darkIntensity);
ColorStateList color = ColorStateList.valueOf(getTint(area, this, tint));
mIn.setImageTintList(color);
diff --git a/com/android/systemui/statusbar/StatusBarWifiView.java b/com/android/systemui/statusbar/StatusBarWifiView.java
index afd373ed..62cd16fc 100644
--- a/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar;
import static com.android.systemui.statusbar.policy.DarkIconDispatcher.getTint;
+import static com.android.systemui.statusbar.policy.DarkIconDispatcher.isInArea;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -175,6 +176,9 @@ public class StatusBarWifiView extends AlphaOptimizedLinearLayout implements Dar
@Override
public void onDarkChanged(Rect area, float darkIntensity, int tint) {
+ if (!isInArea(area, this)) {
+ return;
+ }
mDarkIntensity = darkIntensity;
Drawable d = mWifiIcon.getDrawable();
if (d instanceof NeutralGoodDrawable) {
diff --git a/com/android/systemui/statusbar/car/CarFacetButton.java b/com/android/systemui/statusbar/car/CarFacetButton.java
index 5f3e2e35..46f88635 100644
--- a/com/android/systemui/statusbar/car/CarFacetButton.java
+++ b/com/android/systemui/statusbar/car/CarFacetButton.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
@@ -37,11 +38,17 @@ public class CarFacetButton extends LinearLayout {
private AlphaOptimizedImageButton mIcon;
private AlphaOptimizedImageButton mMoreIcon;
private boolean mSelected = false;
+ private String[] mComponentNames;
/** App categories that are to be used with this widget */
private String[] mFacetCategories;
/** App packages that are allowed to be used with this widget */
private String[] mFacetPackages;
private int mIconResourceId;
+ /**
+ * If defined in the xml this will be the icon that's rendered when the button is marked as
+ * selected
+ */
+ private int mSelectedIconResourceId;
private boolean mUseMoreIcon = true;
private float mSelectedAlpha = 1f;
private float mUnselectedAlpha = 1f;
@@ -70,6 +77,8 @@ public class CarFacetButton extends LinearLayout {
String longPressIntentString = typedArray.getString(R.styleable.CarFacetButton_longIntent);
String categoryString = typedArray.getString(R.styleable.CarFacetButton_categories);
String packageString = typedArray.getString(R.styleable.CarFacetButton_packages);
+ String componentNameString =
+ typedArray.getString(R.styleable.CarFacetButton_componentNames);
try {
final Intent intent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
intent.putExtra(EXTRA_FACET_ID, Integer.toString(getId()));
@@ -82,17 +91,20 @@ public class CarFacetButton extends LinearLayout {
mFacetCategories = categoryString.split(FACET_FILTER_DELIMITER);
intent.putExtra(EXTRA_FACET_CATEGORIES, mFacetCategories);
}
+ if (componentNameString != null) {
+ mComponentNames = componentNameString.split(FACET_FILTER_DELIMITER);
+ }
setOnClickListener(v -> {
intent.putExtra(EXTRA_FACET_LAUNCH_PICKER, mSelected);
- mContext.startActivity(intent);
+ mContext.startActivityAsUser(intent, UserHandle.CURRENT);
});
if (longPressIntentString != null) {
final Intent longPressIntent = Intent.parseUri(longPressIntentString,
Intent.URI_INTENT_SCHEME);
setOnLongClickListener(v -> {
- mContext.startActivity(longPressIntent);
+ mContext.startActivityAsUser(longPressIntent, UserHandle.CURRENT);
return true;
});
}
@@ -112,10 +124,9 @@ public class CarFacetButton extends LinearLayout {
mIcon.setClickable(false);
mIcon.setAlpha(mUnselectedAlpha);
mIconResourceId = styledAttributes.getResourceId(R.styleable.CarFacetButton_icon, 0);
- if (mIconResourceId == 0) {
- throw new RuntimeException("specified icon resource was not found and is required");
- }
mIcon.setImageResource(mIconResourceId);
+ mSelectedIconResourceId = styledAttributes.getResourceId(
+ R.styleable.CarFacetButton_selectedIcon, mIconResourceId);
mMoreIcon = findViewById(R.id.car_nav_button_more_icon);
mMoreIcon.setClickable(false);
@@ -144,6 +155,13 @@ public class CarFacetButton extends LinearLayout {
return mFacetPackages;
}
+ public String[] getComponentName() {
+ if (mComponentNames == null) {
+ return new String[0];
+ }
+ return mComponentNames;
+ }
+
/**
* Updates the alpha of the icons to "selected" and shows the "More icon"
* @param selected true if the view must be selected, false otherwise
@@ -161,22 +179,10 @@ public class CarFacetButton extends LinearLayout {
*/
public void setSelected(boolean selected, boolean showMoreIcon) {
mSelected = selected;
- if (selected) {
- if (mUseMoreIcon) {
- mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : GONE);
- }
- mIcon.setAlpha(mSelectedAlpha);
- } else {
- mMoreIcon.setVisibility(GONE);
- mIcon.setAlpha(mUnselectedAlpha);
- }
- }
-
- public void setIcon(Drawable d) {
- if (d != null) {
- mIcon.setImageDrawable(d);
- } else {
- mIcon.setImageResource(mIconResourceId);
+ mIcon.setAlpha(mSelected ? mSelectedAlpha : mUnselectedAlpha);
+ mIcon.setImageResource(mSelected ? mSelectedIconResourceId : mIconResourceId);
+ if (mUseMoreIcon) {
+ mMoreIcon.setVisibility(showMoreIcon ? VISIBLE : GONE);
}
}
}
diff --git a/com/android/systemui/statusbar/car/CarFacetButtonController.java b/com/android/systemui/statusbar/car/CarFacetButtonController.java
index b7d501e7..2d30ce17 100644
--- a/com/android/systemui/statusbar/car/CarFacetButtonController.java
+++ b/com/android/systemui/statusbar/car/CarFacetButtonController.java
@@ -1,10 +1,12 @@
package com.android.systemui.statusbar.car;
import android.app.ActivityManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.util.Log;
import java.util.HashMap;
import java.util.List;
@@ -19,6 +21,7 @@ public class CarFacetButtonController {
protected HashMap<String, CarFacetButton> mButtonsByCategory = new HashMap<>();
protected HashMap<String, CarFacetButton> mButtonsByPackage = new HashMap<>();
+ protected HashMap<String, CarFacetButton> mButtonsByComponentName = new HashMap<>();
protected CarFacetButton mSelectedFacetButton;
protected Context mContext;
@@ -34,28 +37,32 @@ public class CarFacetButtonController {
*/
public void addFacetButton(CarFacetButton facetButton) {
String[] categories = facetButton.getCategories();
- for (int j = 0; j < categories.length; j++) {
- String category = categories[j];
- mButtonsByCategory.put(category, facetButton);
+ for (int i = 0; i < categories.length; i++) {
+ mButtonsByCategory.put(categories[i], facetButton);
}
String[] facetPackages = facetButton.getFacetPackages();
- for (int j = 0; j < facetPackages.length; j++) {
- String facetPackage = facetPackages[j];
- mButtonsByPackage.put(facetPackage, facetButton);
+ for (int i = 0; i < facetPackages.length; i++) {
+ mButtonsByPackage.put(facetPackages[i], facetButton);
+ }
+ String[] componentNames = facetButton.getComponentName();
+ for (int i = 0; i < componentNames.length; i++) {
+ mButtonsByComponentName.put(componentNames[i], facetButton);
}
}
public void removeAll() {
mButtonsByCategory.clear();
mButtonsByPackage.clear();
+ mButtonsByComponentName.clear();
mSelectedFacetButton = null;
}
/**
* This will unselect the currently selected CarFacetButton and determine which one should be
* selected next. It does this by reading the properties on the CarFacetButton and seeing if
- * they are a match with the supplied taskino.
+ * they are a match with the supplied taskInfo.
+ * Order of selection detection ComponentName, PackageName, Category
* @param taskInfo of the currently running application
*/
public void taskChanged(ActivityManager.RunningTaskInfo taskInfo) {
@@ -69,7 +76,10 @@ public class CarFacetButtonController {
if (mSelectedFacetButton != null) {
mSelectedFacetButton.setSelected(false);
}
- CarFacetButton facetButton = mButtonsByPackage.get(packageName);
+ CarFacetButton facetButton = findFacetButtongByComponentName(taskInfo.topActivity);
+ if (facetButton == null) {
+ facetButton = mButtonsByPackage.get(packageName);
+ }
if (facetButton != null) {
facetButton.setSelected(true);
mSelectedFacetButton = facetButton;
@@ -83,6 +93,12 @@ public class CarFacetButtonController {
}
}
+ private CarFacetButton findFacetButtongByComponentName(ComponentName componentName) {
+ CarFacetButton button = mButtonsByComponentName.get(componentName.flattenToShortString());
+ return (button != null) ? button :
+ mButtonsByComponentName.get(componentName.flattenToString());
+ }
+
protected String getPackageCategory(String packageName) {
PackageManager pm = mContext.getPackageManager();
Set<String> supportedCategories = mButtonsByCategory.keySet();
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarView.java b/com/android/systemui/statusbar/car/CarNavigationBarView.java
index e73b1736..b2cef162 100644
--- a/com/android/systemui/statusbar/car/CarNavigationBarView.java
+++ b/com/android/systemui/statusbar/car/CarNavigationBarView.java
@@ -25,7 +25,9 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.keyguard.AlphaOptimizedImageButton;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
/**
* A custom navigation bar for the automotive use case.
@@ -52,6 +54,17 @@ class CarNavigationBarView extends LinearLayout {
if (mNotificationsButton != null) {
mNotificationsButton.setOnClickListener(this::onNotificationsClick);
}
+ View mStatusIcons = findViewById(R.id.statusIcons);
+ if (mStatusIcons != null) {
+ // Attach the controllers for Status icons such as wifi and bluetooth if the standard
+ // container is in the view.
+ StatusBarIconController.DarkIconManager mDarkIconManager =
+ new StatusBarIconController.DarkIconManager(
+ mStatusIcons.findViewById(R.id.statusIcons));
+ mDarkIconManager.setShouldLog(true);
+ Dependency.get(StatusBarIconController.class).addIconGroup(mDarkIconManager);
+ }
+
}
void setStatusBar(CarStatusBar carStatusBar) {
diff --git a/com/android/systemui/statusbar/car/CarNavigationButton.java b/com/android/systemui/statusbar/car/CarNavigationButton.java
index 0cdaec14..084c136f 100644
--- a/com/android/systemui/statusbar/car/CarNavigationButton.java
+++ b/com/android/systemui/statusbar/car/CarNavigationButton.java
@@ -3,7 +3,9 @@ package com.android.systemui.statusbar.car;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
+import android.os.UserHandle;
import android.util.AttributeSet;
+import android.util.Log;
import android.widget.ImageView;
import com.android.systemui.R;
@@ -17,23 +19,34 @@ import java.net.URISyntaxException;
*/
public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImageButton {
- private static final float SELECTED_ALPHA = 1;
- private static final float UNSELECTED_ALPHA = 0.7f;
-
+ private static final String TAG = "CarNavigationButton";
private Context mContext;
- private String mIntent = null;
- private String mLongIntent = null;
- private boolean mBroadcastIntent = false;
+ private String mIntent;
+ private String mLongIntent;
+ private boolean mBroadcastIntent;
private boolean mSelected = false;
+ private float mSelectedAlpha = 1f;
+ private float mUnselectedAlpha = 1f;
+ private int mSelectedIconResourceId;
+ private int mIconResourceId;
public CarNavigationButton(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
- TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CarNavigationButton);
+ TypedArray typedArray = context.obtainStyledAttributes(
+ attrs, R.styleable.CarNavigationButton);
mIntent = typedArray.getString(R.styleable.CarNavigationButton_intent);
mLongIntent = typedArray.getString(R.styleable.CarNavigationButton_longIntent);
mBroadcastIntent = typedArray.getBoolean(R.styleable.CarNavigationButton_broadcast, false);
+ mSelectedAlpha = typedArray.getFloat(
+ R.styleable.CarNavigationButton_selectedAlpha, mSelectedAlpha);
+ mUnselectedAlpha = typedArray.getFloat(
+ R.styleable.CarNavigationButton_unselectedAlpha, mUnselectedAlpha);
+ mIconResourceId = typedArray.getResourceId(
+ com.android.internal.R.styleable.ImageView_src, 0);
+ mSelectedIconResourceId = typedArray.getResourceId(
+ R.styleable.CarNavigationButton_selectedIcon, mIconResourceId);
}
@@ -45,17 +58,20 @@ public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImag
public void onFinishInflate() {
super.onFinishInflate();
setScaleType(ImageView.ScaleType.CENTER);
- setAlpha(UNSELECTED_ALPHA);
+ setAlpha(mUnselectedAlpha);
try {
if (mIntent != null) {
final Intent intent = Intent.parseUri(mIntent, Intent.URI_INTENT_SCHEME);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
setOnClickListener(v -> {
- if (mBroadcastIntent) {
- mContext.sendBroadcast(intent);
- return;
+ try {
+ if (mBroadcastIntent) {
+ mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+ return;
+ }
+ mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to launch intent", e);
}
- mContext.startActivity(intent);
});
}
} catch (URISyntaxException e) {
@@ -65,9 +81,13 @@ public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImag
try {
if (mLongIntent != null) {
final Intent intent = Intent.parseUri(mLongIntent, Intent.URI_INTENT_SCHEME);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
setOnLongClickListener(v -> {
- mContext.startActivity(intent);
+ try {
+ mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to launch intent", e);
+ }
+ // consume event either way
return true;
});
}
@@ -82,6 +102,7 @@ public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImag
public void setSelected(boolean selected) {
super.setSelected(selected);
mSelected = selected;
- setAlpha(mSelected ? SELECTED_ALPHA : UNSELECTED_ALPHA);
+ setAlpha(mSelected ? mSelectedAlpha : mUnselectedAlpha);
+ setImageResource(mSelected ? mSelectedIconResourceId : mIconResourceId);
}
}
diff --git a/com/android/systemui/statusbar/car/CarStatusBar.java b/com/android/systemui/statusbar/car/CarStatusBar.java
index 3fb11376..008794c1 100644
--- a/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -418,8 +418,7 @@ public class CarStatusBar extends StatusBar implements
Dependency.get(UserSwitcherController.class);
if (userSwitcherController.useFullscreenUserSwitcher()) {
mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
- userSwitcherController,
- mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub));
+ mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub), mContext);
} else {
super.createUserSwitcher();
}
diff --git a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
index bc353f2d..5a02d18c 100644
--- a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
+++ b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -18,14 +18,15 @@ package com.android.systemui.statusbar.car;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.content.res.Resources;
+import android.content.Context;
import android.view.View;
import android.view.ViewStub;
import android.widget.ProgressBar;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
import com.android.systemui.R;
import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
/**
* Manages the fullscreen user switcher.
@@ -33,42 +34,59 @@ import com.android.systemui.statusbar.policy.UserSwitcherController;
public class FullscreenUserSwitcher {
private final View mContainer;
private final View mParent;
- private final UserGridView mUserGridView;
- private final UserSwitcherController mUserSwitcherController;
+ private final UserGridRecyclerView mUserGridView;
private final ProgressBar mSwitchingUsers;
private final int mShortAnimDuration;
+ private final StatusBar mStatusBar;
private boolean mShowing;
- public FullscreenUserSwitcher(StatusBar statusBar,
- UserSwitcherController userSwitcherController,
- ViewStub containerStub) {
- mUserSwitcherController = userSwitcherController;
+ public FullscreenUserSwitcher(StatusBar statusBar, ViewStub containerStub, Context context) {
+ mStatusBar = statusBar;
mParent = containerStub.inflate();
mContainer = mParent.findViewById(R.id.container);
mUserGridView = mContainer.findViewById(R.id.user_grid);
- mUserGridView.init(statusBar, mUserSwitcherController, true /* overrideAlpha */);
- mUserGridView.setUserSelectionListener(record -> {
- if (!record.isCurrent) {
- toggleSwitchInProgress(true);
- }
- });
+ GridLayoutManager layoutManager = new GridLayoutManager(context,
+ context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col));
+ mUserGridView.setLayoutManager(layoutManager);
+ mUserGridView.buildAdapter();
+ mUserGridView.setUserSelectionListener(this::onUserSelected);
- PageIndicator pageIndicator = mContainer.findViewById(R.id.user_switcher_page_indicator);
- pageIndicator.setupWithViewPager(mUserGridView);
+ mShortAnimDuration = mContainer.getResources()
+ .getInteger(android.R.integer.config_shortAnimTime);
- Resources res = mContainer.getResources();
- mShortAnimDuration = res.getInteger(android.R.integer.config_shortAnimTime);
+ mSwitchingUsers = mParent.findViewById(R.id.switching_users);
+ }
- mContainer.findViewById(R.id.start_driving).setOnClickListener(v -> {
- automaticallySelectUser();
- });
+ public void show() {
+ if (mShowing) {
+ return;
+ }
+ mShowing = true;
+ mParent.setVisibility(View.VISIBLE);
+ }
- mSwitchingUsers = mParent.findViewById(R.id.switching_users);
+ public void hide() {
+ mShowing = false;
+ toggleSwitchInProgress(false);
+ mParent.setVisibility(View.GONE);
}
public void onUserSwitched(int newUserId) {
- mUserGridView.onUserSwitched(newUserId);
+ mParent.post(this::showOfflineAuthUi);
+ }
+
+ private void onUserSelected(UserGridRecyclerView.UserRecord record) {
+ if (record.mIsForeground) {
+ showOfflineAuthUi();
+ return;
+ }
+ toggleSwitchInProgress(true);
+ }
+
+ private void showOfflineAuthUi() {
+ mStatusBar.executeRunnableDismissingKeyguard(null/* runnable */, null /* cancelAction */,
+ true /* dismissShade */, true /* afterKeyguardGone */, true /* deferred */);
}
private void toggleSwitchInProgress(boolean inProgress) {
@@ -101,24 +119,4 @@ public class FullscreenUserSwitcher {
}
});
}
-
- public void show() {
- if (mShowing) {
- return;
- }
- mShowing = true;
- mParent.setVisibility(View.VISIBLE);
- }
-
- public void hide() {
- mShowing = false;
- toggleSwitchInProgress(false);
- mParent.setVisibility(View.GONE);
- }
-
- private void automaticallySelectUser() {
- // TODO: Switch according to some policy. This implementation just tries to drop the
- // keyguard for the current user.
- mUserGridView.showOfflineAuthUi();
- }
}
diff --git a/com/android/systemui/statusbar/car/UserGridRecyclerView.java b/com/android/systemui/statusbar/car/UserGridRecyclerView.java
new file mode 100644
index 00000000..5ad08acb
--- /dev/null
+++ b/com/android/systemui/statusbar/car/UserGridRecyclerView.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.systemui.statusbar.car;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.drawable.GradientDrawable;
+import android.os.AsyncTask;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.settingslib.users.UserManagerHelper;
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays a GridLayout with icons for the users in the system to allow switching between users.
+ * One of the uses of this is for the lock screen in auto.
+ */
+public class UserGridRecyclerView extends RecyclerView implements
+ UserManagerHelper.OnUsersUpdateListener {
+ private UserSelectionListener mUserSelectionListener;
+ private UserAdapter mAdapter;
+ private UserManagerHelper mUserManagerHelper;
+ private Context mContext;
+
+ public UserGridRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ super.setHasFixedSize(true);
+ mContext = context;
+ mUserManagerHelper = new UserManagerHelper(mContext);
+ }
+
+ /**
+ * Register listener for any update to the users
+ */
+ @Override
+ public void onFinishInflate() {
+ mUserManagerHelper.registerOnUsersUpdateListener(this);
+ }
+
+ /**
+ * Unregisters listener checking for any change to the users
+ */
+ @Override
+ public void onDetachedFromWindow() {
+ mUserManagerHelper.unregisterOnUsersUpdateListener();
+ }
+
+ /**
+ * Initializes the adapter that populates the grid layout
+ *
+ * @return the adapter
+ */
+ public void buildAdapter() {
+ List<UserRecord> userRecords = createUserRecords(mUserManagerHelper
+ .getAllUsers());
+ mAdapter = new UserAdapter(mContext, userRecords);
+ super.setAdapter(mAdapter);
+ }
+
+ private List<UserRecord> createUserRecords(List<UserInfo> userInfoList) {
+ List<UserRecord> userRecords = new ArrayList<>();
+ for (UserInfo userInfo : userInfoList) {
+ boolean isForeground = mUserManagerHelper.getForegroundUserId() == userInfo.id;
+ UserRecord record = new UserRecord(userInfo, false /* isGuest */,
+ false /* isAddUser */, isForeground);
+ userRecords.add(record);
+ }
+
+ // Add guest user record if the foreground user is not a guest
+ if (!mUserManagerHelper.foregroundUserIsGuestUser()) {
+ userRecords.add(addGuestUserRecord());
+ }
+
+ // Add add user record if the foreground user can add users
+ if (mUserManagerHelper.foregroundUserCanAddUsers()) {
+ userRecords.add(addUserRecord());
+ }
+
+ return userRecords;
+ }
+
+ /**
+ * Create guest user record
+ */
+ private UserRecord addGuestUserRecord() {
+ UserInfo userInfo = new UserInfo();
+ userInfo.name = mContext.getString(R.string.car_guest);
+ return new UserRecord(userInfo, true /* isGuest */,
+ false /* isAddUser */, false /* isForeground */);
+ }
+
+ /**
+ * Create add user record
+ */
+ private UserRecord addUserRecord() {
+ UserInfo userInfo = new UserInfo();
+ userInfo.name = mContext.getString(R.string.car_add_user);
+ return new UserRecord(userInfo, false /* isGuest */,
+ true /* isAddUser */, false /* isForeground */);
+ }
+
+ public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
+ mUserSelectionListener = userSelectionListener;
+ }
+
+ @Override
+ public void onUsersUpdate() {
+ mAdapter.clearUsers();
+ mAdapter.updateUsers(createUserRecords(mUserManagerHelper.getAllUsers()));
+ mAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Adapter to populate the grid layout with the available user profiles
+ */
+ public final class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserAdapterViewHolder> {
+
+ private final Context mContext;
+ private List<UserRecord> mUsers;
+ private final int mPodImageAvatarWidth;
+ private final int mPodImageAvatarHeight;
+ private final Resources mRes;
+ private final String mGuestName;
+ private final String mNewUserName;
+
+ public UserAdapter(Context context, List<UserRecord> users) {
+ mRes = context.getResources();
+ mContext = context;
+ updateUsers(users);
+ mPodImageAvatarWidth = mRes.getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_image_avatar_width);
+ mPodImageAvatarHeight = mRes.getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_image_avatar_height);
+ mGuestName = mRes.getString(R.string.car_guest);
+ mNewUserName = mRes.getString(R.string.car_new_user);
+ }
+
+ public void clearUsers() {
+ mUsers.clear();
+ }
+
+ public void updateUsers(List<UserRecord> users) {
+ mUsers = users;
+ }
+
+ @Override
+ public UserAdapterViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(mContext)
+ .inflate(R.layout.car_fullscreen_user_pod, parent, false);
+ view.setAlpha(1f);
+ view.bringToFront();
+ return new UserAdapterViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(UserAdapterViewHolder holder, int position) {
+ UserRecord userRecord = mUsers.get(position);
+ holder.mUserAvatarImageView.setImageBitmap(getDefaultUserIcon(userRecord));
+ holder.mUserNameTextView.setText(userRecord.mInfo.name);
+ holder.mView.setOnClickListener(v -> {
+ if (userRecord == null) {
+ return;
+ }
+
+ // Notify the listener which user was selected
+ if (mUserSelectionListener != null) {
+ mUserSelectionListener.onUserSelected(userRecord);
+ }
+
+ // If the user selects Guest, switch to Guest profile
+ if (userRecord.mIsGuest) {
+ mUserManagerHelper.switchToGuest(mGuestName);
+ return;
+ }
+
+ // If the user wants to add a user, start task to add new user
+ if (userRecord.mIsAddUser) {
+ new AddNewUserTask().execute(mNewUserName);
+ return;
+ }
+
+ // If the user doesn't want to be a guest or add a user, switch to the user selected
+ mUserManagerHelper.switchToUser(userRecord.mInfo);
+ });
+
+ }
+
+ private class AddNewUserTask extends AsyncTask<String, Void, UserInfo> {
+
+ @Override
+ protected UserInfo doInBackground(String... userNames) {
+ return mUserManagerHelper.createNewUser(userNames[0]);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ }
+
+ @Override
+ protected void onPostExecute(UserInfo user) {
+ if (user != null) {
+ mUserManagerHelper.switchToUser(user);
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mUsers.size();
+ }
+
+ /**
+ * Returns the default user icon. This icon is a circle with a letter in it. The letter is
+ * the first character in the username.
+ *
+ * @param record the profile of the user for which the icon should be created
+ */
+ private Bitmap getDefaultUserIcon(UserRecord record) {
+ CharSequence displayText;
+ boolean isAddUserText = false;
+ if (record.mIsAddUser) {
+ displayText = "+";
+ isAddUserText = true;
+ } else {
+ displayText = record.mInfo.name.subSequence(0, 1);
+ }
+ Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight,
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(out);
+
+ // Draw the circle background.
+ GradientDrawable shape = new GradientDrawable();
+ shape.setShape(GradientDrawable.RADIAL_GRADIENT);
+ shape.setGradientRadius(1.0f);
+ shape.setColor(mContext.getColor(R.color.car_user_switcher_no_user_image_bgcolor));
+ shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight);
+ shape.draw(canvas);
+
+ // Draw the letter in the center.
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setColor(mContext.getColor(R.color.car_user_switcher_no_user_image_fgcolor));
+ paint.setTextAlign(Align.CENTER);
+ if (isAddUserText) {
+ paint.setTextSize(mRes.getDimensionPixelSize(
+ R.dimen.car_touch_target_size));
+ } else {
+ paint.setTextSize(mRes.getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_icon_text_size));
+ }
+
+ Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
+ // The Y coordinate is measured by taking half the height of the pod, but that would
+ // draw the character putting the bottom of the font in the middle of the pod. To
+ // correct this, half the difference between the top and bottom distance metrics of the
+ // font gives the offset of the font. Bottom is a positive value, top is negative, so
+ // the different is actually a sum. The "half" operation is then factored out.
+ canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2,
+ (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint);
+
+ return out;
+ }
+
+ public class UserAdapterViewHolder extends RecyclerView.ViewHolder {
+
+ public ImageView mUserAvatarImageView;
+ public TextView mUserNameTextView;
+ public View mView;
+
+ public UserAdapterViewHolder(View view) {
+ super(view);
+ mView = view;
+ mUserAvatarImageView = (ImageView) view.findViewById(R.id.user_avatar);
+ mUserNameTextView = (TextView) view.findViewById(R.id.user_name);
+ }
+ }
+ }
+
+ /**
+ * Object wrapper class for the userInfo. Use it to distinguish if a profile is a
+ * guest profile, add user profile, or the foreground user.
+ */
+ public static final class UserRecord {
+
+ public final UserInfo mInfo;
+ public final boolean mIsGuest;
+ public final boolean mIsAddUser;
+ public final boolean mIsForeground;
+
+ public UserRecord(UserInfo userInfo, boolean isGuest, boolean isAddUser,
+ boolean isForeground) {
+ mInfo = userInfo;
+ mIsGuest = isGuest;
+ mIsAddUser = isAddUser;
+ mIsForeground = isForeground;
+ }
+ }
+
+ /**
+ * Listener used to notify when a user has been selected
+ */
+ interface UserSelectionListener {
+
+ void onUserSelected(UserRecord record);
+ }
+}
diff --git a/com/android/systemui/statusbar/car/UserGridView.java b/com/android/systemui/statusbar/car/UserGridView.java
deleted file mode 100644
index 1bd820db..00000000
--- a/com/android/systemui/statusbar/car/UserGridView.java
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.car;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.systemui.Dependency;
-import com.android.systemui.R;
-import com.android.systemui.qs.car.CarQSFragment;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.UserInfoController;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Vector;
-
-/**
- * Displays a ViewPager with icons for the users in the system to allow switching between users.
- * One of the uses of this is for the lock screen in auto.
- */
-public class UserGridView extends ViewPager implements
- UserInfoController.OnUserInfoChangedListener {
- private StatusBar mStatusBar;
- private UserSwitcherController mUserSwitcherController;
- private Adapter mAdapter;
- private UserSelectionListener mUserSelectionListener;
- private UserInfoController mUserInfoController;
- private Vector mUserContainers;
- private int mContainerWidth;
- private boolean mOverrideAlpha;
- private CarQSFragment.UserSwitchCallback mUserSwitchCallback;
-
- public UserGridView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public void init(StatusBar statusBar, UserSwitcherController userSwitcherController,
- boolean overrideAlpha) {
- mStatusBar = statusBar;
- mUserSwitcherController = userSwitcherController;
- mAdapter = new Adapter(mUserSwitcherController);
- mUserInfoController = Dependency.get(UserInfoController.class);
- mOverrideAlpha = overrideAlpha;
- // Whenever the container width changes, the containers must be refreshed. Instead of
- // doing an initial refreshContainers() to populate the containers, this listener will
- // refresh them on layout change because that affects how the users are split into
- // containers. Furthermore, at this point, the container width is unknown, so
- // refreshContainers() cannot populate any containers.
- addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- int newWidth = Math.max(left - right, right - left);
- if (mContainerWidth != newWidth) {
- mContainerWidth = newWidth;
- refreshContainers();
- }
- });
- }
-
- private void refreshContainers() {
- mUserContainers = new Vector();
-
- Context context = getContext();
- LayoutInflater inflater = LayoutInflater.from(context);
-
- for (int i = 0; i < mAdapter.getCount(); i++) {
- ViewGroup pods = (ViewGroup) inflater.inflate(
- R.layout.car_fullscreen_user_pod_container, null);
-
- int iconsPerPage = mAdapter.getIconsPerPage();
- int limit = Math.min(mUserSwitcherController.getUsers().size(), (i + 1) * iconsPerPage);
- for (int j = i * iconsPerPage; j < limit; j++) {
- View v = mAdapter.makeUserPod(inflater, context, j, pods);
- if (mOverrideAlpha) {
- v.setAlpha(1f);
- }
- pods.addView(v);
- // This is hacky, but the dividers on the pod container LinearLayout don't seem
- // to work for whatever reason. Instead, set a right margin on the pod if it's not
- // the right-most pod and there is more than one pod in the container.
- if (i < limit - 1 && limit > 1) {
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) v.getLayoutParams();
- params.setMargins(0, 0, getResources().getDimensionPixelSize(
- R.dimen.car_fullscreen_user_pod_margin_between), 0);
- v.setLayoutParams(params);
- }
- }
- mUserContainers.add(pods);
- }
-
- mAdapter = new Adapter(mUserSwitcherController);
- setAdapter(mAdapter);
- }
-
- @Override
- public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
- refreshContainers();
- }
-
- public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) {
- mUserSwitchCallback = callback;
- }
-
- public void onUserSwitched(int newUserId) {
- // Bring up security view after user switch is completed.
- post(this::showOfflineAuthUi);
- }
-
- public void setUserSelectionListener(UserSelectionListener userSelectionListener) {
- mUserSelectionListener = userSelectionListener;
- }
-
- public void setListening(boolean listening) {
- if (listening) {
- mUserInfoController.addCallback(this);
- } else {
- mUserInfoController.removeCallback(this);
- }
- }
-
- void showOfflineAuthUi() {
- // TODO: Show keyguard UI in-place.
- mStatusBar.executeRunnableDismissingKeyguard(null, null, true, true, true);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // Wrap content doesn't work in ViewPagers, so simulate the behavior in code.
- int height = 0;
- if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
- height = MeasureSpec.getSize(heightMeasureSpec);
- } else {
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- child.measure(widthMeasureSpec,
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- height = Math.max(child.getMeasuredHeight(), height);
- }
-
- // Respect the AT_MOST request from parent.
- if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
- height = Math.min(MeasureSpec.getSize(heightMeasureSpec), height);
- }
- }
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- /**
- * This is a ViewPager.PagerAdapter which deletegates the work to a
- * UserSwitcherController.BaseUserAdapter. Java doesn't support multiple inheritance so we have
- * to use composition instead to achieve the same goal since both the base classes are abstract
- * classes and not interfaces.
- */
- private final class Adapter extends PagerAdapter {
- private final int mPodWidth;
- private final int mPodMarginBetween;
- private final int mPodImageAvatarWidth;
- private final int mPodImageAvatarHeight;
-
- private final WrappedBaseUserAdapter mUserAdapter;
-
- public Adapter(UserSwitcherController controller) {
- super();
- mUserAdapter = new WrappedBaseUserAdapter(controller, this);
-
- Resources res = getResources();
- mPodWidth = res.getDimensionPixelSize(R.dimen.car_fullscreen_user_pod_width);
- mPodMarginBetween = res.getDimensionPixelSize(
- R.dimen.car_fullscreen_user_pod_margin_between);
- mPodImageAvatarWidth = res.getDimensionPixelSize(
- R.dimen.car_fullscreen_user_pod_image_avatar_width);
- mPodImageAvatarHeight = res.getDimensionPixelSize(
- R.dimen.car_fullscreen_user_pod_image_avatar_height);
- }
-
- @Override
- public void destroyItem(ViewGroup container, int position, Object object) {
- container.removeView((View) object);
- }
-
- private int getIconsPerPage() {
- // We need to know how many pods we need in this page. Each pod has its own width and
- // a margin between them. We can then divide the measured width of the parent by the
- // sum of pod width and margin to get the number of pods that will completely fit.
- // There is one less margin than the number of pods (eg. for 5 pods, there are 4
- // margins), so need to add the margin to the measured width to account for that.
- return (mContainerWidth + mPodMarginBetween) /
- (mPodWidth + mPodMarginBetween);
- }
-
- @Override
- public void finishUpdate(ViewGroup container) {
- if (mUserSwitchCallback != null) {
- mUserSwitchCallback.resetShowing();
- }
- }
-
- @Override
- public Object instantiateItem(ViewGroup container, int position) {
- if (position < mUserContainers.size()) {
- container.addView((View) mUserContainers.get(position));
- return mUserContainers.get(position);
- } else {
- return null;
- }
- }
-
- /**
- * Returns the default user icon. This icon is a circle with a letter in it. The letter is
- * the first character in the username.
- *
- * @param userName the username of the user for which the icon is to be created
- */
- private Bitmap getDefaultUserIcon(CharSequence userName) {
- CharSequence displayText = userName.subSequence(0, 1);
- Bitmap out = Bitmap.createBitmap(mPodImageAvatarWidth, mPodImageAvatarHeight,
- Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(out);
-
- // Draw the circle background.
- GradientDrawable shape = new GradientDrawable();
- shape.setShape(GradientDrawable.RADIAL_GRADIENT);
- shape.setGradientRadius(1.0f);
- shape.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_bgcolor));
- shape.setBounds(0, 0, mPodImageAvatarWidth, mPodImageAvatarHeight);
- shape.draw(canvas);
-
- // Draw the letter in the center.
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
- paint.setColor(getContext().getColor(R.color.car_user_switcher_no_user_image_fgcolor));
- paint.setTextAlign(Align.CENTER);
- paint.setTextSize(getResources().getDimensionPixelSize(
- R.dimen.car_fullscreen_user_pod_icon_text_size));
- Paint.FontMetricsInt metrics = paint.getFontMetricsInt();
- // The Y coordinate is measured by taking half the height of the pod, but that would
- // draw the character putting the bottom of the font in the middle of the pod. To
- // correct this, half the difference between the top and bottom distance metrics of the
- // font gives the offset of the font. Bottom is a positive value, top is negative, so
- // the different is actually a sum. The "half" operation is then factored out.
- canvas.drawText(displayText.toString(), mPodImageAvatarWidth / 2,
- (mPodImageAvatarHeight - (metrics.bottom + metrics.top)) / 2, paint);
-
- return out;
- }
-
- private View makeUserPod(LayoutInflater inflater, Context context,
- int position, ViewGroup parent) {
- final UserSwitcherController.UserRecord record = mUserAdapter.getItem(position);
- View view = inflater.inflate(R.layout.car_fullscreen_user_pod, parent, false);
-
- TextView nameView = view.findViewById(R.id.user_name);
- if (record != null) {
- nameView.setText(mUserAdapter.getName(context, record));
- view.setActivated(record.isCurrent);
- } else {
- nameView.setText(context.getString(R.string.unknown_user_label));
- }
-
- ImageView iconView = (ImageView) view.findViewById(R.id.user_avatar);
- if (record == null || (record.picture == null && !record.isAddUser)) {
- iconView.setImageBitmap(getDefaultUserIcon(nameView.getText()));
- } else if (record.isAddUser) {
- Drawable icon = context.getDrawable(R.drawable.ic_add_circle_qs);
- icon.setTint(context.getColor(R.color.car_user_switcher_no_user_image_bgcolor));
- iconView.setImageDrawable(icon);
- } else {
- iconView.setImageBitmap(record.picture);
- }
-
- iconView.setOnClickListener(v -> {
- if (record == null) {
- return;
- }
-
- if (mUserSelectionListener != null) {
- mUserSelectionListener.onUserSelected(record);
- }
-
- if (record.isCurrent) {
- showOfflineAuthUi();
- } else {
- mUserSwitcherController.switchTo(record);
- }
- });
-
- return view;
- }
-
- @Override
- public int getCount() {
- int iconsPerPage = getIconsPerPage();
- if (iconsPerPage == 0) {
- return 0;
- }
- return (int) Math.ceil((double) mUserAdapter.getCount() / getIconsPerPage());
- }
-
- public void refresh() {
- mUserAdapter.refresh();
- }
-
- @Override
- public boolean isViewFromObject(View view, Object object) {
- return view == object;
- }
- }
-
- private final class WrappedBaseUserAdapter extends UserSwitcherController.BaseUserAdapter {
- private final Adapter mContainer;
-
- public WrappedBaseUserAdapter(UserSwitcherController controller, Adapter container) {
- super(controller);
- mContainer = container;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- throw new UnsupportedOperationException("unused");
- }
-
- @Override
- public void notifyDataSetChanged() {
- super.notifyDataSetChanged();
- mContainer.notifyDataSetChanged();
- }
- }
-
- interface UserSelectionListener {
- void onUserSelected(UserSwitcherController.UserRecord record);
- };
-}
diff --git a/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
index 3bbfe3c1..b8bce951 100644
--- a/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
+++ b/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java
@@ -238,6 +238,7 @@ public class ActivityLaunchAnimator {
t.deferTransactionUntilSurface(app.leash, systemUiSurface,
systemUiSurface.getNextFrameNumber());
}
+ t.setEarlyWakeup();
t.apply();
}
diff --git a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index 75b31c5a..9fcb0905 100644
--- a/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -14,6 +14,7 @@
package com.android.systemui.statusbar.phone;
+import static android.app.StatusBarManager.DISABLE_CLOCK;
import static android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS;
import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO;
@@ -96,6 +97,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mSystemIconArea = mStatusBar.findViewById(R.id.system_icon_area);
mClockView = mStatusBar.findViewById(R.id.clock);
showSystemIconArea(false);
+ showClock(false);
initEmergencyCryptkeeperText();
initOperatorName();
}
@@ -163,6 +165,13 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
showNotificationIconArea(animate);
}
}
+ if ((diff1 & DISABLE_CLOCK) != 0) {
+ if ((state1 & DISABLE_CLOCK) != 0) {
+ hideClock(animate);
+ } else {
+ showClock(animate);
+ }
+ }
}
protected int adjustDisableFlags(int state) {
@@ -171,6 +180,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
&& shouldHideNotificationIcons()) {
state |= DISABLE_NOTIFICATION_ICONS;
state |= DISABLE_SYSTEM_INFO;
+ state |= DISABLE_CLOCK;
}
if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
if (mNetworkController.hasEmergencyCryptKeeperText()) {
@@ -195,11 +205,17 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
public void hideSystemIconArea(boolean animate) {
animateHide(mSystemIconArea, animate);
- animateHide(mClockView, animate);
}
public void showSystemIconArea(boolean animate) {
animateShow(mSystemIconArea, animate);
+ }
+
+ public void hideClock(boolean animate) {
+ animateHide(mClockView, animate);
+ }
+
+ public void showClock(boolean animate) {
animateShow(mClockView, animate);
}
diff --git a/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index df2b8171..60a3474b 100644
--- a/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -159,6 +159,11 @@ public class KeyguardBouncer {
*/
public void onFullyShown() {
mFalsingManager.onBouncerShown();
+ if (mKeyguardView == null) {
+ Log.wtf(TAG, "onFullyShown when view was null");
+ } else {
+ mKeyguardView.onResume();
+ }
}
/**
@@ -180,7 +185,6 @@ public class KeyguardBouncer {
@Override
public void run() {
mRoot.setVisibility(View.VISIBLE);
- mKeyguardView.onResume();
showPromptReason(mBouncerPromptReason);
final CharSequence customMessage = mCallback.consumeCustomMessage();
if (customMessage != null) {
@@ -296,7 +300,7 @@ public class KeyguardBouncer {
public boolean isShowing() {
return (mShowingSoon || (mRoot != null && mRoot.getVisibility() == View.VISIBLE))
- && mExpansion == 0;
+ && mExpansion == 0 && !isAnimatingAway();
}
/**
diff --git a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 3d7067d1..1fb1ddd5 100644
--- a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -99,6 +99,11 @@ public class KeyguardClockPositionAlgorithm {
private int mBurnInPreventionOffsetY;
/**
+ * Clock vertical padding when pulsing.
+ */
+ private int mPulsingPadding;
+
+ /**
* Doze/AOD transition amount.
*/
private float mDarkAmount;
@@ -109,9 +114,9 @@ public class KeyguardClockPositionAlgorithm {
private boolean mCurrentlySecure;
/**
- * If notification panel view currently has a touch.
+ * Dozing and receiving a notification (AOD notification.)
*/
- private boolean mTracking;
+ private boolean mPulsing;
/**
* Distance in pixels between the top of the screen and the first view of the bouncer.
@@ -130,11 +135,13 @@ public class KeyguardClockPositionAlgorithm {
R.dimen.burn_in_prevention_offset_x);
mBurnInPreventionOffsetY = res.getDimensionPixelSize(
R.dimen.burn_in_prevention_offset_y);
+ mPulsingPadding = res.getDimensionPixelSize(
+ R.dimen.widget_pulsing_bottom_padding);
}
public void setup(int minTopMargin, int maxShadeBottom, int notificationStackHeight,
float expandedHeight, float maxPanelHeight, int parentHeight, int keyguardStatusHeight,
- float dark, boolean secure, boolean tracking, int bouncerTop) {
+ float dark, boolean secure, boolean pulsing, int bouncerTop) {
mMinTopMargin = minTopMargin + mContainerTopPadding;
mMaxShadeBottom = maxShadeBottom;
mNotificationStackHeight = notificationStackHeight;
@@ -144,7 +151,7 @@ public class KeyguardClockPositionAlgorithm {
mKeyguardStatusHeight = keyguardStatusHeight;
mDarkAmount = dark;
mCurrentlySecure = secure;
- mTracking = tracking;
+ mPulsing = pulsing;
mBouncerTop = bouncerTop;
}
@@ -152,7 +159,7 @@ public class KeyguardClockPositionAlgorithm {
final int y = getClockY();
result.clockY = y;
result.clockAlpha = getClockAlpha(y);
- result.stackScrollerPadding = y + mKeyguardStatusHeight;
+ result.stackScrollerPadding = y + (mPulsing ? 0 : mKeyguardStatusHeight);
result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
}
@@ -194,9 +201,13 @@ public class KeyguardClockPositionAlgorithm {
private int getClockY() {
// Dark: Align the bottom edge of the clock at about half of the screen:
- final float clockYDark = getMaxClockY() + burnInPreventionOffsetY();
- final float clockYRegular = getExpandedClockPosition();
- final boolean hasEnoughSpace = mMinTopMargin + mKeyguardStatusHeight < mBouncerTop;
+ float clockYDark = getMaxClockY() + burnInPreventionOffsetY();
+ if (mPulsing) {
+ clockYDark -= mPulsingPadding;
+ }
+
+ float clockYRegular = getExpandedClockPosition();
+ boolean hasEnoughSpace = mMinTopMargin + mKeyguardStatusHeight < mBouncerTop;
float clockYTarget = mCurrentlySecure && hasEnoughSpace ?
mMinTopMargin : -mKeyguardStatusHeight;
diff --git a/android/security/keystore/BadCertificateFormatException.java b/com/android/systemui/statusbar/phone/KeyguardDismissHandler.java
index c51b7737..759a0d17 100644
--- a/android/security/keystore/BadCertificateFormatException.java
+++ b/com/android/systemui/statusbar/phone/KeyguardDismissHandler.java
@@ -11,17 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
-package android.security.keystore;
+package com.android.systemui.statusbar.phone;
-/**
- * @deprecated Use {@link android.security.keystore.recovery.BadCertificateFormatException}.
- * @hide
- */
-public class BadCertificateFormatException extends RecoveryControllerException {
- public BadCertificateFormatException(String msg) {
- super(msg);
- }
+import android.annotation.Nullable;
+
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
+
+
+/** Executes actions that require the screen to be unlocked. */
+public interface KeyguardDismissHandler {
+ /** Executes an action that requres the screen to be unlocked. */
+ void dismissKeyguardThenExecute(
+ OnDismissAction action, @Nullable Runnable cancelAction, boolean afterKeyguardGone);
}
diff --git a/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java b/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
new file mode 100644
index 00000000..c38b0b63
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/KeyguardDismissUtil.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.android.systemui.statusbar.phone;
+
+import android.util.Log;
+
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
+
+/**
+ * Executes actions that require the screen to be unlocked. Delegates the actual handling to an
+ * implementation passed via {@link #setDismissHandler}.
+ */
+public class KeyguardDismissUtil implements KeyguardDismissHandler {
+ private static final String TAG = "KeyguardDismissUtil";
+
+ private volatile KeyguardDismissHandler mDismissHandler;
+
+ /** Sets the actual {@link DismissHandler} implementation. */
+ public void setDismissHandler(KeyguardDismissHandler dismissHandler) {
+ mDismissHandler = dismissHandler;
+ }
+
+ /**
+ * Executes an action that requres the screen to be unlocked.
+ *
+ * <p>Must be called after {@link #setDismissHandler}.
+ */
+ @Override
+ public void dismissKeyguardThenExecute(
+ OnDismissAction action, Runnable cancelAction, boolean afterKeyguardGone) {
+ KeyguardDismissHandler dismissHandler = mDismissHandler;
+ if (dismissHandler == null) {
+ Log.wtf(TAG, "KeyguardDismissHandler not set.");
+ action.onDismiss();
+ return;
+ }
+ dismissHandler.dismissKeyguardThenExecute(action, cancelAction, afterKeyguardGone);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index 58f8baa6..ca6d5968 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -103,6 +103,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
/**
* Fragment containing the NavigationBarFragment. Contains logic for what happens
@@ -1109,8 +1110,11 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
// Only hide the icon if the top task changes its requestedOrientation
// Launcher can alter its requestedOrientation while it's not on top, don't hide on this
- final boolean top = ActivityManagerWrapper.getInstance().getRunningTask().id == taskId;
- if (top) setRotateSuggestionButtonState(false);
+ Optional.ofNullable(ActivityManagerWrapper.getInstance())
+ .map(ActivityManagerWrapper::getRunningTask)
+ .ifPresent(a -> {
+ if (a.id == taskId) setRotateSuggestionButtonState(false);
+ });
}
}
diff --git a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 98942353..91cf8f08 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -25,6 +25,7 @@ import android.view.Display;
import android.view.Display.Mode;
import android.view.Gravity;
import android.view.LayoutInflater;
+import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -80,6 +81,7 @@ public class NavigationBarInflaterView extends FrameLayout
private static final String WEIGHT_CENTERED_SUFFIX = "WC";
private final List<NavBarButtonProvider> mPlugins = new ArrayList<>();
+ private final Display mDisplay;
protected LayoutInflater mLayoutInflater;
protected LayoutInflater mLandscapeInflater;
@@ -99,9 +101,9 @@ public class NavigationBarInflaterView extends FrameLayout
public NavigationBarInflaterView(Context context, AttributeSet attrs) {
super(context, attrs);
createInflaters();
- Display display = ((WindowManager)
+ mDisplay = ((WindowManager)
context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
- Mode displayMode = display.getMode();
+ Mode displayMode = mDisplay.getMode();
isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
}
@@ -173,6 +175,17 @@ public class NavigationBarInflaterView extends FrameLayout
}
}
+ public void updateButtonDispatchersCurrentView() {
+ if (mButtonDispatchers != null) {
+ final int rotation = mDisplay.getRotation();
+ final View view = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+ ? mRot0 : mRot90;
+ for (int i = 0; i < mButtonDispatchers.size(); i++) {
+ mButtonDispatchers.valueAt(i).setCurrentView(view);
+ }
+ }
+ }
+
public void setAlternativeOrder(boolean alternativeOrder) {
if (alternativeOrder != mAlternativeOrder) {
mAlternativeOrder = alternativeOrder;
@@ -239,6 +252,8 @@ public class NavigationBarInflaterView extends FrameLayout
inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);
inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
+
+ updateButtonDispatchersCurrentView();
}
private void addGravitySpacer(LinearLayout layout) {
diff --git a/com/android/systemui/statusbar/phone/NavigationBarView.java b/com/android/systemui/statusbar/phone/NavigationBarView.java
index f216695c..d79f308e 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -39,7 +39,6 @@ import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.os.SystemProperties;
-import android.os.VibrationEffect;
import android.support.annotation.ColorInt;
import android.util.AttributeSet;
import android.util.Log;
@@ -69,7 +68,6 @@ import com.android.systemui.recents.RecentsOnboarding;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.NavigationBarCompat;
import com.android.systemui.stackdivider.Divider;
-import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.DeadZone;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
import com.android.systemui.statusbar.policy.TintedKeyButtonDrawable;
@@ -124,7 +122,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private TintedKeyButtonDrawable mRotateSuggestionIcon;
private GestureHelper mGestureHelper;
- private DeadZone mDeadZone;
+ private final DeadZone mDeadZone;
private final NavigationBarTransitions mBarTransitions;
private final OverviewProxyService mOverviewProxyService;
@@ -150,7 +148,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private Divider mDivider;
private RecentsOnboarding mRecentsOnboarding;
private NotificationPanelView mPanelView;
- private final VibratorHelper mVibratorHelper;
private int mRotateBtnStyle = R.style.RotateButtonCCWStart90;
@@ -246,7 +243,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mOverviewProxyService = Dependency.get(OverviewProxyService.class);
mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService);
- mVibratorHelper = Dependency.get(VibratorHelper.class);
mConfiguration = new Configuration();
mConfiguration.updateFrom(context.getResources().getConfiguration());
@@ -263,6 +259,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
new ButtonDispatcher(R.id.accessibility_button));
mButtonDispatchers.put(R.id.rotate_suggestion,
new ButtonDispatcher(R.id.rotate_suggestion));
+ mDeadZone = new DeadZone(this);
}
public BarTransitions getBarTransitions() {
@@ -297,6 +294,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mDeadZone.onTouchEvent(event)) {
+ // Consumed the touch event
+ return true;
+ }
switch (event.getActionMasked()) {
case ACTION_DOWN:
int x = (int) event.getX();
@@ -309,9 +310,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
} else if (mRecentsButtonBounds.contains(x, y)) {
mDownHitTarget = HIT_TARGET_OVERVIEW;
}
-
- // Vibrate tick whenever down occurs on navigation bar
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
break;
}
return mGestureHelper.onInterceptTouchEvent(event);
@@ -319,6 +317,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
@Override
public boolean onTouchEvent(MotionEvent event) {
+ if (mDeadZone.onTouchEvent(event)) {
+ // Consumed the touch event
+ return true;
+ }
if (mGestureHelper.onTouchEvent(event)) {
return true;
}
@@ -389,7 +391,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
public boolean isQuickScrubEnabled() {
return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", true)
- && mOverviewProxyService.getProxy() != null && isOverviewEnabled()
+ && mOverviewProxyService.isEnabled() && isOverviewEnabled()
&& ((mOverviewProxyService.getInteractionFlags() & FLAG_DISABLE_QUICK_SCRUB) == 0);
}
@@ -587,7 +589,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
// recents buttons when disconnected from launcher service in screen pinning mode,
// as they are used for exiting.
final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
- if (mOverviewProxyService.getProxy() != null) {
+ if (mOverviewProxyService.isEnabled()) {
// Use interaction flags to show/hide navigation buttons but will be shown if required
// to exit screen pinning.
final int flags = mOverviewProxyService.getInteractionFlags();
@@ -810,14 +812,12 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
if (mGestureHelper != null) {
mGestureHelper.onDarkIntensityChange(intensity);
}
- if (mRecentsOnboarding != null) {
- mRecentsOnboarding.setContentDarkIntensity(intensity);
- }
}
@Override
protected void onDraw(Canvas canvas) {
mGestureHelper.onDraw(canvas);
+ mDeadZone.onDraw(canvas);
super.onDraw(canvas);
}
@@ -828,6 +828,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
updateButtonLocationOnScreen(getHomeButton(), mHomeButtonBounds);
updateButtonLocationOnScreen(getRecentsButton(), mRecentsButtonBounds);
mGestureHelper.onLayout(changed, left, top, right, bottom);
+ mRecentsOnboarding.setNavBarHeight(getMeasuredHeight());
}
private void updateButtonLocationOnScreen(ButtonDispatcher button, Rect buttonBounds) {
@@ -870,9 +871,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mCurrentView = mRotatedViews[rot];
mCurrentView.setVisibility(View.VISIBLE);
mNavigationInflaterView.setAlternativeOrder(rot == Surface.ROTATION_90);
- for (int i = 0; i < mButtonDispatchers.size(); i++) {
- mButtonDispatchers.valueAt(i).setCurrentView(mCurrentView);
- }
+ mNavigationInflaterView.updateButtonDispatchersCurrentView();
updateLayoutTransitionsEnabled();
mCurrentRotation = rot;
}
@@ -889,10 +888,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
public void reorient() {
updateCurrentView();
- mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
-
((NavigationBarFrame) getRootView()).setDeadZone(mDeadZone);
- mDeadZone.setDisplayRotation(mCurrentRotation);
+ mDeadZone.onConfigurationChanged(mCurrentRotation);
// force the low profile & disabled states into compliance
mBarTransitions.init();
diff --git a/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index b6a11f71..6bc19ea6 100644
--- a/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -125,7 +125,14 @@ public class NotificationIconAreaController implements DarkReceiver {
} else {
mTintArea.set(tintArea);
}
- mIconTint = iconTint;
+ if (mNotificationIconArea != null) {
+ if (DarkIconDispatcher.isInArea(tintArea, mNotificationIconArea)) {
+ mIconTint = iconTint;
+ }
+ } else {
+ mIconTint = iconTint;
+ }
+
applyNotificationIconsTint();
}
diff --git a/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 55174349..8c257fe2 100644
--- a/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -151,6 +151,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
private ArrayMap<String, ArrayList<StatusBarIcon>> mReplacingIcons;
// Keep track of the last visible icon so collapsed container can report on its location
private IconState mLastVisibleIconState;
+ private IconState mFirstVisibleIconState;
private float mVisualOverflowStart;
// Keep track of overflow in range [0, 3]
private int mNumDots;
@@ -159,7 +160,6 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
private int[] mAbsolutePosition = new int[2];
private View mIsolatedIconForAnimation;
-
public NotificationIconContainer(Context context, AttributeSet attrs) {
super(context, attrs);
initDimens();
@@ -192,10 +192,15 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
paint.setColor(Color.BLUE);
canvas.drawLine(end, 0, end, height, paint);
- paint.setColor(Color.BLACK);
+ paint.setColor(Color.GREEN);
int lastIcon = (int) mLastVisibleIconState.xTranslation;
canvas.drawLine(lastIcon, 0, lastIcon, height, paint);
+ if (mFirstVisibleIconState != null) {
+ int firstIcon = (int) mFirstVisibleIconState.xTranslation;
+ canvas.drawLine(firstIcon, 0, firstIcon, height, paint);
+ }
+
paint.setColor(Color.RED);
canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint);
@@ -210,6 +215,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
super.onConfigurationChanged(newConfig);
initDimens();
}
+
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
float centerY = getHeight() / 2.0f;
@@ -364,11 +370,15 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
float layoutEnd = getLayoutEnd();
float overflowStart = getMaxOverflowStart();
mVisualOverflowStart = 0;
+ mFirstVisibleIconState = null;
boolean hasAmbient = mSpeedBumpIndex != -1 && mSpeedBumpIndex < getChildCount();
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
IconState iconState = mIconStates.get(view);
iconState.xTranslation = translationX;
+ if (mFirstVisibleIconState == null) {
+ mFirstVisibleIconState = iconState;
+ }
boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
&& iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
boolean noOverflowAfter = i == childCount - 1;
@@ -417,10 +427,16 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
} else if (childCount > 0) {
View lastChild = getChildAt(childCount - 1);
mLastVisibleIconState = mIconStates.get(lastChild);
+ mFirstVisibleIconState = mIconStates.get(getChildAt(0));
}
boolean center = mDark;
if (center && translationX < getLayoutEnd()) {
- float delta = (getLayoutEnd() - translationX) / 2;
+ float initialTranslation =
+ mFirstVisibleIconState == null ? 0 : mFirstVisibleIconState.xTranslation;
+ float contentWidth = getFinalTranslationX() - initialTranslation;
+ float availableSpace = getLayoutEnd() - getActualPaddingStart();
+ float delta = (availableSpace - contentWidth) / 2;
+
if (firstOverflowIndex != -1) {
// If we have an overflow, only count those half for centering because the dots
// don't have a lot of visual weight.
diff --git a/com/android/systemui/statusbar/phone/NotificationPanelView.java b/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 27ca0d11..351633b5 100644
--- a/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -238,6 +238,7 @@ public class NotificationPanelView extends PanelView implements
private boolean mIsFullWidth;
private float mDarkAmount;
private float mDarkAmountTarget;
+ private boolean mPulsing;
private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
private boolean mNoVisibleNotifications = true;
private ValueAnimator mDarkAnimator;
@@ -477,7 +478,7 @@ public class NotificationPanelView extends PanelView implements
mKeyguardStatusView.getHeight(),
mDarkAmount,
mStatusBar.isKeyguardCurrentlySecure(),
- mTracking,
+ mPulsing,
mBouncerTop);
mClockPositionAlgorithm.run(mClockPositionResult);
if (animate || mClockAnimator != null) {
@@ -659,6 +660,14 @@ public class NotificationPanelView extends PanelView implements
expand(true /* animate */);
}
+ public void expandWithoutQs() {
+ if (isQsExpanded()) {
+ flingSettings(0 /* velocity */, false /* expand */);
+ } else {
+ expand(true /* animate */);
+ }
+ }
+
@Override
public void fling(float vel, boolean expand) {
GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
@@ -2681,14 +2690,8 @@ public class NotificationPanelView extends PanelView implements
positionClockAndNotifications();
}
- public void setNoVisibleNotifications(boolean noNotifications) {
- mNoVisibleNotifications = noNotifications;
- if (mQs != null) {
- mQs.setHasNotifications(!noNotifications);
- }
- }
-
public void setPulsing(boolean pulsing) {
+ mPulsing = pulsing;
mKeyguardStatusView.setPulsing(pulsing);
positionClockAndNotifications();
mNotificationStackScroller.setPulsing(pulsing, mKeyguardStatusView.getLocationOnScreen()[1]
diff --git a/com/android/systemui/statusbar/phone/PanelView.java b/com/android/systemui/statusbar/phone/PanelView.java
index 04cb620b..304a4997 100644
--- a/com/android/systemui/statusbar/phone/PanelView.java
+++ b/com/android/systemui/statusbar/phone/PanelView.java
@@ -488,7 +488,7 @@ public abstract class PanelView extends FrameLayout {
mUpdateFlingVelocity = vel;
}
} else if (mPanelClosedOnDown && !mHeadsUpManager.hasPinnedHeadsUp() && !mTracking
- && !mStatusBar.isBouncerShowing()) {
+ && !mStatusBar.isBouncerShowing() && !mStatusBar.isKeyguardFadingAway()) {
long timePassed = SystemClock.uptimeMillis() - mDownTime;
if (timePassed < ViewConfiguration.getLongPressTimeout()) {
// Lets show the user that he can actually expand the panel
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
index 12bdfc67..a7d5acaa 100644
--- a/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarTransitions.java
@@ -43,10 +43,9 @@ public final class PhoneStatusBarTransitions extends BarTransitions {
}
public void init() {
- mLeftSide = mView.findViewById(R.id.notification_icon_area);
+ mLeftSide = mView.findViewById(R.id.status_bar_left_side);
mStatusIcons = mView.findViewById(R.id.statusIcons);
mBattery = mView.findViewById(R.id.battery);
- mClock = mView.findViewById(R.id.clock);
applyModeBackground(-1, getMode(), false /*animate*/);
applyMode(getMode(), false /*animate*/);
}
@@ -89,8 +88,7 @@ public final class PhoneStatusBarTransitions extends BarTransitions {
anims.playTogether(
animateTransitionTo(mLeftSide, newAlpha),
animateTransitionTo(mStatusIcons, newAlpha),
- animateTransitionTo(mBattery, newAlphaBC),
- animateTransitionTo(mClock, newAlphaBC)
+ animateTransitionTo(mBattery, newAlphaBC)
);
if (isLightsOut(mode)) {
anims.setDuration(LIGHTS_OUT_DURATION);
@@ -101,7 +99,6 @@ public final class PhoneStatusBarTransitions extends BarTransitions {
mLeftSide.setAlpha(newAlpha);
mStatusIcons.setAlpha(newAlpha);
mBattery.setAlpha(newAlphaBC);
- mClock.setAlpha(newAlphaBC);
}
}
} \ No newline at end of file
diff --git a/com/android/systemui/statusbar/phone/QuickStepController.java b/com/android/systemui/statusbar/phone/QuickStepController.java
index a51cd937..d3790d44 100644
--- a/com/android/systemui/statusbar/phone/QuickStepController.java
+++ b/com/android/systemui/statusbar/phone/QuickStepController.java
@@ -51,6 +51,10 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
import static com.android.systemui.OverviewProxyService.TAG_OPS;
import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_HOME;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_SCRUB_DRAG_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_SCRUB_TOUCH_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_DRAG_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_TOUCH_SLOP_PX;
/**
* Class to detect gestures on the navigation bar and implement quick scrub.
@@ -69,6 +73,7 @@ public class QuickStepController implements GestureHelper {
private float mTranslation;
private int mTouchDownX;
private int mTouchDownY;
+ private boolean mDragScrubActive;
private boolean mDragPositive;
private boolean mIsVertical;
private boolean mIsRTL;
@@ -82,7 +87,6 @@ public class QuickStepController implements GestureHelper {
private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
private final Rect mTrackRect = new Rect();
private final Paint mTrackPaint = new Paint();
- private final int mScrollTouchSlop;
private final OverviewProxyService mOverviewEventSender;
private final int mTrackThickness;
private final int mTrackPadding;
@@ -115,6 +119,7 @@ public class QuickStepController implements GestureHelper {
@Override
public void onAnimationEnd(Animator animation) {
mQuickScrubActive = false;
+ mDragScrubActive = false;
mTranslation = 0;
mQuickScrubEndAnimator.setCurrentPlayTime(mQuickScrubEndAnimator.getDuration());
mHomeButtonView = null;
@@ -123,7 +128,6 @@ public class QuickStepController implements GestureHelper {
public QuickStepController(Context context) {
mContext = context;
- mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mOverviewEventSender = Dependency.get(OverviewProxyService.class);
mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
@@ -168,8 +172,8 @@ public class QuickStepController implements GestureHelper {
}
private boolean handleTouchEvent(MotionEvent event) {
- if (!mNavigationBarView.isQuickScrubEnabled()
- && !mNavigationBarView.isQuickStepSwipeUpEnabled()) {
+ if (mOverviewEventSender.getProxy() == null || (!mNavigationBarView.isQuickScrubEnabled()
+ && !mNavigationBarView.isQuickStepSwipeUpEnabled())) {
mNavigationBarView.getHomeButton().setDelayTouchFeedback(false /* delay */);
return false;
}
@@ -177,8 +181,8 @@ public class QuickStepController implements GestureHelper {
final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
final boolean homePressed = mNavigationBarView.getDownHitTarget() == HIT_TARGET_HOME;
- int action = event.getAction();
- switch (action & MotionEvent.ACTION_MASK) {
+ int action = event.getActionMasked();
+ switch (action) {
case MotionEvent.ACTION_DOWN: {
int x = (int) event.getX();
int y = (int) event.getY();
@@ -199,28 +203,29 @@ public class QuickStepController implements GestureHelper {
break;
}
case MotionEvent.ACTION_MOVE: {
- if (mQuickStepStarted || !mAllowGestureDetection){
+ if (mQuickStepStarted || !mAllowGestureDetection || mHomeButtonView == null){
break;
}
int x = (int) event.getX();
int y = (int) event.getY();
int xDiff = Math.abs(x - mTouchDownX);
int yDiff = Math.abs(y - mTouchDownY);
- boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
- boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
- boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
+
+ boolean exceededScrubTouchSlop, exceededSwipeUpTouchSlop, exceededScrubDragSlop;
int pos, touchDown, offset, trackSize;
if (mIsVertical) {
- exceededTouchSlop = exceededTouchSlopY;
- exceededPerpendicularTouchSlop = exceededTouchSlopX;
+ exceededScrubTouchSlop = yDiff > QUICK_STEP_TOUCH_SLOP_PX && yDiff > xDiff;
+ exceededSwipeUpTouchSlop = xDiff > QUICK_STEP_DRAG_SLOP_PX && xDiff > yDiff;
+ exceededScrubDragSlop = yDiff > QUICK_SCRUB_DRAG_SLOP_PX && yDiff > xDiff;
pos = y;
touchDown = mTouchDownY;
offset = pos - mTrackRect.top;
trackSize = mTrackRect.height();
} else {
- exceededTouchSlop = exceededTouchSlopX;
- exceededPerpendicularTouchSlop = exceededTouchSlopY;
+ exceededScrubTouchSlop = xDiff > QUICK_STEP_TOUCH_SLOP_PX && xDiff > yDiff;
+ exceededSwipeUpTouchSlop = yDiff > QUICK_SCRUB_TOUCH_SLOP_PX && yDiff > xDiff;
+ exceededScrubDragSlop = xDiff > QUICK_SCRUB_DRAG_SLOP_PX && xDiff > yDiff;
pos = x;
touchDown = mTouchDownX;
offset = pos - mTrackRect.left;
@@ -228,7 +233,7 @@ public class QuickStepController implements GestureHelper {
}
// Decide to start quickstep if dragging away from the navigation bar, otherwise in
// the parallel direction, decide to start quickscrub. Only one may run.
- if (!mQuickScrubActive && exceededPerpendicularTouchSlop) {
+ if (!mQuickScrubActive && exceededSwipeUpTouchSlop) {
if (mNavigationBarView.isQuickStepSwipeUpEnabled()) {
startQuickStep(event);
}
@@ -244,29 +249,38 @@ public class QuickStepController implements GestureHelper {
offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
}
- // Control the button movement
- if (!mQuickScrubActive && exceededTouchSlop) {
- boolean allowDrag = !mDragPositive
- ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
- if (allowDrag) {
+ final boolean allowDrag = !mDragPositive
+ ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
+ if (allowDrag) {
+ // Passing the drag slop is for visual feedback and will not initiate anything
+ if (!mDragScrubActive && exceededScrubDragSlop) {
mDownOffset = offset;
+ mDragScrubActive = true;
+ }
+
+ // Passing the drag slop then touch slop will start quick step
+ if (!mQuickScrubActive && exceededScrubTouchSlop) {
homeButton.abortCurrentGesture();
startQuickScrub();
}
}
- if (mQuickScrubActive && (mDragPositive && offset >= 0
+
+ if ((mQuickScrubActive || mDragScrubActive) && (mDragPositive && offset >= 0
|| !mDragPositive && offset <= 0)) {
- float scrubFraction = Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
mTranslation = !mDragPositive
- ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
- : Utilities.clamp(offset - mDownOffset, 0, trackSize);
- try {
- mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
- if (DEBUG_OVERVIEW_PROXY) {
- Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+ ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
+ : Utilities.clamp(offset - mDownOffset, 0, trackSize);
+ if (mQuickScrubActive) {
+ float scrubFraction =
+ Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
+ try {
+ mOverviewEventSender.getProxy().onQuickScrubProgress(scrubFraction);
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send progress of quick scrub.", e);
}
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to send progress of quick scrub.", e);
}
if (mIsVertical) {
mHomeButtonView.setTranslationY(mTranslation);
@@ -283,7 +297,9 @@ public class QuickStepController implements GestureHelper {
}
// Proxy motion events to launcher if not handled by quick scrub
- if (!mQuickScrubActive && mAllowGestureDetection) {
+ // Proxy motion events up/cancel that would be sent after long press on any nav button
+ if (!mQuickScrubActive && (mAllowGestureDetection || action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP)) {
proxyMotionEvents(event);
}
return mQuickScrubActive || mQuickStepStarted;
@@ -370,10 +386,14 @@ public class QuickStepController implements GestureHelper {
mOverviewEventSender.notifyQuickStepStarted();
mNavigationBarView.getHomeButton().abortCurrentGesture();
mHandler.removeCallbacksAndMessages(null);
+
+ if (mDragScrubActive) {
+ animateEnd();
+ }
}
private void startQuickScrub() {
- if (!mQuickScrubActive) {
+ if (!mQuickScrubActive && mDragScrubActive) {
mQuickScrubActive = true;
mLightTrackColor = mContext.getColor(R.color.quick_step_track_background_light);
mDarkTrackColor = mContext.getColor(R.color.quick_step_track_background_dark);
@@ -391,15 +411,17 @@ public class QuickStepController implements GestureHelper {
}
private void endQuickScrub(boolean animate) {
- if (mQuickScrubActive) {
+ if (mQuickScrubActive || mDragScrubActive) {
animateEnd();
- try {
- mOverviewEventSender.getProxy().onQuickScrubEnd();
- if (DEBUG_OVERVIEW_PROXY) {
- Log.d(TAG_OPS, "Quick Scrub End");
+ if (mQuickScrubActive) {
+ try {
+ mOverviewEventSender.getProxy().onQuickScrubEnd();
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub End");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send end of quick scrub.", e);
}
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to send end of quick scrub.", e);
}
}
if (mHomeButtonView != null && !animate) {
diff --git a/com/android/systemui/statusbar/phone/ScrimController.java b/com/android/systemui/statusbar/phone/ScrimController.java
index 2c025b5e..cc143bb8 100644
--- a/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/com/android/systemui/statusbar/phone/ScrimController.java
@@ -68,8 +68,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
private static final String TAG = "ScrimController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ /**
+ * General scrim animation duration.
+ */
public static final long ANIMATION_DURATION = 220;
-
+ /**
+ * Longer duration, currently only used when going to AOD.
+ */
+ public static final long ANIMATION_DURATION_LONG = 1000;
/**
* When both scrims have 0 alpha.
*/
@@ -85,7 +91,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
/**
* Default alpha value for most scrims.
*/
- public static final float GRADIENT_SCRIM_ALPHA = 0.45f;
+ public static final float GRADIENT_SCRIM_ALPHA = 0.70f;
/**
* A scrim varies its opacity based on a busyness factor, for example
* how many notifications are currently visible.
@@ -105,7 +111,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
private final Context mContext;
protected final ScrimView mScrimBehind;
protected final ScrimView mScrimInFront;
- private final LightBarController mLightBarController;
private final UnlockMethodCache mUnlockMethodCache;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private final DozeParameters mDozeParameters;
@@ -139,6 +144,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
private int mCurrentBehindTint;
private boolean mWallpaperVisibilityTimedOut;
private int mScrimsVisibility;
+ private final Consumer<GradientColors> mScrimInFrontColorListener;
+ private final Consumer<Float> mScrimBehindAlphaListener;
private final Consumer<Integer> mScrimVisibleListener;
private boolean mBlankScreen;
private boolean mScreenBlankingCallbackCalled;
@@ -155,17 +162,20 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
private boolean mWakeLockHeld;
private boolean mKeyguardOccluded;
- public ScrimController(LightBarController lightBarController, ScrimView scrimBehind,
- ScrimView scrimInFront, Consumer<Integer> scrimVisibleListener,
- DozeParameters dozeParameters, AlarmManager alarmManager) {
+ public ScrimController(ScrimView scrimBehind, ScrimView scrimInFront,
+ Consumer<Float> scrimBehindAlphaListener,
+ Consumer<GradientColors> scrimInFrontColorListener,
+ Consumer<Integer> scrimVisibleListener, DozeParameters dozeParameters,
+ AlarmManager alarmManager) {
mScrimBehind = scrimBehind;
mScrimInFront = scrimInFront;
+ mScrimBehindAlphaListener = scrimBehindAlphaListener;
+ mScrimInFrontColorListener = scrimInFrontColorListener;
mScrimVisibleListener = scrimVisibleListener;
mContext = scrimBehind.getContext();
mUnlockMethodCache = UnlockMethodCache.getInstance(mContext);
mDarkenWhileDragging = !mUnlockMethodCache.canSkipBouncer();
mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
- mLightBarController = lightBarController;
mScrimBehindAlphaResValue = mContext.getResources().getFloat(R.dimen.scrim_behind_alpha);
mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
"hide_aod_wallpaper", new Handler());
@@ -190,6 +200,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
}
mState = ScrimState.UNINITIALIZED;
+ mScrimBehind.setDefaultFocusHighlightEnabled(false);
+ mScrimInFront.setDefaultFocusHighlightEnabled(false);
+
updateScrims();
}
@@ -361,6 +374,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
setOrAdaptCurrentAnimation(mScrimBehind);
setOrAdaptCurrentAnimation(mScrimInFront);
+
+ mScrimBehindAlphaListener.accept(mScrimBehind.getViewAlpha());
}
}
@@ -389,7 +404,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
// Darken scrim as you pull down the shade when unlocked
float behindFraction = getInterpolatedFraction();
behindFraction = (float) Math.pow(behindFraction, 0.8f);
- mCurrentBehindAlpha = behindFraction * mScrimBehindAlphaKeyguard;
+ mCurrentBehindAlpha = behindFraction * GRADIENT_SCRIM_ALPHA_BUSY;
mCurrentInFrontAlpha = 0;
} else if (mState == ScrimState.KEYGUARD) {
// Either darken of make the scrim transparent when you
@@ -469,7 +484,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
float minOpacity = ColorUtils.calculateMinimumBackgroundAlpha(textColor, mainColor,
4.5f /* minimumContrast */) / 255f;
mScrimBehindAlpha = Math.max(mScrimBehindAlphaResValue, minOpacity);
- mLightBarController.setScrimColor(mScrimInFront.getColors());
+ mScrimInFrontColorListener.accept(mScrimInFront.getColors());
}
// We want to override the back scrim opacity for the AOD state
@@ -528,8 +543,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
if (alpha == 0f) {
scrim.setClickable(false);
} else {
- // Eat touch events (unless dozing).
- scrim.setClickable(!mState.isLowPowerState());
+ // Eat touch events (unless dozing or pulsing).
+ scrim.setClickable(mState != ScrimState.AOD && mState != ScrimState.PULSING);
}
updateScrim(scrim, alpha);
}
@@ -696,9 +711,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
}
}
- // TODO factor mLightBarController out of this class
if (scrim == mScrimBehind) {
- mLightBarController.setScrimAlpha(alpha);
+ mScrimBehindAlphaListener.accept(alpha);
}
final boolean wantsAlphaUpdate = alpha != currentAlpha;
@@ -807,7 +821,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo
@VisibleForTesting
protected WakeLock createWakeLock() {
return new DelayedWakeLock(getHandler(),
- WakeLock.createPartial(mContext, "Doze"));
+ WakeLock.createPartial(mContext, "Scrims"));
}
@Override
diff --git a/com/android/systemui/statusbar/phone/ScrimState.java b/com/android/systemui/statusbar/phone/ScrimState.java
index f4b6c38c..bbdaa999 100644
--- a/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/com/android/systemui/statusbar/phone/ScrimState.java
@@ -111,9 +111,10 @@ public enum ScrimState {
mCurrentInFrontAlpha = alwaysOnEnabled ? mAodFrontScrimAlpha : 1f;
mCurrentInFrontTint = Color.BLACK;
mCurrentBehindTint = Color.BLACK;
- // DisplayPowerManager will blank the screen for us, we just need
- // to set our state.
- mAnimateChange = !mDisplayRequiresBlanking;
+ mAnimationDuration = ScrimController.ANIMATION_DURATION_LONG;
+ // DisplayPowerManager may blank the screen for us,
+ // in this case we just need to set our state.
+ mAnimateChange = mDozeParameters.shouldControlScreenOff();
}
@Override
diff --git a/com/android/systemui/statusbar/phone/StatusBar.java b/com/android/systemui/statusbar/phone/StatusBar.java
index 750d2a5b..a3da8075 100644
--- a/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/com/android/systemui/statusbar/phone/StatusBar.java
@@ -213,6 +213,7 @@ import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.ActivityLaunchAnimator;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.UnlockMethodCache.OnUnlockMethodChangedListener;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -915,8 +916,14 @@ public class StatusBar extends SystemUI implements DemoMode,
ScrimView scrimBehind = mStatusBarWindow.findViewById(R.id.scrim_behind);
ScrimView scrimInFront = mStatusBarWindow.findViewById(R.id.scrim_in_front);
- mScrimController = SystemUIFactory.getInstance().createScrimController(mLightBarController,
+ mScrimController = SystemUIFactory.getInstance().createScrimController(
scrimBehind, scrimInFront, mLockscreenWallpaper,
+ scrimBehindAlpha -> {
+ mLightBarController.setScrimAlpha(scrimBehindAlpha);
+ },
+ scrimInFrontColor -> {
+ mLightBarController.setScrimColor(scrimInFrontColor);
+ },
scrimsVisible -> {
if (mStatusBarWindowManager != null) {
mStatusBarWindowManager.setScrimsVisibility(scrimsVisible);
@@ -1186,12 +1193,10 @@ public class StatusBar extends SystemUI implements DemoMode,
public void manageNotifications() {
Intent intent = new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent, true, true);
+ startActivity(intent, true, true, Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
public void clearAllNotifications() {
-
// animate-swipe all dismissable notifications, then animate the shade closed
int numChildren = mStackScroller.getChildCount();
@@ -1300,6 +1305,8 @@ public class StatusBar extends SystemUI implements DemoMode,
mKeyguardViewMediatorCallback = keyguardViewMediator.getViewMediatorCallback();
mLightBarController.setFingerprintUnlockController(mFingerprintUnlockController);
+ Dependency.get(KeyguardDismissUtil.class).setDismissHandler(
+ this::dismissKeyguardThenExecute);
Trace.endSection();
}
@@ -1425,11 +1432,15 @@ public class StatusBar extends SystemUI implements DemoMode,
}
public void addQsTile(ComponentName tile) {
- mQSPanel.getHost().addTile(tile);
+ if (mQSPanel != null && mQSPanel.getHost() != null) {
+ mQSPanel.getHost().addTile(tile);
+ }
}
public void remQsTile(ComponentName tile) {
- mQSPanel.getHost().removeTile(tile);
+ if (mQSPanel != null && mQSPanel.getHost() != null) {
+ mQSPanel.getHost().removeTile(tile);
+ }
}
public void clickTile(ComponentName tile) {
@@ -1439,7 +1450,8 @@ public class StatusBar extends SystemUI implements DemoMode,
@VisibleForTesting
protected void updateFooter() {
boolean showFooterView = mState != StatusBarState.KEYGUARD
- && mEntryManager.getNotificationData().getActiveNotifications().size() != 0;
+ && mEntryManager.getNotificationData().getActiveNotifications().size() != 0
+ && !mRemoteInputManager.getController().isRemoteInputActive();
boolean showDismissView = mClearAllEnabled && mState != StatusBarState.KEYGUARD
&& hasActiveClearableNotifications();
@@ -1824,6 +1836,11 @@ public class StatusBar extends SystemUI implements DemoMode,
return new StatusBar.H();
}
+ private void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade,
+ int flags) {
+ startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, flags);
+ }
+
@Override
public void startActivity(Intent intent, boolean dismissShade) {
startActivityDismissingKeyguard(intent, false, dismissShade);
@@ -1837,7 +1854,7 @@ public class StatusBar extends SystemUI implements DemoMode,
@Override
public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
startActivityDismissingKeyguard(intent, false, dismissShade,
- false /* disallowEnterPictureInPictureWhileLaunching */, callback);
+ false /* disallowEnterPictureInPictureWhileLaunching */, callback, 0);
}
public void setQsExpanded(boolean expanded) {
@@ -2043,11 +2060,19 @@ public class StatusBar extends SystemUI implements DemoMode,
}
/**
+ * Decides if the status bar (clock + notifications + signal cluster) should be visible
+ * or not when showing the bouncer.
+ *
+ * We want to hide it when:
+ * • User swipes up on the keyguard
+ * • Locked activity that doesn't show a status bar requests the bouncer
+ *
* @param animate should the change of the icons be animated.
*/
private void updateHideIconsForBouncer(boolean animate) {
- boolean shouldHideIconsForBouncer = !mPanelExpanded && mTopHidesStatusBar && mIsOccluded
- && (mBouncerShowing || mStatusBarWindowHidden);
+ boolean hideBecauseApp = mTopHidesStatusBar && mIsOccluded;
+ boolean hideBecauseKeyguard = !mPanelExpanded && !mIsOccluded && mBouncerShowing;
+ boolean shouldHideIconsForBouncer = hideBecauseApp || hideBecauseKeyguard;
if (mHideIconsForBouncer != shouldHideIconsForBouncer) {
mHideIconsForBouncer = shouldHideIconsForBouncer;
if (!shouldHideIconsForBouncer && mBouncerWasShowingWhenHidden) {
@@ -2286,7 +2311,7 @@ public class StatusBar extends SystemUI implements DemoMode,
return ;
}
- mNotificationPanel.expand(true /* animate */);
+ mNotificationPanel.expandWithoutQs();
if (false) postStartTracing();
}
@@ -2811,6 +2836,7 @@ public class StatusBar extends SystemUI implements DemoMode,
boolean remoteInputActive) {
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.row.notifyHeightChanged(true /* needsAnimation */);
+ updateFooter();
}
public void lockScrollTo(NotificationData.Entry entry) {
mStackScroller.lockScrollTo(entry.row);
@@ -2851,14 +2877,20 @@ public class StatusBar extends SystemUI implements DemoMode,
}
public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
- boolean dismissShade) {
+ boolean dismissShade, int flags) {
startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade,
- false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+ false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */,
+ flags);
+ }
+
+ public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
+ boolean dismissShade) {
+ startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, 0);
}
public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching,
- final Callback callback) {
+ final Callback callback, int flags) {
if (onlyProvisioned && !isDeviceProvisioned()) return;
final boolean afterKeyguardGone = PreviewInflater.wouldLaunchResolverActivity(
@@ -2867,6 +2899,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mAssistManager.hideAssist();
intent.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(flags);
int result = ActivityManager.START_CANCELED;
ActivityOptions options = new ActivityOptions(getActivityOptions(
null /* remoteAnimation */));
@@ -3884,7 +3917,11 @@ public class StatusBar extends SystemUI implements DemoMode,
}
public boolean onBackPressed() {
- if (mStatusBarKeyguardViewManager.onBackPressed()) {
+ boolean isScrimmedBouncer = mScrimController.getState() == ScrimState.BOUNCER_SCRIMMED;
+ if (mStatusBarKeyguardViewManager.onBackPressed(isScrimmedBouncer /* hideImmediately */)) {
+ if (!isScrimmedBouncer) {
+ mNotificationPanel.expandWithoutQs();
+ }
return true;
}
if (mNotificationPanel.isQsExpanded()) {
@@ -3915,7 +3952,8 @@ public class StatusBar extends SystemUI implements DemoMode,
}
private void showBouncerIfKeyguard() {
- if (mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED) {
+ if ((mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)
+ && !mKeyguardViewMediator.isHiding()) {
showBouncer(true /* animated */);
}
}
@@ -4547,7 +4585,7 @@ public class StatusBar extends SystemUI implements DemoMode,
if (!mStatusBarKeyguardViewManager.isShowing()) {
startActivityDismissingKeyguard(KeyguardBottomAreaView.INSECURE_CAMERA_INTENT,
false /* onlyProvisioned */, true /* dismissShade */,
- true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */);
+ true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0);
} else {
if (!mDeviceInteractive) {
// Avoid flickering of the scrim when we instant launch the camera and the bouncer
@@ -4979,6 +5017,14 @@ public class StatusBar extends SystemUI implements DemoMode,
@Override
public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
+ RemoteInputController controller = mRemoteInputManager.getController();
+ if (controller.isRemoteInputActive(row.getEntry())
+ && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
+ // We have an active remote input typed and the user clicked on the notification.
+ // this was probably unintentional, so we're closing the edit text instead.
+ controller.closeRemoteInputs();
+ return;
+ }
Notification notification = sbn.getNotification();
final PendingIntent intent = notification.contentIntent != null
? notification.contentIntent
@@ -5042,12 +5088,7 @@ public class StatusBar extends SystemUI implements DemoMode,
Intent fillInIntent = null;
Entry entry = row.getEntry();
CharSequence remoteInputText = null;
- RemoteInputController controller = mRemoteInputManager.getController();
- if (controller.isRemoteInputActive(entry)) {
- remoteInputText = row.getActiveRemoteInputText();
- }
- if (TextUtils.isEmpty(remoteInputText)
- && !TextUtils.isEmpty(entry.remoteInputText)) {
+ if (!TextUtils.isEmpty(entry.remoteInputText)) {
remoteInputText = entry.remoteInputText;
}
if (!TextUtils.isEmpty(remoteInputText)
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 510af03e..b4e7575d 100644
--- a/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -306,17 +306,6 @@ public class StatusBarIconControllerImpl extends StatusBarIconList implements Tu
mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
}
- /**
- * For mobile essentially (an array of holders in one slot)
- */
- private void handleSet(int slotIndex, List<StatusBarIconHolder> holders) {
- for (StatusBarIconHolder holder : holders) {
- int viewIndex = getViewIndex(slotIndex, holder.getTag());
- mIconLogger.onIconVisibility(getSlotName(slotIndex), holder.isVisible());
- mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder));
- }
- }
-
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(TAG + " state:");
diff --git a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 6b6ea10b..670c68f3 100644
--- a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -89,7 +89,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
protected boolean mFirstUpdate = true;
protected boolean mLastShowing;
protected boolean mLastOccluded;
- private boolean mLastTracking;
private boolean mLastBouncerShowing;
private boolean mLastBouncerDismissible;
protected boolean mLastRemoteInputActive;
@@ -152,28 +151,19 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
// • The user quickly taps on the display and we show "swipe up to unlock."
// • Keyguard will be dismissed by an action. a.k.a: FLAG_DISMISS_KEYGUARD_ACTIVITY
// • Full-screen user switcher is displayed.
- final boolean noLongerTracking = mLastTracking != tracking && !tracking;
if (mOccluded || mNotificationPanelView.isUnlockHintRunning()
|| mBouncer.willDismissWithAction()
|| mStatusBar.isFullScreenUserSwitcherState()) {
mBouncer.setExpansion(0);
} else if (mShowing && mStatusBar.isKeyguardCurrentlySecure() && !mDozing) {
mBouncer.setExpansion(expansion);
- if (expansion == 1) {
- mBouncer.onFullyHidden();
- } else if (!mBouncer.isShowing() && !mBouncer.isAnimatingAway()) {
+ if (expansion != 1 && tracking && !mBouncer.isShowing()
+ && !mBouncer.isAnimatingAway()) {
mBouncer.show(false /* resetSecuritySelection */, false /* animated */);
- } else if (noLongerTracking) {
- // Notify that falsing manager should stop its session when user stops touching,
- // even before the animation ends, to guarantee that we're not recording sensitive
- // data.
- mBouncer.onFullyShown();
- }
- if (expansion == 0 || expansion == 1) {
+ } else if (expansion == 0 || expansion == 1) {
updateStates();
}
}
- mLastTracking = tracking;
}
/**
@@ -522,12 +512,15 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
/**
* Notifies this manager that the back button has been pressed.
*
+ * @param hideImmediately Hide bouncer when {@code true}, keep it around otherwise.
+ * Non-scrimmed bouncers have a special animation tied to the expansion
+ * of the notification panel.
* @return whether the back press has been handled
*/
- public boolean onBackPressed() {
+ public boolean onBackPressed(boolean hideImmediately) {
if (mBouncer.isShowing()) {
mStatusBar.endAffordanceLaunch();
- reset(true /* hideBouncerWhenShowing */);
+ reset(hideImmediately);
return true;
}
return false;
@@ -595,6 +588,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
if (bouncerShowing != mLastBouncerShowing || mFirstUpdate) {
mStatusBarWindowManager.setBouncerShowing(bouncerShowing);
mStatusBar.setBouncerShowing(bouncerShowing);
+ if (bouncerShowing) {
+ mBouncer.onFullyShown();
+ } else {
+ mBouncer.onFullyHidden();
+ }
}
KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
diff --git a/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index c5a3a0d3..94ac4f62 100644
--- a/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -414,7 +414,7 @@ public class StatusBarSignalPolicy implements NetworkControllerImpl.SignalCallba
@Override public String toString() {
return "MobileIconState(subId=" + subId + ", strengthId=" + strengthId + ", roaming="
- + roaming + ", visible=" + visible + ")";
+ + roaming + ", typeId=" + typeId + ", visible=" + visible + ")";
}
}
}
diff --git a/com/android/systemui/statusbar/phone/SystemUIDialog.java b/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 378dad76..6a8d3a50 100644
--- a/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -69,6 +69,10 @@ public class SystemUIDialog extends AlertDialog {
setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
}
+ public void setNeutralButton(int resId, OnClickListener onClick) {
+ setButton(BUTTON_NEUTRAL, mContext.getString(resId), onClick);
+ }
+
public static void setShowForAllUsers(Dialog dialog, boolean show) {
if (show) {
dialog.getWindow().getAttributes().privateFlags |=
diff --git a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 94db95a6..cd17cfcd 100644
--- a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -280,7 +280,7 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
@Override
- public void onProfileAudioStateChanged(int bluetoothProfile, int state) {}
+ public void onAudioModeChanged() {}
private ActuallyCachedState getCachedState(CachedBluetoothDevice device) {
ActuallyCachedState state = mCachedState.get(device);
diff --git a/com/android/systemui/statusbar/policy/Clock.java b/com/android/systemui/statusbar/policy/Clock.java
index 4c92d01e..9aa80448 100644
--- a/com/android/systemui/statusbar/policy/Clock.java
+++ b/com/android/systemui/statusbar/policy/Clock.java
@@ -16,9 +16,6 @@
package com.android.systemui.statusbar.policy;
-import libcore.icu.LocaleData;
-
-import android.app.ActivityManager;
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -40,11 +37,13 @@ import android.view.Display;
import android.view.View;
import android.widget.TextView;
+import com.android.settingslib.Utils;
import com.android.systemui.DemoMode;
import com.android.systemui.Dependency;
import com.android.systemui.FontSizeUtils;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.settings.CurrentUserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -52,6 +51,8 @@ import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
+import libcore.icu.LocaleData;
+
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
@@ -65,6 +66,9 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
public static final String CLOCK_SECONDS = "clock_seconds";
+ private final CurrentUserTracker mCurrentUserTracker;
+ private int mCurrentUserId;
+
private boolean mClockVisibleByPolicy = true;
private boolean mClockVisibleByUser = true;
@@ -84,6 +88,17 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
private boolean mShowSeconds;
private Handler mSecondsHandler;
+ /**
+ * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings
+ * for text.
+ */
+ private boolean mUseWallpaperTextColor;
+
+ /**
+ * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized.
+ */
+ private int mNonAdaptedColor;
+
public Clock(Context context) {
this(context, null);
}
@@ -101,9 +116,16 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
try {
mAmPmStyle = a.getInt(R.styleable.Clock_amPmStyle, AM_PM_STYLE_GONE);
mShowDark = a.getBoolean(R.styleable.Clock_showDark, true);
+ mNonAdaptedColor = getCurrentTextColor();
} finally {
a.recycle();
}
+ mCurrentUserTracker = new CurrentUserTracker(context) {
+ @Override
+ public void onUserSwitched(int newUserId) {
+ mCurrentUserId = newUserId;
+ }
+ };
}
@Override
@@ -128,6 +150,8 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
if (mShowDark) {
Dependency.get(DarkIconDispatcher.class).addDarkReceiver(this);
}
+ mCurrentUserTracker.startTracking();
+ mCurrentUserId = mCurrentUserTracker.getCurrentUserId();
}
// NOTE: It's safe to do these after registering the receiver since the receiver always runs
@@ -153,6 +177,7 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
if (mShowDark) {
Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(this);
}
+ mCurrentUserTracker.stopTracking();
}
}
@@ -227,7 +252,10 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
@Override
public void onDarkChanged(Rect area, float darkIntensity, int tint) {
- setTextColor(DarkIconDispatcher.getTint(area, this, tint));
+ mNonAdaptedColor = DarkIconDispatcher.getTint(area, this, tint);
+ if (!mUseWallpaperTextColor) {
+ setTextColor(mNonAdaptedColor);
+ }
}
@Override
@@ -242,6 +270,25 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
0);
}
+ /**
+ * Sets whether the clock uses the wallpaperTextColor. If we're not using it, we'll revert back
+ * to dark-mode-based/tinted colors.
+ *
+ * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for text color
+ */
+ public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) {
+ if (shouldUseWallpaperTextColor == mUseWallpaperTextColor) {
+ return;
+ }
+ mUseWallpaperTextColor = shouldUseWallpaperTextColor;
+
+ if (mUseWallpaperTextColor) {
+ setTextColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor));
+ } else {
+ setTextColor(mNonAdaptedColor);
+ }
+ }
+
private void updateShowSeconds() {
if (mShowSeconds) {
// Wait until we have a display to start trying to show seconds.
@@ -267,7 +314,7 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
private final CharSequence getSmallTime() {
Context context = getContext();
- boolean is24 = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser());
+ boolean is24 = DateFormat.is24HourFormat(context, mCurrentUserId);
LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
final char MAGIC1 = '\uEF00';
@@ -357,8 +404,7 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C
} else if (hhmm != null && hhmm.length() == 4) {
int hh = Integer.parseInt(hhmm.substring(0, 2));
int mm = Integer.parseInt(hhmm.substring(2));
- boolean is24 = DateFormat.is24HourFormat(
- getContext(), ActivityManager.getCurrentUser());
+ boolean is24 = DateFormat.is24HourFormat(getContext(), mCurrentUserId);
if (is24) {
mCalendar.set(Calendar.HOUR_OF_DAY, hh);
} else {
diff --git a/com/android/systemui/statusbar/policy/DateView.java b/com/android/systemui/statusbar/policy/DateView.java
index 74a30fa8..ef630c72 100644
--- a/com/android/systemui/statusbar/policy/DateView.java
+++ b/com/android/systemui/statusbar/policy/DateView.java
@@ -27,6 +27,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.widget.TextView;
+import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.R;
@@ -42,6 +43,17 @@ public class DateView extends TextView {
private String mLastText;
private String mDatePattern;
+ /**
+ * Whether we should use colors that adapt based on wallpaper/the scrim behind quick settings
+ * for text.
+ */
+ private boolean mUseWallpaperTextColor;
+
+ /**
+ * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized.
+ */
+ private int mNonAdaptedTextColor;
+
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -62,6 +74,7 @@ public class DateView extends TextView {
public DateView(Context context, AttributeSet attrs) {
super(context, attrs);
+ mNonAdaptedTextColor = getCurrentTextColor();
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.DateView,
@@ -117,6 +130,25 @@ public class DateView extends TextView {
}
}
+ /**
+ * Sets whether the date view uses the wallpaperTextColor. If we're not using it, we'll revert
+ * back to dark-mode-based/tinted colors.
+ *
+ * @param shouldUseWallpaperTextColor whether we should use wallpaperTextColor for text color
+ */
+ public void useWallpaperTextColor(boolean shouldUseWallpaperTextColor) {
+ if (shouldUseWallpaperTextColor == mUseWallpaperTextColor) {
+ return;
+ }
+ mUseWallpaperTextColor = shouldUseWallpaperTextColor;
+
+ if (mUseWallpaperTextColor) {
+ setTextColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor));
+ } else {
+ setTextColor(mNonAdaptedTextColor);
+ }
+ }
+
public void setDatePattern(String pattern) {
if (TextUtils.equals(pattern, mDatePattern)) {
return;
diff --git a/com/android/systemui/statusbar/policy/DeadZone.java b/com/android/systemui/statusbar/policy/DeadZone.java
index 06040e2b..4a117548 100644
--- a/com/android/systemui/statusbar/policy/DeadZone.java
+++ b/com/android/systemui/statusbar/policy/DeadZone.java
@@ -17,18 +17,16 @@
package com.android.systemui.statusbar.policy;
import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
+import android.content.res.Resources;
import android.graphics.Canvas;
import android.os.SystemClock;
-import android.util.AttributeSet;
import android.util.Slog;
import android.view.MotionEvent;
import android.view.Surface;
-import android.view.View;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
+import com.android.systemui.statusbar.phone.NavigationBarView;
import com.android.systemui.statusbar.phone.StatusBar;
/**
@@ -38,7 +36,7 @@ import com.android.systemui.statusbar.phone.StatusBar;
* outside the navigation bar (since this is when accidental taps are more likely), then contracts
* back over time (since a later tap might be intended for the top of the bar).
*/
-public class DeadZone extends View {
+public class DeadZone {
public static final String TAG = "DeadZone";
public static final boolean DEBUG = false;
@@ -47,6 +45,7 @@ public class DeadZone extends View {
private static final boolean CHATTY = true; // print to logcat when we eat a click
private final StatusBar mStatusBar;
+ private final NavigationBarView mNavigationBarView;
private boolean mShouldFlash;
private float mFlashFrac = 0f;
@@ -67,31 +66,11 @@ public class DeadZone extends View {
}
};
- public DeadZone(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public DeadZone(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DeadZone,
- defStyle, 0);
-
- mHold = a.getInteger(R.styleable.DeadZone_holdTime, 0);
- mDecay = a.getInteger(R.styleable.DeadZone_decayTime, 0);
-
- mSizeMin = a.getDimensionPixelSize(R.styleable.DeadZone_minSize, 0);
- mSizeMax = a.getDimensionPixelSize(R.styleable.DeadZone_maxSize, 0);
-
- int index = a.getInt(R.styleable.DeadZone_orientation, -1);
- mVertical = (index == VERTICAL);
-
- if (DEBUG)
- Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
- + (mVertical ? " vertical" : " horizontal"));
-
- setFlashOnTouchCapture(context.getResources().getBoolean(R.bool.config_dead_zone_flash));
- mStatusBar = SysUiServiceProvider.getComponent(context, StatusBar.class);
+ public DeadZone(NavigationBarView view) {
+ mNavigationBarView = view;
+ mStatusBar = SysUiServiceProvider.getComponent(mNavigationBarView.getContext(),
+ StatusBar.class);
+ onConfigurationChanged(HORIZONTAL);
}
static float lerp(float a, float b, float f) {
@@ -112,11 +91,29 @@ public class DeadZone extends View {
public void setFlashOnTouchCapture(boolean dbg) {
mShouldFlash = dbg;
mFlashFrac = 0f;
- postInvalidate();
+ mNavigationBarView.postInvalidate();
+ }
+
+ public void onConfigurationChanged(int rotation) {
+ mDisplayRotation = rotation;
+
+ final Resources res = mNavigationBarView.getResources();
+ mHold = res.getInteger(R.integer.navigation_bar_deadzone_hold);
+ mDecay = res.getInteger(R.integer.navigation_bar_deadzone_decay);
+
+ mSizeMin = res.getDimensionPixelSize(R.dimen.navigation_bar_deadzone_size);
+ mSizeMax = res.getDimensionPixelSize(R.dimen.navigation_bar_deadzone_size_max);
+ int index = res.getInteger(R.integer.navigation_bar_deadzone_orientation);
+ mVertical = (index == VERTICAL);
+
+ if (DEBUG) {
+ Slog.v(TAG, this + " size=[" + mSizeMin + "-" + mSizeMax + "] hold=" + mHold
+ + (mVertical ? " vertical" : " horizontal"));
+ }
+ setFlashOnTouchCapture(res.getBoolean(R.bool.config_dead_zone_flash));
}
// I made you a touch event...
- @Override
public boolean onTouchEvent(MotionEvent event) {
if (DEBUG) {
Slog.v(TAG, this + " onTouch: " + MotionEvent.actionToString(event.getAction()));
@@ -143,7 +140,7 @@ public class DeadZone extends View {
final boolean consumeEvent;
if (mVertical) {
if (mDisplayRotation == Surface.ROTATION_270) {
- consumeEvent = event.getX() > getWidth() - size;
+ consumeEvent = event.getX() > mNavigationBarView.getWidth() - size;
} else {
consumeEvent = event.getX() < size;
}
@@ -155,8 +152,8 @@ public class DeadZone extends View {
Slog.v(TAG, "consuming errant click: (" + event.getX() + "," + event.getY() + ")");
}
if (mShouldFlash) {
- post(mDebugFlash);
- postInvalidate();
+ mNavigationBarView.post(mDebugFlash);
+ mNavigationBarView.postInvalidate();
}
return true; // ...but I eated it
}
@@ -168,19 +165,18 @@ public class DeadZone extends View {
mLastPokeTime = event.getEventTime();
if (DEBUG)
Slog.v(TAG, "poked! size=" + getSize(mLastPokeTime));
- if (mShouldFlash) postInvalidate();
+ if (mShouldFlash) mNavigationBarView.postInvalidate();
}
public void setFlash(float f) {
mFlashFrac = f;
- postInvalidate();
+ mNavigationBarView.postInvalidate();
}
public float getFlash() {
return mFlashFrac;
}
- @Override
public void onDraw(Canvas can) {
if (!mShouldFlash || mFlashFrac <= 0f) {
return;
@@ -202,10 +198,6 @@ public class DeadZone extends View {
if (DEBUG && size > mSizeMin)
// crazy aggressive redrawing here, for debugging only
- postInvalidateDelayed(100);
- }
-
- public void setDisplayRotation(int rotation) {
- mDisplayRotation = rotation;
+ mNavigationBarView.postInvalidateDelayed(100);
}
}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonView.java b/com/android/systemui/statusbar/policy/KeyButtonView.java
index 5d7e9383..1b02e152 100644
--- a/com/android/systemui/statusbar/policy/KeyButtonView.java
+++ b/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -28,7 +28,6 @@ import android.metrics.LogMaker;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
-import android.os.VibrationEffect;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.HapticFeedbackConstants;
@@ -50,10 +49,12 @@ import com.android.systemui.OverviewProxyService;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.statusbar.VibratorHelper;
+import static android.view.KeyEvent.KEYCODE_HOME;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_SCRUB_TOUCH_SLOP_PX;
+import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_TOUCH_SLOP_PX;
public class KeyButtonView extends ImageView implements ButtonInterface {
private static final String TAG = KeyButtonView.class.getSimpleName();
@@ -62,9 +63,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
private int mContentDescriptionRes;
private long mDownTime;
private int mCode;
- private int mTouchSlop;
private int mTouchDownX;
private int mTouchDownY;
+ private boolean mIsVertical;
private boolean mSupportsLongpress = true;
private AudioManager mAudioManager;
private boolean mGestureAborted;
@@ -72,7 +73,6 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
private OnClickListener mOnClickListener;
private final KeyButtonRipple mRipple;
private final OverviewProxyService mOverviewProxyService;
- private final VibratorHelper mVibratorHelper;
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final Runnable mCheckLongPress = new Runnable() {
@@ -115,11 +115,9 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
a.recycle();
setClickable(true);
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mRipple = new KeyButtonRipple(context, this);
- mVibratorHelper = Dependency.get(VibratorHelper.class);
mOverviewProxyService = Dependency.get(OverviewProxyService.class);
setBackground(mRipple);
}
@@ -200,7 +198,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
}
public boolean onTouchEvent(MotionEvent ev) {
- final boolean isProxyConnected = mOverviewProxyService.getProxy() != null;
+ final boolean showSwipeUI = mOverviewProxyService.shouldShowSwipeUpUI();
final int action = ev.getAction();
int x, y;
if (action == MotionEvent.ACTION_DOWN) {
@@ -226,7 +224,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
// Provide the same haptic feedback that the system offers for virtual keys.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
- if (!isProxyConnected) {
+ if (!showSwipeUI) {
playSoundEffect(SoundEffectConstants.CLICK);
}
removeCallbacks(mCheckLongPress);
@@ -235,8 +233,11 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
case MotionEvent.ACTION_MOVE:
x = (int)ev.getRawX();
y = (int)ev.getRawY();
- boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) > mTouchSlop;
- boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) > mTouchSlop;
+
+ boolean exceededTouchSlopX = Math.abs(x - mTouchDownX) >
+ (mIsVertical ? QUICK_SCRUB_TOUCH_SLOP_PX : QUICK_STEP_TOUCH_SLOP_PX);
+ boolean exceededTouchSlopY = Math.abs(y - mTouchDownY) >
+ (mIsVertical ? QUICK_STEP_TOUCH_SLOP_PX : QUICK_SCRUB_TOUCH_SLOP_PX);
if (exceededTouchSlopX || exceededTouchSlopY) {
// When quick step is enabled, prevent animating the ripple triggered by
// setPressed and decide to run it on touch up
@@ -255,11 +256,10 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
final boolean doIt = isPressed() && !mLongClicked;
setPressed(false);
final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
- if (isProxyConnected) {
+ if (showSwipeUI) {
if (doIt) {
- if (doHapticFeedback) {
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
- }
+ // Apply haptic feedback on touch up since there is none on touch down
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
playSoundEffect(SoundEffectConstants.CLICK);
}
} else if (doHapticFeedback && !mLongClicked) {
@@ -270,8 +270,12 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
if (mCode != 0) {
if (doIt) {
// If there was a pending remote recents animation, then we need to
- // cancel the animation now before we handle the button itself
- ActivityManagerWrapper.getInstance().cancelRecentsAnimation();
+ // cancel the animation now before we handle the button itself. In the case
+ // where we are going home and the recents animation has already started,
+ // just cancel the recents animation, leaving the home stack in place
+ boolean isHomeKey = mCode == KEYCODE_HOME;
+ ActivityManagerWrapper.getInstance().cancelRecentsAnimation(!isHomeKey);
+
sendEvent(KeyEvent.ACTION_UP, 0);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
} else {
@@ -342,7 +346,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
@Override
public void setVertical(boolean vertical) {
- //no op
+ mIsVertical = vertical;
}
}
diff --git a/com/android/systemui/statusbar/policy/SmartReplyView.java b/com/android/systemui/statusbar/policy/SmartReplyView.java
index 790135fc..4c79ee32 100644
--- a/com/android/systemui/statusbar/policy/SmartReplyView.java
+++ b/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -20,8 +20,12 @@ import android.view.ViewGroup;
import android.widget.Button;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.statusbar.NotificationData;
+import com.android.systemui.statusbar.SmartReplyLogger;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import java.text.BreakIterator;
import java.util.Comparator;
@@ -42,6 +46,7 @@ public class SmartReplyView extends ViewGroup {
private static final int SQUEEZE_FAILED = -1;
private final SmartReplyConstants mConstants;
+ private final KeyguardDismissUtil mKeyguardDismissUtil;
/** Spacing to be applied between views. */
private final int mSpacing;
@@ -62,6 +67,7 @@ public class SmartReplyView extends ViewGroup {
public SmartReplyView(Context context, AttributeSet attrs) {
super(context, attrs);
mConstants = Dependency.get(SmartReplyConstants.class);
+ mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
int spacing = 0;
int singleLineButtonPaddingHorizontal = 0;
@@ -105,14 +111,16 @@ public class SmartReplyView extends ViewGroup {
Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
}
- public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent) {
+ public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent,
+ SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
removeAllViews();
if (remoteInput != null && pendingIntent != null) {
CharSequence[] choices = remoteInput.getChoices();
if (choices != null) {
- for (CharSequence choice : choices) {
+ for (int i = 0; i < choices.length; ++i) {
Button replyButton = inflateReplyButton(
- getContext(), this, choice, remoteInput, pendingIntent);
+ getContext(), this, i, choices[i], remoteInput, pendingIntent,
+ smartReplyLogger, entry);
addView(replyButton);
}
}
@@ -126,12 +134,14 @@ public class SmartReplyView extends ViewGroup {
}
@VisibleForTesting
- static Button inflateReplyButton(Context context, ViewGroup root, CharSequence choice,
- RemoteInput remoteInput, PendingIntent pendingIntent) {
+ Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
+ CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
+ SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
Button b = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_reply_button, root, false);
b.setText(choice);
- b.setOnClickListener(view -> {
+
+ OnDismissAction action = () -> {
Bundle results = new Bundle();
results.putString(remoteInput.getResultKey(), choice.toString());
Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -142,6 +152,13 @@ public class SmartReplyView extends ViewGroup {
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Unable to send smart reply", e);
}
+ smartReplyLogger.smartReplySent(entry, replyIndex);
+ return false; // do not defer
+ };
+
+ b.setOnClickListener(view -> {
+ mKeyguardDismissUtil.dismissKeyguardThenExecute(
+ action, null /* cancelAction */, false /* afterKeyguardGone */);
});
return b;
}
diff --git a/com/android/systemui/statusbar/policy/TelephonyIcons.java b/com/android/systemui/statusbar/policy/TelephonyIcons.java
index 7e6fe022..bd768202 100644
--- a/com/android/systemui/statusbar/policy/TelephonyIcons.java
+++ b/com/android/systemui/statusbar/policy/TelephonyIcons.java
@@ -208,7 +208,7 @@ class TelephonyIcons {
0,
0,
AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH[0],
- R.string.cell_data_off,
+ R.string.cell_data_off_content_description,
0,
false);
}
diff --git a/com/android/systemui/statusbar/stack/AmbientState.java b/com/android/systemui/statusbar/stack/AmbientState.java
index 7c1c566a..91a4b07c 100644
--- a/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/com/android/systemui/statusbar/stack/AmbientState.java
@@ -70,8 +70,8 @@ public class AmbientState {
private int mIntrinsicPadding;
private int mExpandAnimationTopChange;
private ExpandableNotificationRow mExpandingNotification;
- private boolean mFullyDark;
private int mDarkTopPadding;
+ private float mDarkAmount;
public AmbientState(Context context) {
reload(context);
@@ -149,6 +149,16 @@ public class AmbientState {
mDark = dark;
}
+ /** Dark ratio of the status bar **/
+ public void setDarkAmount(float darkAmount) {
+ mDarkAmount = darkAmount;
+ }
+
+ /** Returns the dark ratio of the status bar */
+ public float getDarkAmount() {
+ return mDarkAmount;
+ }
+
public void setHideSensitive(boolean hideSensitive) {
mHideSensitive = hideSensitive;
}
@@ -413,17 +423,10 @@ public class AmbientState {
}
/**
- * {@see isFullyDark}
- */
- public void setFullyDark(boolean fullyDark) {
- mFullyDark = fullyDark;
- }
-
- /**
* @return {@code true } when shade is completely dark: in AOD or ambient display.
*/
public boolean isFullyDark() {
- return mFullyDark;
+ return mDarkAmount == 1;
}
public void setDarkTopPadding(int darkTopPadding) {
diff --git a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 375e8606..bc5a848f 100644
--- a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -96,6 +96,7 @@ import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
+import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.ScrimController;
@@ -269,8 +270,6 @@ public class NotificationStackScrollLayout extends ViewGroup
*/
private boolean mOnlyScrollingInThisMotion;
private boolean mDisallowDismissInThisMotion;
- private boolean mInterceptDelegateEnabled;
- private boolean mDelegateToScrollView;
private boolean mDisallowScrollingInThisMotion;
private long mGoToFullShadeDelay;
private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
@@ -562,17 +561,17 @@ public class NotificationStackScrollLayout extends ViewGroup
return;
}
- final int color;
- if (mAmbientState.isDark()) {
- color = Color.WHITE;
- } else {
- float alpha =
- BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
- alpha *= 1f - mDarkAmount;
- // We need to manually blend in the background color
- int scrimColor = mScrimController.getBackgroundColor();
- color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
- }
+ float alpha =
+ BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
+ alpha *= 1f - mDarkAmount;
+ // We need to manually blend in the background color.
+ int scrimColor = mScrimController.getBackgroundColor();
+ int awakeColor = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
+
+ // Interpolate between semi-transparent notification panel background color
+ // and white AOD separator.
+ float colorInterpolation = Interpolators.DECELERATE_QUINT.getInterpolation(mDarkAmount);
+ int color = ColorUtils.blendARGB(awakeColor, Color.WHITE, colorInterpolation);
if (mCachedBackgroundColor != color) {
mCachedBackgroundColor = color;
@@ -3023,6 +3022,11 @@ public class NotificationStackScrollLayout extends ViewGroup
public void setAnimationsEnabled(boolean animationsEnabled) {
mAnimationsEnabled = animationsEnabled;
updateNotificationAnimationStates();
+ if (!animationsEnabled) {
+ mSwipedOutViews.clear();
+ mChildrenToRemoveAnimated.clear();
+ clearTemporaryViewsInGroup(this);
+ }
}
private void updateNotificationAnimationStates() {
@@ -3090,6 +3094,21 @@ public class NotificationStackScrollLayout extends ViewGroup
@Override
public void changeViewPosition(View child, int newIndex) {
int currentIndex = indexOfChild(child);
+
+ if (currentIndex == -1) {
+ boolean isTransient = false;
+ if (child instanceof ExpandableNotificationRow
+ && ((ExpandableNotificationRow)child).getTransientContainer() != null) {
+ isTransient = true;
+ }
+ Log.e(TAG, "Attempting to re-position "
+ + (isTransient ? "transient" : "")
+ + " view {"
+ + child
+ + "}");
+ return;
+ }
+
if (child != null && child.getParent() == this && currentIndex != newIndex) {
mChangePositionInProgress = true;
((ExpandableView)child).setChangingPosition(true);
@@ -3569,17 +3588,17 @@ public class NotificationStackScrollLayout extends ViewGroup
private void clearTemporaryViews() {
// lets make sure nothing is in the overlay / transient anymore
- clearTemporaryViews(this);
+ clearTemporaryViewsInGroup(this);
for (int i = 0; i < getChildCount(); i++) {
ExpandableView child = (ExpandableView) getChildAt(i);
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
- clearTemporaryViews(row.getChildrenContainer());
+ clearTemporaryViewsInGroup(row.getChildrenContainer());
}
}
}
- private void clearTemporaryViews(ViewGroup viewGroup) {
+ private void clearTemporaryViewsInGroup(ViewGroup viewGroup) {
while (viewGroup != null && viewGroup.getTransientViewCount() != 0) {
viewGroup.removeTransientView(viewGroup.getTransientView(0));
}
@@ -3922,12 +3941,11 @@ public class NotificationStackScrollLayout extends ViewGroup
requestChildrenUpdate();
applyCurrentBackgroundBounds();
updateWillNotDraw();
- updateAntiBurnInTranslation();
notifyHeightChangeListener(mShelf);
}
private void updateAntiBurnInTranslation() {
- setTranslationX(mAmbientState.isDark() ? mAntiBurnInOffsetX : 0);
+ setTranslationX(mAntiBurnInOffsetX * mDarkAmount);
}
/**
@@ -3942,12 +3960,18 @@ public class NotificationStackScrollLayout extends ViewGroup
private void setDarkAmount(float darkAmount) {
mDarkAmount = darkAmount;
- final boolean fullyDark = darkAmount == 1;
- if (mAmbientState.isFullyDark() != fullyDark) {
- mAmbientState.setFullyDark(fullyDark);
+ boolean wasFullyDark = mAmbientState.isFullyDark();
+ mAmbientState.setDarkAmount(darkAmount);
+ if (mAmbientState.isFullyDark() != wasFullyDark) {
updateContentHeight();
+ DozeParameters dozeParameters = DozeParameters.getInstance(mContext);
+ if (mAmbientState.isFullyDark() && dozeParameters.shouldControlScreenOff()) {
+ mShelf.fadeInTranslating();
+ }
}
updateBackgroundDimming();
+ updateAntiBurnInTranslation();
+ requestChildrenUpdate();
}
public float getDarkAmount() {
diff --git a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index a8d2d98b..f4d7f8d4 100644
--- a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -276,8 +276,6 @@ public class StackScrollAlgorithm {
if (i >= firstHiddenIndex) {
// we need normal padding now, to be in sync with what the stack calculates
lastView = null;
- ExpandableViewState viewState = resultState.getViewStateForView(v);
- viewState.hidden = true;
}
notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
float increasedPadding = v.getIncreasedPaddingAmount();
diff --git a/com/android/systemui/util/wakelock/DelayedWakeLock.java b/com/android/systemui/util/wakelock/DelayedWakeLock.java
index 5ec3dff0..a901e882 100644
--- a/com/android/systemui/util/wakelock/DelayedWakeLock.java
+++ b/com/android/systemui/util/wakelock/DelayedWakeLock.java
@@ -23,7 +23,7 @@ import android.os.Handler;
*/
public class DelayedWakeLock implements WakeLock {
- private static final long RELEASE_DELAY_MS = 120;
+ private static final long RELEASE_DELAY_MS = 140;
private final Handler mHandler;
private final WakeLock mInner;
diff --git a/com/android/systemui/volume/Events.java b/com/android/systemui/volume/Events.java
index 2c85bb6e..ca55e1f2 100644
--- a/com/android/systemui/volume/Events.java
+++ b/com/android/systemui/volume/Events.java
@@ -52,26 +52,28 @@ public class Events {
public static final int EVENT_MUTE_CHANGED = 15; // (stream|int) (muted|bool)
public static final int EVENT_TOUCH_LEVEL_DONE = 16; // (stream|int) (level|bool)
public static final int EVENT_ZEN_CONFIG_CHANGED = 17; // (allow/disallow|string)
+ public static final int EVENT_RINGER_TOGGLE = 18; // (ringer_mode)
private static final String[] EVENT_TAGS = {
- "show_dialog",
- "dismiss_dialog",
- "active_stream_changed",
- "expand",
- "key",
- "collection_started",
- "collection_stopped",
- "icon_click",
- "settings_click",
- "touch_level_changed",
- "level_changed",
- "internal_ringer_mode_changed",
- "external_ringer_mode_changed",
- "zen_mode_changed",
- "suppressor_changed",
- "mute_changed",
- "touch_level_done",
- "zen_mode_config_changed",
+ "show_dialog",
+ "dismiss_dialog",
+ "active_stream_changed",
+ "expand",
+ "key",
+ "collection_started",
+ "collection_stopped",
+ "icon_click",
+ "settings_click",
+ "touch_level_changed",
+ "level_changed",
+ "internal_ringer_mode_changed",
+ "external_ringer_mode_changed",
+ "zen_mode_changed",
+ "suppressor_changed",
+ "mute_changed",
+ "touch_level_done",
+ "zen_mode_config_changed",
+ "ringer_toggle"
};
public static final int DISMISS_REASON_UNKNOWN = 0;
@@ -112,6 +114,7 @@ public class Events {
public static Callback sCallback;
public static void writeEvent(Context context, int tag, Object... list) {
+ MetricsLogger logger = new MetricsLogger();
final long time = System.currentTimeMillis();
final StringBuilder sb = new StringBuilder("writeEvent ").append(EVENT_TAGS[tag]);
if (list != null && list.length > 0) {
@@ -139,7 +142,7 @@ public class Events {
break;
case EVENT_ICON_CLICK:
MetricsLogger.action(context, MetricsEvent.ACTION_VOLUME_ICON,
- (Integer) list[1]);
+ (Integer) list[0]);
sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ')
.append(iconStateToString((Integer) list[1]));
break;
@@ -155,10 +158,16 @@ public class Events {
break;
case EVENT_KEY:
MetricsLogger.action(context, MetricsEvent.ACTION_VOLUME_KEY,
- (Integer) list[1]);
+ (Integer) list[0]);
sb.append(AudioSystem.streamToString((Integer) list[0])).append(' ')
.append(list[1]);
break;
+ case EVENT_RINGER_TOGGLE:
+ logger.action(MetricsEvent.ACTION_VOLUME_RINGER_TOGGLE, (Integer) list[0]);
+ break;
+ case EVENT_SETTINGS_CLICK:
+ logger.action(MetricsEvent.ACTION_VOLUME_SETTINGS);
+ break;
case EVENT_EXTERNAL_RINGER_MODE_CHANGED:
MetricsLogger.action(context, MetricsEvent.ACTION_RINGER_MODE,
(Integer) list[0]);
diff --git a/com/android/systemui/volume/VolumeDialogImpl.java b/com/android/systemui/volume/VolumeDialogImpl.java
index 6f71e55b..00874e3e 100644
--- a/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/com/android/systemui/volume/VolumeDialogImpl.java
@@ -212,8 +212,7 @@ public class VolumeDialogImpl implements VolumeDialog {
.setDuration(300)
.setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
.withEndAction(() -> {
- mWindow.getDecorView().requestAccessibilityFocus();
- if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true)) {
+ if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
mRingerIcon.postOnAnimationDelayed(mSinglePress, 1500);
}
})
@@ -302,15 +301,8 @@ public class VolumeDialogImpl implements VolumeDialog {
if (D.BUG) Slog.d(TAG, "Adding row for stream " + stream);
VolumeRow row = new VolumeRow();
initRow(row, stream, iconRes, iconMuteRes, important, defaultStream);
- if (dynamic && mRows.size() > 2) {
- // Dynamic Streams should be the first in the list, so they're shown to start of
- // everything except a11y
- mDialogRowsView.addView(row.view, 1);
- mRows.add(1, row);
- } else {
- mDialogRowsView.addView(row.view);
- mRows.add(row);
- }
+ mDialogRowsView.addView(row.view);
+ mRows.add(row);
}
private void addExistingRows() {
@@ -423,6 +415,7 @@ public class VolumeDialogImpl implements VolumeDialog {
mSettingsView.setVisibility(
mDeviceProvisionedController.isDeviceProvisioned() ? VISIBLE : GONE);
mSettingsIcon.setOnClickListener(v -> {
+ Events.writeEvent(mContext, Events.EVENT_SETTINGS_CLICK);
Intent intent = new Intent(Settings.ACTION_SOUND_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
dismissH(DISMISS_REASON_SETTINGS_CLICKED);
@@ -432,8 +425,6 @@ public class VolumeDialogImpl implements VolumeDialog {
public void initRingerH() {
mRingerIcon.setOnClickListener(v -> {
- Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, AudioManager.STREAM_RING,
- mRingerIcon.getTag());
Prefs.putBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, true);
final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
if (ss == null) {
@@ -457,6 +448,7 @@ public class VolumeDialogImpl implements VolumeDialog {
mController.setStreamVolume(AudioManager.STREAM_RING, 1);
}
}
+ Events.writeEvent(mContext, Events.EVENT_RINGER_TOGGLE, newRingerMode);
updateRingerH();
provideTouchFeedbackH(newRingerMode);
mController.setRingerMode(newRingerMode, false);
@@ -604,7 +596,8 @@ public class VolumeDialogImpl implements VolumeDialog {
return activeRow.stream == STREAM_RING
|| activeRow.stream == STREAM_ALARM
|| activeRow.stream == STREAM_VOICE_CALL
- || activeRow.stream == STREAM_ACCESSIBILITY;
+ || activeRow.stream == STREAM_ACCESSIBILITY
+ || mDynamic.get(activeRow.stream);
}
return false;
diff --git a/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
index 7c9aeded..3d5476d0 100644
--- a/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
+++ b/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * 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.
@@ -16,24 +16,55 @@
package com.android.uiautomator.testrunner;
-import android.app.Instrumentation;
+import android.content.Context;
import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
-import android.test.InstrumentationTestCase;
+import android.view.inputmethod.InputMethodInfo;
-import com.android.uiautomator.core.InstrumentationUiAutomatorBridge;
+import com.android.internal.view.IInputMethodManager;
import com.android.uiautomator.core.UiDevice;
+import junit.framework.TestCase;
+
+import java.util.List;
+
/**
- * UI Automator test case that is executed on the device.
+ * UI automation test should extend this class. This class provides access
+ * to the following:
+ * {@link UiDevice} instance
+ * {@link Bundle} for command line parameters.
+ * @since API Level 16
* @deprecated New tests should be written using UI Automator 2.0 which is available as part of the
* Android Testing Support Library.
*/
@Deprecated
-public class UiAutomatorTestCase extends InstrumentationTestCase {
+public class UiAutomatorTestCase extends TestCase {
+ private static final String DISABLE_IME = "disable_ime";
+ private static final String DUMMY_IME_PACKAGE = "com.android.testing.dummyime";
+ private UiDevice mUiDevice;
private Bundle mParams;
private IAutomationSupport mAutomationSupport;
+ private boolean mShouldDisableIme = false;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mShouldDisableIme = "true".equals(mParams.getString(DISABLE_IME));
+ if (mShouldDisableIme) {
+ setDummyIme();
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mShouldDisableIme) {
+ restoreActiveIme();
+ }
+ super.tearDown();
+ }
/**
* Get current instance of {@link UiDevice}. Works similar to calling the static
@@ -41,7 +72,7 @@ public class UiAutomatorTestCase extends InstrumentationTestCase {
* @since API Level 16
*/
public UiDevice getUiDevice() {
- return UiDevice.getInstance();
+ return mUiDevice;
}
/**
@@ -54,43 +85,34 @@ public class UiAutomatorTestCase extends InstrumentationTestCase {
return mParams;
}
- void setAutomationSupport(IAutomationSupport automationSupport) {
- mAutomationSupport = automationSupport;
- }
-
/**
* Provides support for running tests to report interim status
*
* @return IAutomationSupport
* @since API Level 16
- * @deprecated Use {@link Instrumentation#sendStatus(int, Bundle)} instead
*/
public IAutomationSupport getAutomationSupport() {
- if (mAutomationSupport == null) {
- mAutomationSupport = new InstrumentationAutomationSupport(getInstrumentation());
- }
return mAutomationSupport;
}
/**
- * Initializes this test case.
- *
- * @param params Instrumentation arguments.
+ * package private
+ * @param uiDevice
*/
- void initialize(Bundle params) {
- mParams = params;
+ void setUiDevice(UiDevice uiDevice) {
+ mUiDevice = uiDevice;
+ }
- // check if this is a monkey test mode
- String monkeyVal = mParams.getString("monkey");
- if (monkeyVal != null) {
- // only if the monkey key is specified, we alter the state of monkey
- // else we should leave things as they are.
- getInstrumentation().getUiAutomation().setRunAsMonkey(Boolean.valueOf(monkeyVal));
- }
+ /**
+ * package private
+ * @param params
+ */
+ void setParams(Bundle params) {
+ mParams = params;
+ }
- UiDevice.getInstance().initialize(new InstrumentationUiAutomatorBridge(
- getInstrumentation().getContext(),
- getInstrumentation().getUiAutomation()));
+ void setAutomationSupport(IAutomationSupport automationSupport) {
+ mAutomationSupport = automationSupport;
}
/**
@@ -101,4 +123,28 @@ public class UiAutomatorTestCase extends InstrumentationTestCase {
public void sleep(long ms) {
SystemClock.sleep(ms);
}
+
+ private void setDummyIme() throws RemoteException {
+ IInputMethodManager im = IInputMethodManager.Stub.asInterface(ServiceManager
+ .getService(Context.INPUT_METHOD_SERVICE));
+ List<InputMethodInfo> infos = im.getInputMethodList();
+ String id = null;
+ for (InputMethodInfo info : infos) {
+ if (DUMMY_IME_PACKAGE.equals(info.getComponent().getPackageName())) {
+ id = info.getId();
+ }
+ }
+ if (id == null) {
+ throw new RuntimeException(String.format(
+ "Required testing fixture missing: IME package (%s)", DUMMY_IME_PACKAGE));
+ }
+ im.setInputMethod(null, id);
+ }
+
+ private void restoreActiveIme() throws RemoteException {
+ // TODO: figure out a way to restore active IME
+ // Currently retrieving active IME requires querying secure settings provider, which is hard
+ // to do without a Context; so the caveat here is that to make the post test device usable,
+ // the active IME needs to be manually switched.
+ }
}
diff --git a/foo/bar/ComplexDatabase.java b/foo/bar/ComplexDatabase.java
index 7f82dcd8..2c473424 100644
--- a/foo/bar/ComplexDatabase.java
+++ b/foo/bar/ComplexDatabase.java
@@ -93,6 +93,7 @@ public class ComplexDatabase_Impl extends ComplexDatabase {
@Override
public void clearAllTables() {
+ super.assertNotMainThread();
final SupportSQLiteDatabase _db = super.getOpenHelper().getWritableDatabase();
try {
super.beginTransaction();
@@ -101,7 +102,9 @@ public class ComplexDatabase_Impl extends ComplexDatabase {
} finally {
super.endTransaction();
_db.query("PRAGMA wal_checkpoint(FULL)").close();
- _db.execSQL("VACUUM");
+ if (!_db.inTransaction()) {
+ _db.execSQL("VACUUM");
+ }
}
}
diff --git a/java/lang/String.java b/java/lang/String.java
index 7e823479..de54882a 100644
--- a/java/lang/String.java
+++ b/java/lang/String.java
@@ -144,7 +144,7 @@ public final class String
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
- * <a href="{@docRoot}/../platform/serialization/spec/output.html">
+ * <a href="{@docRoot}openjdk-redirect.html?v=8&path=/platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
diff --git a/java/lang/invoke/CallSite.java b/java/lang/invoke/CallSite.java
index 85b4bb9f..1ff1eb84 100644
--- a/java/lang/invoke/CallSite.java
+++ b/java/lang/invoke/CallSite.java
@@ -25,363 +25,15 @@
package java.lang.invoke;
-// Android-changed: Not using Empty
-//import sun.invoke.empty.Empty;
-import static java.lang.invoke.MethodHandleStatics.*;
-import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
-
-/**
- * A {@code CallSite} is a holder for a variable {@link MethodHandle},
- * which is called its {@code target}.
- * An {@code invokedynamic} instruction linked to a {@code CallSite} delegates
- * all calls to the site's current target.
- * A {@code CallSite} may be associated with several {@code invokedynamic}
- * instructions, or it may be "free floating", associated with none.
- * In any case, it may be invoked through an associated method handle
- * called its {@linkplain #dynamicInvoker dynamic invoker}.
- * <p>
- * {@code CallSite} is an abstract class which does not allow
- * direct subclassing by users. It has three immediate,
- * concrete subclasses that may be either instantiated or subclassed.
- * <ul>
- * <li>If a mutable target is not required, an {@code invokedynamic} instruction
- * may be permanently bound by means of a {@linkplain ConstantCallSite constant call site}.
- * <li>If a mutable target is required which has volatile variable semantics,
- * because updates to the target must be immediately and reliably witnessed by other threads,
- * a {@linkplain VolatileCallSite volatile call site} may be used.
- * <li>Otherwise, if a mutable target is required,
- * a {@linkplain MutableCallSite mutable call site} may be used.
- * </ul>
- * <p>
- * A non-constant call site may be <em>relinked</em> by changing its target.
- * The new target must have the same {@linkplain MethodHandle#type() type}
- * as the previous target.
- * Thus, though a call site can be relinked to a series of
- * successive targets, it cannot change its type.
- * <p>
- * Here is a sample use of call sites and bootstrap methods which links every
- * dynamic call site to print its arguments:
-<blockquote><pre>{@code
-static void test() throws Throwable {
- // THE FOLLOWING LINE IS PSEUDOCODE FOR A JVM INSTRUCTION
- InvokeDynamic[#bootstrapDynamic].baz("baz arg", 2, 3.14);
-}
-private static void printArgs(Object... args) {
- System.out.println(java.util.Arrays.deepToString(args));
-}
-private static final MethodHandle printArgs;
-static {
- MethodHandles.Lookup lookup = MethodHandles.lookup();
- Class thisClass = lookup.lookupClass(); // (who am I?)
- printArgs = lookup.findStatic(thisClass,
- "printArgs", MethodType.methodType(void.class, Object[].class));
-}
-private static CallSite bootstrapDynamic(MethodHandles.Lookup caller, String name, MethodType type) {
- // ignore caller and name, but match the type:
- return new ConstantCallSite(printArgs.asType(type));
-}
-}</pre></blockquote>
- * @author John Rose, JSR 292 EG
- */
abstract
public class CallSite {
- // Android-changed: not used.
- // static { MethodHandleImpl.initStatics(); }
-
- // The actual payload of this call site:
- /*package-private*/
- MethodHandle target; // Note: This field is known to the JVM. Do not change.
-
- /**
- * Make a blank call site object with the given method type.
- * An initial target method is supplied which will throw
- * an {@link IllegalStateException} if called.
- * <p>
- * Before this {@code CallSite} object is returned from a bootstrap method,
- * it is usually provided with a more useful target method,
- * via a call to {@link CallSite#setTarget(MethodHandle) setTarget}.
- * @throws NullPointerException if the proposed type is null
- */
- /*package-private*/
- CallSite(MethodType type) {
- // Android-changed: No cache for these so create uninitializedCallSite target here using
- // method handle transformations to create a method handle that has the expected method
- // type but throws an IllegalStateException.
- // target = makeUninitializedCallSite(type);
- this.target = MethodHandles.throwException(type.returnType(), IllegalStateException.class);
- this.target = MethodHandles.insertArguments(
- this.target, 0, new IllegalStateException("uninitialized call site"));
- if (type.parameterCount() > 0) {
- this.target = MethodHandles.dropArguments(this.target, 0, type.ptypes());
- }
-
- // Android-changed: Using initializer method for GET_TARGET
- // rather than complex static initializer.
- initializeGetTarget();
- }
-
- /**
- * Make a call site object equipped with an initial target method handle.
- * @param target the method handle which will be the initial target of the call site
- * @throws NullPointerException if the proposed target is null
- */
- /*package-private*/
- CallSite(MethodHandle target) {
- target.type(); // null check
- this.target = target;
-
- // Android-changed: Using initializer method for GET_TARGET
- // rather than complex static initializer.
- initializeGetTarget();
- }
- /**
- * Make a call site object equipped with an initial target method handle.
- * @param targetType the desired type of the call site
- * @param createTargetHook a hook which will bind the call site to the target method handle
- * @throws WrongMethodTypeException if the hook cannot be invoked on the required arguments,
- * or if the target returned by the hook is not of the given {@code targetType}
- * @throws NullPointerException if the hook returns a null value
- * @throws ClassCastException if the hook returns something other than a {@code MethodHandle}
- * @throws Throwable anything else thrown by the hook function
- */
- /*package-private*/
- CallSite(MethodType targetType, MethodHandle createTargetHook) throws Throwable {
- this(targetType);
- ConstantCallSite selfCCS = (ConstantCallSite) this;
- MethodHandle boundTarget = (MethodHandle) createTargetHook.invokeWithArguments(selfCCS);
- checkTargetChange(this.target, boundTarget);
- this.target = boundTarget;
+ public MethodType type() { return null; }
- // Android-changed: Using initializer method for GET_TARGET
- // rather than complex static initializer.
- initializeGetTarget();
- }
-
- /**
- * Returns the type of this call site's target.
- * Although targets may change, any call site's type is permanent, and can never change to an unequal type.
- * The {@code setTarget} method enforces this invariant by refusing any new target that does
- * not have the previous target's type.
- * @return the type of the current target, which is also the type of any future target
- */
- public MethodType type() {
- // warning: do not call getTarget here, because CCS.getTarget can throw IllegalStateException
- return target.type();
- }
-
- /**
- * Returns the target method of the call site, according to the
- * behavior defined by this call site's specific class.
- * The immediate subclasses of {@code CallSite} document the
- * class-specific behaviors of this method.
- *
- * @return the current linkage state of the call site, its target method handle
- * @see ConstantCallSite
- * @see VolatileCallSite
- * @see #setTarget
- * @see ConstantCallSite#getTarget
- * @see MutableCallSite#getTarget
- * @see VolatileCallSite#getTarget
- */
public abstract MethodHandle getTarget();
- /**
- * Updates the target method of this call site, according to the
- * behavior defined by this call site's specific class.
- * The immediate subclasses of {@code CallSite} document the
- * class-specific behaviors of this method.
- * <p>
- * The type of the new target must be {@linkplain MethodType#equals equal to}
- * the type of the old target.
- *
- * @param newTarget the new target
- * @throws NullPointerException if the proposed new target is null
- * @throws WrongMethodTypeException if the proposed new target
- * has a method type that differs from the previous target
- * @see CallSite#getTarget
- * @see ConstantCallSite#setTarget
- * @see MutableCallSite#setTarget
- * @see VolatileCallSite#setTarget
- */
public abstract void setTarget(MethodHandle newTarget);
- void checkTargetChange(MethodHandle oldTarget, MethodHandle newTarget) {
- MethodType oldType = oldTarget.type();
- MethodType newType = newTarget.type(); // null check!
- if (!newType.equals(oldType))
- throw wrongTargetType(newTarget, oldType);
- }
-
- private static WrongMethodTypeException wrongTargetType(MethodHandle target, MethodType type) {
- return new WrongMethodTypeException(String.valueOf(target)+" should be of type "+type);
- }
-
- /**
- * Produces a method handle equivalent to an invokedynamic instruction
- * which has been linked to this call site.
- * <p>
- * This method is equivalent to the following code:
- * <blockquote><pre>{@code
- * MethodHandle getTarget, invoker, result;
- * getTarget = MethodHandles.publicLookup().bind(this, "getTarget", MethodType.methodType(MethodHandle.class));
- * invoker = MethodHandles.exactInvoker(this.type());
- * result = MethodHandles.foldArguments(invoker, getTarget)
- * }</pre></blockquote>
- *
- * @return a method handle which always invokes this call site's current target
- */
public abstract MethodHandle dynamicInvoker();
- /*non-public*/ MethodHandle makeDynamicInvoker() {
- // Android-changed: Use bindTo() rather than bindArgumentL() (not implemented).
- MethodHandle getTarget = GET_TARGET.bindTo(this);
- MethodHandle invoker = MethodHandles.exactInvoker(this.type());
- return MethodHandles.foldArguments(invoker, getTarget);
- }
-
- // Android-changed: no longer final. GET_TARGET assigned in initializeGetTarget().
- private static MethodHandle GET_TARGET = null;
-
- private void initializeGetTarget() {
- // Android-changed: moved from static initializer for
- // GET_TARGET to avoid issues with running early. Called from
- // constructors. CallSite creation is not performance critical.
- synchronized (CallSite.class) {
- if (GET_TARGET == null) {
- try {
- GET_TARGET = IMPL_LOOKUP.
- findVirtual(CallSite.class, "getTarget",
- MethodType.methodType(MethodHandle.class));
- } catch (ReflectiveOperationException e) {
- throw new InternalError(e);
- }
- }
- }
- }
-
- // Android-changed: not used.
- // /** This guy is rolled into the default target if a MethodType is supplied to the constructor. */
- // /*package-private*/
- // static Empty uninitializedCallSite() {
- // throw new IllegalStateException("uninitialized call site");
- // }
-
- // unsafe stuff:
- private static final long TARGET_OFFSET;
- static {
- try {
- TARGET_OFFSET = UNSAFE.objectFieldOffset(CallSite.class.getDeclaredField("target"));
- } catch (Exception ex) { throw new Error(ex); }
- }
-
- /*package-private*/
- void setTargetNormal(MethodHandle newTarget) {
- // Android-changed: Set value directly.
- // MethodHandleNatives.setCallSiteTargetNormal(this, newTarget);
- target = newTarget;
- }
- /*package-private*/
- MethodHandle getTargetVolatile() {
- return (MethodHandle) UNSAFE.getObjectVolatile(this, TARGET_OFFSET);
- }
- /*package-private*/
- void setTargetVolatile(MethodHandle newTarget) {
- // Android-changed: Set value directly.
- // MethodHandleNatives.setCallSiteTargetVolatile(this, newTarget);
- UNSAFE.putObjectVolatile(this, TARGET_OFFSET, newTarget);
- }
-
- // Android-changed: not used.
- // this implements the upcall from the JVM, MethodHandleNatives.makeDynamicCallSite:
- // static CallSite makeSite(MethodHandle bootstrapMethod,
- // // Callee information:
- // String name, MethodType type,
- // // Extra arguments for BSM, if any:
- // Object info,
- // // Caller information:
- // Class<?> callerClass) {
- // MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
- // CallSite site;
- // try {
- // Object binding;
- // info = maybeReBox(info);
- // if (info == null) {
- // binding = bootstrapMethod.invoke(caller, name, type);
- // } else if (!info.getClass().isArray()) {
- // binding = bootstrapMethod.invoke(caller, name, type, info);
- // } else {
- // Object[] argv = (Object[]) info;
- // maybeReBoxElements(argv);
- // switch (argv.length) {
- // case 0:
- // binding = bootstrapMethod.invoke(caller, name, type);
- // break;
- // case 1:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0]);
- // break;
- // case 2:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1]);
- // break;
- // case 3:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2]);
- // break;
- // case 4:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2], argv[3]);
- // break;
- // case 5:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2], argv[3], argv[4]);
- // break;
- // case 6:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]);
- // break;
- // default:
- // final int NON_SPREAD_ARG_COUNT = 3; // (caller, name, type)
- // if (NON_SPREAD_ARG_COUNT + argv.length > MethodType.MAX_MH_ARITY)
- // throw new BootstrapMethodError("too many bootstrap method arguments");
- // MethodType bsmType = bootstrapMethod.type();
- // MethodType invocationType = MethodType.genericMethodType(NON_SPREAD_ARG_COUNT + argv.length);
- // MethodHandle typedBSM = bootstrapMethod.asType(invocationType);
- // MethodHandle spreader = invocationType.invokers().spreadInvoker(NON_SPREAD_ARG_COUNT);
- // binding = spreader.invokeExact(typedBSM, (Object)caller, (Object)name, (Object)type, argv);
- // }
- // }
- // //System.out.println("BSM for "+name+type+" => "+binding);
- // if (binding instanceof CallSite) {
- // site = (CallSite) binding;
- // } else {
- // throw new ClassCastException("bootstrap method failed to produce a CallSite");
- // }
- // if (!site.getTarget().type().equals(type))
- // throw wrongTargetType(site.getTarget(), type);
- // } catch (Throwable ex) {
- // BootstrapMethodError bex;
- // if (ex instanceof BootstrapMethodError)
- // bex = (BootstrapMethodError) ex;
- // else
- // bex = new BootstrapMethodError("call site initialization exception", ex);
- // throw bex;
- // }
- // return site;
- // }
-
- // private static Object maybeReBox(Object x) {
- // if (x instanceof Integer) {
- // int xi = (int) x;
- // if (xi == (byte) xi)
- // x = xi; // must rebox; see JLS 5.1.7
- // }
- // return x;
- // }
- // private static void maybeReBoxElements(Object[] xa) {
- // for (int i = 0; i < xa.length; i++) {
- // xa[i] = maybeReBox(xa[i]);
- // }
- // }
}
diff --git a/java/lang/invoke/MethodHandle.java b/java/lang/invoke/MethodHandle.java
index cd09322b..159f9dd7 100644
--- a/java/lang/invoke/MethodHandle.java
+++ b/java/lang/invoke/MethodHandle.java
@@ -25,1355 +25,28 @@
package java.lang.invoke;
-
-import dalvik.system.EmulatedStackFrame;
-
-import static java.lang.invoke.MethodHandleStatics.*;
-
-/**
- * A method handle is a typed, directly executable reference to an underlying method,
- * constructor, field, or similar low-level operation, with optional
- * transformations of arguments or return values.
- * These transformations are quite general, and include such patterns as
- * {@linkplain #asType conversion},
- * {@linkplain #bindTo insertion},
- * {@linkplain java.lang.invoke.MethodHandles#dropArguments deletion},
- * and {@linkplain java.lang.invoke.MethodHandles#filterArguments substitution}.
- *
- * <h1>Method handle contents</h1>
- * Method handles are dynamically and strongly typed according to their parameter and return types.
- * They are not distinguished by the name or the defining class of their underlying methods.
- * A method handle must be invoked using a symbolic type descriptor which matches
- * the method handle's own {@linkplain #type type descriptor}.
- * <p>
- * Every method handle reports its type descriptor via the {@link #type type} accessor.
- * This type descriptor is a {@link java.lang.invoke.MethodType MethodType} object,
- * whose structure is a series of classes, one of which is
- * the return type of the method (or {@code void.class} if none).
- * <p>
- * A method handle's type controls the types of invocations it accepts,
- * and the kinds of transformations that apply to it.
- * <p>
- * A method handle contains a pair of special invoker methods
- * called {@link #invokeExact invokeExact} and {@link #invoke invoke}.
- * Both invoker methods provide direct access to the method handle's
- * underlying method, constructor, field, or other operation,
- * as modified by transformations of arguments and return values.
- * Both invokers accept calls which exactly match the method handle's own type.
- * The plain, inexact invoker also accepts a range of other call types.
- * <p>
- * Method handles are immutable and have no visible state.
- * Of course, they can be bound to underlying methods or data which exhibit state.
- * With respect to the Java Memory Model, any method handle will behave
- * as if all of its (internal) fields are final variables. This means that any method
- * handle made visible to the application will always be fully formed.
- * This is true even if the method handle is published through a shared
- * variable in a data race.
- * <p>
- * Method handles cannot be subclassed by the user.
- * Implementations may (or may not) create internal subclasses of {@code MethodHandle}
- * which may be visible via the {@link java.lang.Object#getClass Object.getClass}
- * operation. The programmer should not draw conclusions about a method handle
- * from its specific class, as the method handle class hierarchy (if any)
- * may change from time to time or across implementations from different vendors.
- *
- * <h1>Method handle compilation</h1>
- * A Java method call expression naming {@code invokeExact} or {@code invoke}
- * can invoke a method handle from Java source code.
- * From the viewpoint of source code, these methods can take any arguments
- * and their result can be cast to any return type.
- * Formally this is accomplished by giving the invoker methods
- * {@code Object} return types and variable arity {@code Object} arguments,
- * but they have an additional quality called <em>signature polymorphism</em>
- * which connects this freedom of invocation directly to the JVM execution stack.
- * <p>
- * As is usual with virtual methods, source-level calls to {@code invokeExact}
- * and {@code invoke} compile to an {@code invokevirtual} instruction.
- * More unusually, the compiler must record the actual argument types,
- * and may not perform method invocation conversions on the arguments.
- * Instead, it must push them on the stack according to their own unconverted types.
- * The method handle object itself is pushed on the stack before the arguments.
- * The compiler then calls the method handle with a symbolic type descriptor which
- * describes the argument and return types.
- * <p>
- * To issue a complete symbolic type descriptor, the compiler must also determine
- * the return type. This is based on a cast on the method invocation expression,
- * if there is one, or else {@code Object} if the invocation is an expression
- * or else {@code void} if the invocation is a statement.
- * The cast may be to a primitive type (but not {@code void}).
- * <p>
- * As a corner case, an uncasted {@code null} argument is given
- * a symbolic type descriptor of {@code java.lang.Void}.
- * The ambiguity with the type {@code Void} is harmless, since there are no references of type
- * {@code Void} except the null reference.
- *
- * <h1>Method handle invocation</h1>
- * The first time a {@code invokevirtual} instruction is executed
- * it is linked, by symbolically resolving the names in the instruction
- * and verifying that the method call is statically legal.
- * This is true of calls to {@code invokeExact} and {@code invoke}.
- * In this case, the symbolic type descriptor emitted by the compiler is checked for
- * correct syntax and names it contains are resolved.
- * Thus, an {@code invokevirtual} instruction which invokes
- * a method handle will always link, as long
- * as the symbolic type descriptor is syntactically well-formed
- * and the types exist.
- * <p>
- * When the {@code invokevirtual} is executed after linking,
- * the receiving method handle's type is first checked by the JVM
- * to ensure that it matches the symbolic type descriptor.
- * If the type match fails, it means that the method which the
- * caller is invoking is not present on the individual
- * method handle being invoked.
- * <p>
- * In the case of {@code invokeExact}, the type descriptor of the invocation
- * (after resolving symbolic type names) must exactly match the method type
- * of the receiving method handle.
- * In the case of plain, inexact {@code invoke}, the resolved type descriptor
- * must be a valid argument to the receiver's {@link #asType asType} method.
- * Thus, plain {@code invoke} is more permissive than {@code invokeExact}.
- * <p>
- * After type matching, a call to {@code invokeExact} directly
- * and immediately invoke the method handle's underlying method
- * (or other behavior, as the case may be).
- * <p>
- * A call to plain {@code invoke} works the same as a call to
- * {@code invokeExact}, if the symbolic type descriptor specified by the caller
- * exactly matches the method handle's own type.
- * If there is a type mismatch, {@code invoke} attempts
- * to adjust the type of the receiving method handle,
- * as if by a call to {@link #asType asType},
- * to obtain an exactly invokable method handle {@code M2}.
- * This allows a more powerful negotiation of method type
- * between caller and callee.
- * <p>
- * (<em>Note:</em> The adjusted method handle {@code M2} is not directly observable,
- * and implementations are therefore not required to materialize it.)
- *
- * <h1>Invocation checking</h1>
- * In typical programs, method handle type matching will usually succeed.
- * But if a match fails, the JVM will throw a {@link WrongMethodTypeException},
- * either directly (in the case of {@code invokeExact}) or indirectly as if
- * by a failed call to {@code asType} (in the case of {@code invoke}).
- * <p>
- * Thus, a method type mismatch which might show up as a linkage error
- * in a statically typed program can show up as
- * a dynamic {@code WrongMethodTypeException}
- * in a program which uses method handles.
- * <p>
- * Because method types contain "live" {@code Class} objects,
- * method type matching takes into account both types names and class loaders.
- * Thus, even if a method handle {@code M} is created in one
- * class loader {@code L1} and used in another {@code L2},
- * method handle calls are type-safe, because the caller's symbolic type
- * descriptor, as resolved in {@code L2},
- * is matched against the original callee method's symbolic type descriptor,
- * as resolved in {@code L1}.
- * The resolution in {@code L1} happens when {@code M} is created
- * and its type is assigned, while the resolution in {@code L2} happens
- * when the {@code invokevirtual} instruction is linked.
- * <p>
- * Apart from the checking of type descriptors,
- * a method handle's capability to call its underlying method is unrestricted.
- * If a method handle is formed on a non-public method by a class
- * that has access to that method, the resulting handle can be used
- * in any place by any caller who receives a reference to it.
- * <p>
- * Unlike with the Core Reflection API, where access is checked every time
- * a reflective method is invoked,
- * method handle access checking is performed
- * <a href="MethodHandles.Lookup.html#access">when the method handle is created</a>.
- * In the case of {@code ldc} (see below), access checking is performed as part of linking
- * the constant pool entry underlying the constant method handle.
- * <p>
- * Thus, handles to non-public methods, or to methods in non-public classes,
- * should generally be kept secret.
- * They should not be passed to untrusted code unless their use from
- * the untrusted code would be harmless.
- *
- * <h1>Method handle creation</h1>
- * Java code can create a method handle that directly accesses
- * any method, constructor, or field that is accessible to that code.
- * This is done via a reflective, capability-based API called
- * {@link java.lang.invoke.MethodHandles.Lookup MethodHandles.Lookup}
- * For example, a static method handle can be obtained
- * from {@link java.lang.invoke.MethodHandles.Lookup#findStatic Lookup.findStatic}.
- * There are also conversion methods from Core Reflection API objects,
- * such as {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect}.
- * <p>
- * Like classes and strings, method handles that correspond to accessible
- * fields, methods, and constructors can also be represented directly
- * in a class file's constant pool as constants to be loaded by {@code ldc} bytecodes.
- * A new type of constant pool entry, {@code CONSTANT_MethodHandle},
- * refers directly to an associated {@code CONSTANT_Methodref},
- * {@code CONSTANT_InterfaceMethodref}, or {@code CONSTANT_Fieldref}
- * constant pool entry.
- * (For full details on method handle constants,
- * see sections 4.4.8 and 5.4.3.5 of the Java Virtual Machine Specification.)
- * <p>
- * Method handles produced by lookups or constant loads from methods or
- * constructors with the variable arity modifier bit ({@code 0x0080})
- * have a corresponding variable arity, as if they were defined with
- * the help of {@link #asVarargsCollector asVarargsCollector}.
- * <p>
- * A method reference may refer either to a static or non-static method.
- * In the non-static case, the method handle type includes an explicit
- * receiver argument, prepended before any other arguments.
- * In the method handle's type, the initial receiver argument is typed
- * according to the class under which the method was initially requested.
- * (E.g., if a non-static method handle is obtained via {@code ldc},
- * the type of the receiver is the class named in the constant pool entry.)
- * <p>
- * Method handle constants are subject to the same link-time access checks
- * their corresponding bytecode instructions, and the {@code ldc} instruction
- * will throw corresponding linkage errors if the bytecode behaviors would
- * throw such errors.
- * <p>
- * As a corollary of this, access to protected members is restricted
- * to receivers only of the accessing class, or one of its subclasses,
- * and the accessing class must in turn be a subclass (or package sibling)
- * of the protected member's defining class.
- * If a method reference refers to a protected non-static method or field
- * of a class outside the current package, the receiver argument will
- * be narrowed to the type of the accessing class.
- * <p>
- * When a method handle to a virtual method is invoked, the method is
- * always looked up in the receiver (that is, the first argument).
- * <p>
- * A non-virtual method handle to a specific virtual method implementation
- * can also be created. These do not perform virtual lookup based on
- * receiver type. Such a method handle simulates the effect of
- * an {@code invokespecial} instruction to the same method.
- *
- * <h1>Usage examples</h1>
- * Here are some examples of usage:
- * <blockquote><pre>{@code
-Object x, y; String s; int i;
-MethodType mt; MethodHandle mh;
-MethodHandles.Lookup lookup = MethodHandles.lookup();
-// mt is (char,char)String
-mt = MethodType.methodType(String.class, char.class, char.class);
-mh = lookup.findVirtual(String.class, "replace", mt);
-s = (String) mh.invokeExact("daddy",'d','n');
-// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;
-assertEquals(s, "nanny");
-// weakly typed invocation (using MHs.invoke)
-s = (String) mh.invokeWithArguments("sappy", 'p', 'v');
-assertEquals(s, "savvy");
-// mt is (Object[])List
-mt = MethodType.methodType(java.util.List.class, Object[].class);
-mh = lookup.findStatic(java.util.Arrays.class, "asList", mt);
-assert(mh.isVarargsCollector());
-x = mh.invoke("one", "two");
-// invoke(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
-assertEquals(x, java.util.Arrays.asList("one","two"));
-// mt is (Object,Object,Object)Object
-mt = MethodType.genericMethodType(3);
-mh = mh.asType(mt);
-x = mh.invokeExact((Object)1, (Object)2, (Object)3);
-// invokeExact(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
-assertEquals(x, java.util.Arrays.asList(1,2,3));
-// mt is ()int
-mt = MethodType.methodType(int.class);
-mh = lookup.findVirtual(java.util.List.class, "size", mt);
-i = (int) mh.invokeExact(java.util.Arrays.asList(1,2,3));
-// invokeExact(Ljava/util/List;)I
-assert(i == 3);
-mt = MethodType.methodType(void.class, String.class);
-mh = lookup.findVirtual(java.io.PrintStream.class, "println", mt);
-mh.invokeExact(System.out, "Hello, world.");
-// invokeExact(Ljava/io/PrintStream;Ljava/lang/String;)V
- * }</pre></blockquote>
- * Each of the above calls to {@code invokeExact} or plain {@code invoke}
- * generates a single invokevirtual instruction with
- * the symbolic type descriptor indicated in the following comment.
- * In these examples, the helper method {@code assertEquals} is assumed to
- * be a method which calls {@link java.util.Objects#equals(Object,Object) Objects.equals}
- * on its arguments, and asserts that the result is true.
- *
- * <h1>Exceptions</h1>
- * The methods {@code invokeExact} and {@code invoke} are declared
- * to throw {@link java.lang.Throwable Throwable},
- * which is to say that there is no static restriction on what a method handle
- * can throw. Since the JVM does not distinguish between checked
- * and unchecked exceptions (other than by their class, of course),
- * there is no particular effect on bytecode shape from ascribing
- * checked exceptions to method handle invocations. But in Java source
- * code, methods which perform method handle calls must either explicitly
- * throw {@code Throwable}, or else must catch all
- * throwables locally, rethrowing only those which are legal in the context,
- * and wrapping ones which are illegal.
- *
- * <h1><a name="sigpoly"></a>Signature polymorphism</h1>
- * The unusual compilation and linkage behavior of
- * {@code invokeExact} and plain {@code invoke}
- * is referenced by the term <em>signature polymorphism</em>.
- * As defined in the Java Language Specification,
- * a signature polymorphic method is one which can operate with
- * any of a wide range of call signatures and return types.
- * <p>
- * In source code, a call to a signature polymorphic method will
- * compile, regardless of the requested symbolic type descriptor.
- * As usual, the Java compiler emits an {@code invokevirtual}
- * instruction with the given symbolic type descriptor against the named method.
- * The unusual part is that the symbolic type descriptor is derived from
- * the actual argument and return types, not from the method declaration.
- * <p>
- * When the JVM processes bytecode containing signature polymorphic calls,
- * it will successfully link any such call, regardless of its symbolic type descriptor.
- * (In order to retain type safety, the JVM will guard such calls with suitable
- * dynamic type checks, as described elsewhere.)
- * <p>
- * Bytecode generators, including the compiler back end, are required to emit
- * untransformed symbolic type descriptors for these methods.
- * Tools which determine symbolic linkage are required to accept such
- * untransformed descriptors, without reporting linkage errors.
- *
- * <h1>Interoperation between method handles and the Core Reflection API</h1>
- * Using factory methods in the {@link java.lang.invoke.MethodHandles.Lookup Lookup} API,
- * any class member represented by a Core Reflection API object
- * can be converted to a behaviorally equivalent method handle.
- * For example, a reflective {@link java.lang.reflect.Method Method} can
- * be converted to a method handle using
- * {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect}.
- * The resulting method handles generally provide more direct and efficient
- * access to the underlying class members.
- * <p>
- * As a special case,
- * when the Core Reflection API is used to view the signature polymorphic
- * methods {@code invokeExact} or plain {@code invoke} in this class,
- * they appear as ordinary non-polymorphic methods.
- * Their reflective appearance, as viewed by
- * {@link java.lang.Class#getDeclaredMethod Class.getDeclaredMethod},
- * is unaffected by their special status in this API.
- * For example, {@link java.lang.reflect.Method#getModifiers Method.getModifiers}
- * will report exactly those modifier bits required for any similarly
- * declared method, including in this case {@code native} and {@code varargs} bits.
- * <p>
- * As with any reflected method, these methods (when reflected) may be
- * invoked via {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}.
- * However, such reflective calls do not result in method handle invocations.
- * Such a call, if passed the required argument
- * (a single one, of type {@code Object[]}), will ignore the argument and
- * will throw an {@code UnsupportedOperationException}.
- * <p>
- * Since {@code invokevirtual} instructions can natively
- * invoke method handles under any symbolic type descriptor, this reflective view conflicts
- * with the normal presentation of these methods via bytecodes.
- * Thus, these two native methods, when reflectively viewed by
- * {@code Class.getDeclaredMethod}, may be regarded as placeholders only.
- * <p>
- * In order to obtain an invoker method for a particular type descriptor,
- * use {@link java.lang.invoke.MethodHandles#exactInvoker MethodHandles.exactInvoker},
- * or {@link java.lang.invoke.MethodHandles#invoker MethodHandles.invoker}.
- * The {@link java.lang.invoke.MethodHandles.Lookup#findVirtual Lookup.findVirtual}
- * API is also able to return a method handle
- * to call {@code invokeExact} or plain {@code invoke},
- * for any specified type descriptor .
- *
- * <h1>Interoperation between method handles and Java generics</h1>
- * A method handle can be obtained on a method, constructor, or field
- * which is declared with Java generic types.
- * As with the Core Reflection API, the type of the method handle
- * will constructed from the erasure of the source-level type.
- * When a method handle is invoked, the types of its arguments
- * or the return value cast type may be generic types or type instances.
- * If this occurs, the compiler will replace those
- * types by their erasures when it constructs the symbolic type descriptor
- * for the {@code invokevirtual} instruction.
- * <p>
- * Method handles do not represent
- * their function-like types in terms of Java parameterized (generic) types,
- * because there are three mismatches between function-like types and parameterized
- * Java types.
- * <ul>
- * <li>Method types range over all possible arities,
- * from no arguments to up to the <a href="MethodHandle.html#maxarity">maximum number</a> of allowed arguments.
- * Generics are not variadic, and so cannot represent this.</li>
- * <li>Method types can specify arguments of primitive types,
- * which Java generic types cannot range over.</li>
- * <li>Higher order functions over method handles (combinators) are
- * often generic across a wide range of function types, including
- * those of multiple arities. It is impossible to represent such
- * genericity with a Java type parameter.</li>
- * </ul>
- *
- * <h1><a name="maxarity"></a>Arity limits</h1>
- * The JVM imposes on all methods and constructors of any kind an absolute
- * limit of 255 stacked arguments. This limit can appear more restrictive
- * in certain cases:
- * <ul>
- * <li>A {@code long} or {@code double} argument counts (for purposes of arity limits) as two argument slots.
- * <li>A non-static method consumes an extra argument for the object on which the method is called.
- * <li>A constructor consumes an extra argument for the object which is being constructed.
- * <li>Since a method handle&rsquo;s {@code invoke} method (or other signature-polymorphic method) is non-virtual,
- * it consumes an extra argument for the method handle itself, in addition to any non-virtual receiver object.
- * </ul>
- * These limits imply that certain method handles cannot be created, solely because of the JVM limit on stacked arguments.
- * For example, if a static JVM method accepts exactly 255 arguments, a method handle cannot be created for it.
- * Attempts to create method handles with impossible method types lead to an {@link IllegalArgumentException}.
- * In particular, a method handle&rsquo;s type must not have an arity of the exact maximum 255.
- *
- * @see MethodType
- * @see MethodHandles
- * @author John Rose, JSR 292 EG
- */
public abstract class MethodHandle {
- // Android-changed:
- //
- // static { MethodHandleImpl.initStatics(); }
- //
- // LambdaForm and customizationCount are currently unused in our implementation
- // and will be substituted with appropriate implementation / delegate classes.
- //
- // /*private*/ final LambdaForm form;
- // form is not private so that invokers can easily fetch it
- // /*non-public*/ byte customizationCount;
- // customizationCount should be accessible from invokers
-
-
- /**
- * Internal marker interface which distinguishes (to the Java compiler)
- * those methods which are <a href="MethodHandle.html#sigpoly">signature polymorphic</a>.
- *
- * @hide
- */
- @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
- public @interface PolymorphicSignature { }
-
- /**
- * The type of this method handle, this corresponds to the exact type of the method
- * being invoked.
- */
- private final MethodType type;
-
- /**
- * The nominal type of this method handle, will be non-null if a method handle declares
- * a different type from its "real" type, which is either the type of the method being invoked
- * or the type of the emulated stackframe expected by an underyling adapter.
- */
- private MethodType nominalType;
-
- /**
- * The spread invoker associated with this type with zero trailing arguments.
- * This is used to speed up invokeWithArguments.
- */
- private MethodHandle cachedSpreadInvoker;
-
- /**
- * The INVOKE* constants and SGET/SPUT and IGET/IPUT constants specify the behaviour of this
- * method handle with respect to the ArtField* or the ArtMethod* that it operates on. These
- * behaviours are equivalent to the dex bytecode behaviour on the respective method_id or
- * field_id in the equivalent instruction.
- *
- * INVOKE_TRANSFORM is a special type of handle which doesn't encode any dex bytecode behaviour,
- * instead it transforms the list of input arguments or performs other higher order operations
- * before (optionally) delegating to another method handle.
- *
- * INVOKE_CALLSITE_TRANSFORM is a variation on INVOKE_TRANSFORM where the method type of
- * a MethodHandle dynamically varies based on the callsite. This is used by
- * the VarargsCollector implementation which places any number of trailing arguments
- * into an array before invoking an arity method. The "any number of trailing arguments" means
- * it would otherwise generate WrongMethodTypeExceptions as the callsite method type and
- * VarargsCollector method type appear incompatible.
- */
-
- /** @hide */ public static final int INVOKE_VIRTUAL = 0;
- /** @hide */ public static final int INVOKE_SUPER = 1;
- /** @hide */ public static final int INVOKE_DIRECT = 2;
- /** @hide */ public static final int INVOKE_STATIC = 3;
- /** @hide */ public static final int INVOKE_INTERFACE = 4;
- /** @hide */ public static final int INVOKE_TRANSFORM = 5;
- /** @hide */ public static final int INVOKE_CALLSITE_TRANSFORM = 6;
- /** @hide */ public static final int INVOKE_VAR_HANDLE = 7;
- /** @hide */ public static final int INVOKE_VAR_HANDLE_EXACT = 8;
- /** @hide */ public static final int IGET = 9;
- /** @hide */ public static final int IPUT = 10;
- /** @hide */ public static final int SGET = 11;
- /** @hide */ public static final int SPUT = 12;
-
- // The kind of this method handle (used by the runtime). This is one of the INVOKE_*
- // constants or SGET/SPUT, IGET/IPUT.
- /** @hide */ protected final int handleKind;
-
- // The ArtMethod* or ArtField* associated with this method handle (used by the runtime).
- /** @hide */ protected final long artFieldOrMethod;
-
- /** @hide */
- protected MethodHandle(long artFieldOrMethod, int handleKind, MethodType type) {
- this.artFieldOrMethod = artFieldOrMethod;
- this.handleKind = handleKind;
- this.type = type;
- }
-
- /**
- * Reports the type of this method handle.
- * Every invocation of this method handle via {@code invokeExact} must exactly match this type.
- * @return the method handle type
- */
- public MethodType type() {
- if (nominalType != null) {
- return nominalType;
- }
-
- return type;
- }
-
- /**
- * Invokes the method handle, allowing any caller type descriptor, but requiring an exact type match.
- * The symbolic type descriptor at the call site of {@code invokeExact} must
- * exactly match this method handle's {@link #type type}.
- * No conversions are allowed on arguments or return values.
- * <p>
- * When this method is observed via the Core Reflection API,
- * it will appear as a single native method, taking an object array and returning an object.
- * If this native method is invoked directly via
- * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
- * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
- * it will throw an {@code UnsupportedOperationException}.
- * @param args the signature-polymorphic parameter list, statically represented using varargs
- * @return the signature-polymorphic result, statically represented using {@code Object}
- * @throws WrongMethodTypeException if the target's type is not identical with the caller's symbolic type descriptor
- * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
- */
- public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;
-
- /**
- * Invokes the method handle, allowing any caller type descriptor,
- * and optionally performing conversions on arguments and return values.
- * <p>
- * If the call site's symbolic type descriptor exactly matches this method handle's {@link #type type},
- * the call proceeds as if by {@link #invokeExact invokeExact}.
- * <p>
- * Otherwise, the call proceeds as if this method handle were first
- * adjusted by calling {@link #asType asType} to adjust this method handle
- * to the required type, and then the call proceeds as if by
- * {@link #invokeExact invokeExact} on the adjusted method handle.
- * <p>
- * There is no guarantee that the {@code asType} call is actually made.
- * If the JVM can predict the results of making the call, it may perform
- * adaptations directly on the caller's arguments,
- * and call the target method handle according to its own exact type.
- * <p>
- * The resolved type descriptor at the call site of {@code invoke} must
- * be a valid argument to the receivers {@code asType} method.
- * In particular, the caller must specify the same argument arity
- * as the callee's type,
- * if the callee is not a {@linkplain #asVarargsCollector variable arity collector}.
- * <p>
- * When this method is observed via the Core Reflection API,
- * it will appear as a single native method, taking an object array and returning an object.
- * If this native method is invoked directly via
- * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
- * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
- * it will throw an {@code UnsupportedOperationException}.
- * @param args the signature-polymorphic parameter list, statically represented using varargs
- * @return the signature-polymorphic result, statically represented using {@code Object}
- * @throws WrongMethodTypeException if the target's type cannot be adjusted to the caller's symbolic type descriptor
- * @throws ClassCastException if the target's type can be adjusted to the caller, but a reference cast fails
- * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
- */
- public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;
-
- // Android-changed: Removed implementation details.
- //
- // /*non-public*/ final native @PolymorphicSignature Object invokeBasic(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToVirtual(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToStatic(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToSpecial(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToInterface(Object... args)
-
- /**
- * Performs a variable arity invocation, passing the arguments in the given list
- * to the method handle, as if via an inexact {@link #invoke invoke} from a call site
- * which mentions only the type {@code Object}, and whose arity is the length
- * of the argument list.
- * <p>
- * Specifically, execution proceeds as if by the following steps,
- * although the methods are not guaranteed to be called if the JVM
- * can predict their effects.
- * <ul>
- * <li>Determine the length of the argument array as {@code N}.
- * For a null reference, {@code N=0}. </li>
- * <li>Determine the general type {@code TN} of {@code N} arguments as
- * as {@code TN=MethodType.genericMethodType(N)}.</li>
- * <li>Force the original target method handle {@code MH0} to the
- * required type, as {@code MH1 = MH0.asType(TN)}. </li>
- * <li>Spread the array into {@code N} separate arguments {@code A0, ...}. </li>
- * <li>Invoke the type-adjusted method handle on the unpacked arguments:
- * MH1.invokeExact(A0, ...). </li>
- * <li>Take the return value as an {@code Object} reference. </li>
- * </ul>
- * <p>
- * Because of the action of the {@code asType} step, the following argument
- * conversions are applied as necessary:
- * <ul>
- * <li>reference casting
- * <li>unboxing
- * <li>widening primitive conversions
- * </ul>
- * <p>
- * The result returned by the call is boxed if it is a primitive,
- * or forced to null if the return type is void.
- * <p>
- * This call is equivalent to the following code:
- * <blockquote><pre>{@code
- * MethodHandle invoker = MethodHandles.spreadInvoker(this.type(), 0);
- * Object result = invoker.invokeExact(this, arguments);
- * }</pre></blockquote>
- * <p>
- * Unlike the signature polymorphic methods {@code invokeExact} and {@code invoke},
- * {@code invokeWithArguments} can be accessed normally via the Core Reflection API and JNI.
- * It can therefore be used as a bridge between native or reflective code and method handles.
- *
- * @param arguments the arguments to pass to the target
- * @return the result returned by the target
- * @throws ClassCastException if an argument cannot be converted by reference casting
- * @throws WrongMethodTypeException if the target's type cannot be adjusted to take the given number of {@code Object} arguments
- * @throws Throwable anything thrown by the target method invocation
- * @see MethodHandles#spreadInvoker
- */
- public Object invokeWithArguments(Object... arguments) throws Throwable {
- MethodHandle invoker = null;
- synchronized (this) {
- if (cachedSpreadInvoker == null) {
- cachedSpreadInvoker = MethodHandles.spreadInvoker(this.type(), 0);
- }
-
- invoker = cachedSpreadInvoker;
- }
-
- return invoker.invoke(this, arguments);
- }
-
- /**
- * Performs a variable arity invocation, passing the arguments in the given array
- * to the method handle, as if via an inexact {@link #invoke invoke} from a call site
- * which mentions only the type {@code Object}, and whose arity is the length
- * of the argument array.
- * <p>
- * This method is also equivalent to the following code:
- * <blockquote><pre>{@code
- * invokeWithArguments(arguments.toArray()
- * }</pre></blockquote>
- *
- * @param arguments the arguments to pass to the target
- * @return the result returned by the target
- * @throws NullPointerException if {@code arguments} is a null reference
- * @throws ClassCastException if an argument cannot be converted by reference casting
- * @throws WrongMethodTypeException if the target's type cannot be adjusted to take the given number of {@code Object} arguments
- * @throws Throwable anything thrown by the target method invocation
- */
- public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable {
- return invokeWithArguments(arguments.toArray());
- }
-
- /**
- * Produces an adapter method handle which adapts the type of the
- * current method handle to a new type.
- * The resulting method handle is guaranteed to report a type
- * which is equal to the desired new type.
- * <p>
- * If the original type and new type are equal, returns {@code this}.
- * <p>
- * The new method handle, when invoked, will perform the following
- * steps:
- * <ul>
- * <li>Convert the incoming argument list to match the original
- * method handle's argument list.
- * <li>Invoke the original method handle on the converted argument list.
- * <li>Convert any result returned by the original method handle
- * to the return type of new method handle.
- * </ul>
- * <p>
- * This method provides the crucial behavioral difference between
- * {@link #invokeExact invokeExact} and plain, inexact {@link #invoke invoke}.
- * The two methods
- * perform the same steps when the caller's type descriptor exactly m atches
- * the callee's, but when the types differ, plain {@link #invoke invoke}
- * also calls {@code asType} (or some internal equivalent) in order
- * to match up the caller's and callee's types.
- * <p>
- * If the current method is a variable arity method handle
- * argument list conversion may involve the conversion and collection
- * of several arguments into an array, as
- * {@linkplain #asVarargsCollector described elsewhere}.
- * In every other case, all conversions are applied <em>pairwise</em>,
- * which means that each argument or return value is converted to
- * exactly one argument or return value (or no return value).
- * The applied conversions are defined by consulting the
- * the corresponding component types of the old and new
- * method handle types.
- * <p>
- * Let <em>T0</em> and <em>T1</em> be corresponding new and old parameter types,
- * or old and new return types. Specifically, for some valid index {@code i}, let
- * <em>T0</em>{@code =newType.parameterType(i)} and <em>T1</em>{@code =this.type().parameterType(i)}.
- * Or else, going the other way for return values, let
- * <em>T0</em>{@code =this.type().returnType()} and <em>T1</em>{@code =newType.returnType()}.
- * If the types are the same, the new method handle makes no change
- * to the corresponding argument or return value (if any).
- * Otherwise, one of the following conversions is applied
- * if possible:
- * <ul>
- * <li>If <em>T0</em> and <em>T1</em> are references, then a cast to <em>T1</em> is applied.
- * (The types do not need to be related in any particular way.
- * This is because a dynamic value of null can convert to any reference type.)
- * <li>If <em>T0</em> and <em>T1</em> are primitives, then a Java method invocation
- * conversion (JLS 5.3) is applied, if one exists.
- * (Specifically, <em>T0</em> must convert to <em>T1</em> by a widening primitive conversion.)
- * <li>If <em>T0</em> is a primitive and <em>T1</em> a reference,
- * a Java casting conversion (JLS 5.5) is applied if one exists.
- * (Specifically, the value is boxed from <em>T0</em> to its wrapper class,
- * which is then widened as needed to <em>T1</em>.)
- * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive, an unboxing
- * conversion will be applied at runtime, possibly followed
- * by a Java method invocation conversion (JLS 5.3)
- * on the primitive value. (These are the primitive widening conversions.)
- * <em>T0</em> must be a wrapper class or a supertype of one.
- * (In the case where <em>T0</em> is Object, these are the conversions
- * allowed by {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}.)
- * The unboxing conversion must have a possibility of success, which means that
- * if <em>T0</em> is not itself a wrapper class, there must exist at least one
- * wrapper class <em>TW</em> which is a subtype of <em>T0</em> and whose unboxed
- * primitive value can be widened to <em>T1</em>.
- * <li>If the return type <em>T1</em> is marked as void, any returned value is discarded
- * <li>If the return type <em>T0</em> is void and <em>T1</em> a reference, a null value is introduced.
- * <li>If the return type <em>T0</em> is void and <em>T1</em> a primitive,
- * a zero value is introduced.
- * </ul>
- * (<em>Note:</em> Both <em>T0</em> and <em>T1</em> may be regarded as static types,
- * because neither corresponds specifically to the <em>dynamic type</em> of any
- * actual argument or return value.)
- * <p>
- * The method handle conversion cannot be made if any one of the required
- * pairwise conversions cannot be made.
- * <p>
- * At runtime, the conversions applied to reference arguments
- * or return values may require additional runtime checks which can fail.
- * An unboxing operation may fail because the original reference is null,
- * causing a {@link java.lang.NullPointerException NullPointerException}.
- * An unboxing operation or a reference cast may also fail on a reference
- * to an object of the wrong type,
- * causing a {@link java.lang.ClassCastException ClassCastException}.
- * Although an unboxing operation may accept several kinds of wrappers,
- * if none are available, a {@code ClassCastException} will be thrown.
- *
- * @param newType the expected type of the new method handle
- * @return a method handle which delegates to {@code this} after performing
- * any necessary argument conversions, and arranges for any
- * necessary return value conversions
- * @throws NullPointerException if {@code newType} is a null reference
- * @throws WrongMethodTypeException if the conversion cannot be made
- * @see MethodHandles#explicitCastArguments
- */
- public MethodHandle asType(MethodType newType) {
- // Fast path alternative to a heavyweight {@code asType} call.
- // Return 'this' if the conversion will be a no-op.
- if (newType == type) {
- return this;
- }
-
- if (!type.isConvertibleTo(newType)) {
- throw new WrongMethodTypeException("cannot convert " + this + " to " + newType);
- }
-
- MethodHandle mh = duplicate();
- mh.nominalType = newType;
- return mh;
- }
-
- /**
- * Makes an <em>array-spreading</em> method handle, which accepts a trailing array argument
- * and spreads its elements as positional arguments.
- * The new method handle adapts, as its <i>target</i>,
- * the current method handle. The type of the adapter will be
- * the same as the type of the target, except that the final
- * {@code arrayLength} parameters of the target's type are replaced
- * by a single array parameter of type {@code arrayType}.
- * <p>
- * If the array element type differs from any of the corresponding
- * argument types on the original target,
- * the original target is adapted to take the array elements directly,
- * as if by a call to {@link #asType asType}.
- * <p>
- * When called, the adapter replaces a trailing array argument
- * by the array's elements, each as its own argument to the target.
- * (The order of the arguments is preserved.)
- * They are converted pairwise by casting and/or unboxing
- * to the types of the trailing parameters of the target.
- * Finally the target is called.
- * What the target eventually returns is returned unchanged by the adapter.
- * <p>
- * Before calling the target, the adapter verifies that the array
- * contains exactly enough elements to provide a correct argument count
- * to the target method handle.
- * (The array may also be null when zero elements are required.)
- * <p>
- * If, when the adapter is called, the supplied array argument does
- * not have the correct number of elements, the adapter will throw
- * an {@link IllegalArgumentException} instead of invoking the target.
- * <p>
- * Here are some simple examples of array-spreading method handles:
- * <blockquote><pre>{@code
-MethodHandle equals = publicLookup()
- .findVirtual(String.class, "equals", methodType(boolean.class, Object.class));
-assert( (boolean) equals.invokeExact("me", (Object)"me"));
-assert(!(boolean) equals.invokeExact("me", (Object)"thee"));
-// spread both arguments from a 2-array:
-MethodHandle eq2 = equals.asSpreader(Object[].class, 2);
-assert( (boolean) eq2.invokeExact(new Object[]{ "me", "me" }));
-assert(!(boolean) eq2.invokeExact(new Object[]{ "me", "thee" }));
-// try to spread from anything but a 2-array:
-for (int n = 0; n <= 10; n++) {
- Object[] badArityArgs = (n == 2 ? null : new Object[n]);
- try { assert((boolean) eq2.invokeExact(badArityArgs) && false); }
- catch (IllegalArgumentException ex) { } // OK
-}
-// spread both arguments from a String array:
-MethodHandle eq2s = equals.asSpreader(String[].class, 2);
-assert( (boolean) eq2s.invokeExact(new String[]{ "me", "me" }));
-assert(!(boolean) eq2s.invokeExact(new String[]{ "me", "thee" }));
-// spread second arguments from a 1-array:
-MethodHandle eq1 = equals.asSpreader(Object[].class, 1);
-assert( (boolean) eq1.invokeExact("me", new Object[]{ "me" }));
-assert(!(boolean) eq1.invokeExact("me", new Object[]{ "thee" }));
-// spread no arguments from a 0-array or null:
-MethodHandle eq0 = equals.asSpreader(Object[].class, 0);
-assert( (boolean) eq0.invokeExact("me", (Object)"me", new Object[0]));
-assert(!(boolean) eq0.invokeExact("me", (Object)"thee", (Object[])null));
-// asSpreader and asCollector are approximate inverses:
-for (int n = 0; n <= 2; n++) {
- for (Class<?> a : new Class<?>[]{Object[].class, String[].class, CharSequence[].class}) {
- MethodHandle equals2 = equals.asSpreader(a, n).asCollector(a, n);
- assert( (boolean) equals2.invokeWithArguments("me", "me"));
- assert(!(boolean) equals2.invokeWithArguments("me", "thee"));
- }
-}
-MethodHandle caToString = publicLookup()
- .findStatic(Arrays.class, "toString", methodType(String.class, char[].class));
-assertEquals("[A, B, C]", (String) caToString.invokeExact("ABC".toCharArray()));
-MethodHandle caString3 = caToString.asCollector(char[].class, 3);
-assertEquals("[A, B, C]", (String) caString3.invokeExact('A', 'B', 'C'));
-MethodHandle caToString2 = caString3.asSpreader(char[].class, 2);
-assertEquals("[A, B, C]", (String) caToString2.invokeExact('A', "BC".toCharArray()));
- * }</pre></blockquote>
- * @param arrayType usually {@code Object[]}, the type of the array argument from which to extract the spread arguments
- * @param arrayLength the number of arguments to spread from an incoming array argument
- * @return a new method handle which spreads its final array argument,
- * before calling the original method handle
- * @throws NullPointerException if {@code arrayType} is a null reference
- * @throws IllegalArgumentException if {@code arrayType} is not an array type,
- * or if target does not have at least
- * {@code arrayLength} parameter types,
- * or if {@code arrayLength} is negative,
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- * @throws WrongMethodTypeException if the implied {@code asType} call fails
- * @see #asCollector
- */
- public MethodHandle asSpreader(Class<?> arrayType, int arrayLength) {
- MethodType postSpreadType = asSpreaderChecks(arrayType, arrayLength);
-
- final int targetParamCount = postSpreadType.parameterCount();
- MethodType dropArrayArgs = postSpreadType.dropParameterTypes(
- (targetParamCount - arrayLength), targetParamCount);
- MethodType adapterType = dropArrayArgs.appendParameterTypes(arrayType);
-
- return new Transformers.Spreader(this, adapterType, arrayLength);
- }
-
- /**
- * See if {@code asSpreader} can be validly called with the given arguments.
- * Return the type of the method handle call after spreading but before conversions.
- */
- private MethodType asSpreaderChecks(Class<?> arrayType, int arrayLength) {
- spreadArrayChecks(arrayType, arrayLength);
- int nargs = type().parameterCount();
- if (nargs < arrayLength || arrayLength < 0)
- throw newIllegalArgumentException("bad spread array length");
- Class<?> arrayElement = arrayType.getComponentType();
- MethodType mtype = type();
- boolean match = true, fail = false;
- for (int i = nargs - arrayLength; i < nargs; i++) {
- Class<?> ptype = mtype.parameterType(i);
- if (ptype != arrayElement) {
- match = false;
- if (!MethodType.canConvert(arrayElement, ptype)) {
- fail = true;
- break;
- }
- }
- }
- if (match) return mtype;
- MethodType needType = mtype.asSpreaderType(arrayType, arrayLength);
- if (!fail) return needType;
- // elicit an error:
- this.asType(needType);
- throw newInternalError("should not return", null);
- }
-
- private void spreadArrayChecks(Class<?> arrayType, int arrayLength) {
- Class<?> arrayElement = arrayType.getComponentType();
- if (arrayElement == null)
- throw newIllegalArgumentException("not an array type", arrayType);
- if ((arrayLength & 0x7F) != arrayLength) {
- if ((arrayLength & 0xFF) != arrayLength)
- throw newIllegalArgumentException("array length is not legal", arrayLength);
- assert(arrayLength >= 128);
- if (arrayElement == long.class ||
- arrayElement == double.class)
- throw newIllegalArgumentException("array length is not legal for long[] or double[]", arrayLength);
- }
- }
-
- /**
- * Makes an <em>array-collecting</em> method handle, which accepts a given number of trailing
- * positional arguments and collects them into an array argument.
- * The new method handle adapts, as its <i>target</i>,
- * the current method handle. The type of the adapter will be
- * the same as the type of the target, except that a single trailing
- * parameter (usually of type {@code arrayType}) is replaced by
- * {@code arrayLength} parameters whose type is element type of {@code arrayType}.
- * <p>
- * If the array type differs from the final argument type on the original target,
- * the original target is adapted to take the array type directly,
- * as if by a call to {@link #asType asType}.
- * <p>
- * When called, the adapter replaces its trailing {@code arrayLength}
- * arguments by a single new array of type {@code arrayType}, whose elements
- * comprise (in order) the replaced arguments.
- * Finally the target is called.
- * What the target eventually returns is returned unchanged by the adapter.
- * <p>
- * (The array may also be a shared constant when {@code arrayLength} is zero.)
- * <p>
- * (<em>Note:</em> The {@code arrayType} is often identical to the last
- * parameter type of the original target.
- * It is an explicit argument for symmetry with {@code asSpreader}, and also
- * to allow the target to use a simple {@code Object} as its last parameter type.)
- * <p>
- * In order to create a collecting adapter which is not restricted to a particular
- * number of collected arguments, use {@link #asVarargsCollector asVarargsCollector} instead.
- * <p>
- * Here are some examples of array-collecting method handles:
- * <blockquote><pre>{@code
-MethodHandle deepToString = publicLookup()
- .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-assertEquals("[won]", (String) deepToString.invokeExact(new Object[]{"won"}));
-MethodHandle ts1 = deepToString.asCollector(Object[].class, 1);
-assertEquals(methodType(String.class, Object.class), ts1.type());
-//assertEquals("[won]", (String) ts1.invokeExact( new Object[]{"won"})); //FAIL
-assertEquals("[[won]]", (String) ts1.invokeExact((Object) new Object[]{"won"}));
-// arrayType can be a subtype of Object[]
-MethodHandle ts2 = deepToString.asCollector(String[].class, 2);
-assertEquals(methodType(String.class, String.class, String.class), ts2.type());
-assertEquals("[two, too]", (String) ts2.invokeExact("two", "too"));
-MethodHandle ts0 = deepToString.asCollector(Object[].class, 0);
-assertEquals("[]", (String) ts0.invokeExact());
-// collectors can be nested, Lisp-style
-MethodHandle ts22 = deepToString.asCollector(Object[].class, 3).asCollector(String[].class, 2);
-assertEquals("[A, B, [C, D]]", ((String) ts22.invokeExact((Object)'A', (Object)"B", "C", "D")));
-// arrayType can be any primitive array type
-MethodHandle bytesToString = publicLookup()
- .findStatic(Arrays.class, "toString", methodType(String.class, byte[].class))
- .asCollector(byte[].class, 3);
-assertEquals("[1, 2, 3]", (String) bytesToString.invokeExact((byte)1, (byte)2, (byte)3));
-MethodHandle longsToString = publicLookup()
- .findStatic(Arrays.class, "toString", methodType(String.class, long[].class))
- .asCollector(long[].class, 1);
-assertEquals("[123]", (String) longsToString.invokeExact((long)123));
- * }</pre></blockquote>
- * @param arrayType often {@code Object[]}, the type of the array argument which will collect the arguments
- * @param arrayLength the number of arguments to collect into a new array argument
- * @return a new method handle which collects some trailing argument
- * into an array, before calling the original method handle
- * @throws NullPointerException if {@code arrayType} is a null reference
- * @throws IllegalArgumentException if {@code arrayType} is not an array type
- * or {@code arrayType} is not assignable to this method handle's trailing parameter type,
- * or {@code arrayLength} is not a legal array size,
- * or the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- * @throws WrongMethodTypeException if the implied {@code asType} call fails
- * @see #asSpreader
- * @see #asVarargsCollector
- */
- public MethodHandle asCollector(Class<?> arrayType, int arrayLength) {
- asCollectorChecks(arrayType, arrayLength);
-
- return new Transformers.Collector(this, arrayType, arrayLength);
- }
-
- /**
- * See if {@code asCollector} can be validly called with the given arguments.
- * Return false if the last parameter is not an exact match to arrayType.
- */
- /*non-public*/ boolean asCollectorChecks(Class<?> arrayType, int arrayLength) {
- spreadArrayChecks(arrayType, arrayLength);
- int nargs = type().parameterCount();
- if (nargs != 0) {
- Class<?> lastParam = type().parameterType(nargs-1);
- if (lastParam == arrayType) return true;
- if (lastParam.isAssignableFrom(arrayType)) return false;
- }
- throw newIllegalArgumentException("array type not assignable to trailing argument", this, arrayType);
- }
-
- /**
- * Makes a <em>variable arity</em> adapter which is able to accept
- * any number of trailing positional arguments and collect them
- * into an array argument.
- * <p>
- * The type and behavior of the adapter will be the same as
- * the type and behavior of the target, except that certain
- * {@code invoke} and {@code asType} requests can lead to
- * trailing positional arguments being collected into target's
- * trailing parameter.
- * Also, the last parameter type of the adapter will be
- * {@code arrayType}, even if the target has a different
- * last parameter type.
- * <p>
- * This transformation may return {@code this} if the method handle is
- * already of variable arity and its trailing parameter type
- * is identical to {@code arrayType}.
- * <p>
- * When called with {@link #invokeExact invokeExact}, the adapter invokes
- * the target with no argument changes.
- * (<em>Note:</em> This behavior is different from a
- * {@linkplain #asCollector fixed arity collector},
- * since it accepts a whole array of indeterminate length,
- * rather than a fixed number of arguments.)
- * <p>
- * When called with plain, inexact {@link #invoke invoke}, if the caller
- * type is the same as the adapter, the adapter invokes the target as with
- * {@code invokeExact}.
- * (This is the normal behavior for {@code invoke} when types match.)
- * <p>
- * Otherwise, if the caller and adapter arity are the same, and the
- * trailing parameter type of the caller is a reference type identical to
- * or assignable to the trailing parameter type of the adapter,
- * the arguments and return values are converted pairwise,
- * as if by {@link #asType asType} on a fixed arity
- * method handle.
- * <p>
- * Otherwise, the arities differ, or the adapter's trailing parameter
- * type is not assignable from the corresponding caller type.
- * In this case, the adapter replaces all trailing arguments from
- * the original trailing argument position onward, by
- * a new array of type {@code arrayType}, whose elements
- * comprise (in order) the replaced arguments.
- * <p>
- * The caller type must provides as least enough arguments,
- * and of the correct type, to satisfy the target's requirement for
- * positional arguments before the trailing array argument.
- * Thus, the caller must supply, at a minimum, {@code N-1} arguments,
- * where {@code N} is the arity of the target.
- * Also, there must exist conversions from the incoming arguments
- * to the target's arguments.
- * As with other uses of plain {@code invoke}, if these basic
- * requirements are not fulfilled, a {@code WrongMethodTypeException}
- * may be thrown.
- * <p>
- * In all cases, what the target eventually returns is returned unchanged by the adapter.
- * <p>
- * In the final case, it is exactly as if the target method handle were
- * temporarily adapted with a {@linkplain #asCollector fixed arity collector}
- * to the arity required by the caller type.
- * (As with {@code asCollector}, if the array length is zero,
- * a shared constant may be used instead of a new array.
- * If the implied call to {@code asCollector} would throw
- * an {@code IllegalArgumentException} or {@code WrongMethodTypeException},
- * the call to the variable arity adapter must throw
- * {@code WrongMethodTypeException}.)
- * <p>
- * The behavior of {@link #asType asType} is also specialized for
- * variable arity adapters, to maintain the invariant that
- * plain, inexact {@code invoke} is always equivalent to an {@code asType}
- * call to adjust the target type, followed by {@code invokeExact}.
- * Therefore, a variable arity adapter responds
- * to an {@code asType} request by building a fixed arity collector,
- * if and only if the adapter and requested type differ either
- * in arity or trailing argument type.
- * The resulting fixed arity collector has its type further adjusted
- * (if necessary) to the requested type by pairwise conversion,
- * as if by another application of {@code asType}.
- * <p>
- * When a method handle is obtained by executing an {@code ldc} instruction
- * of a {@code CONSTANT_MethodHandle} constant, and the target method is marked
- * as a variable arity method (with the modifier bit {@code 0x0080}),
- * the method handle will accept multiple arities, as if the method handle
- * constant were created by means of a call to {@code asVarargsCollector}.
- * <p>
- * In order to create a collecting adapter which collects a predetermined
- * number of arguments, and whose type reflects this predetermined number,
- * use {@link #asCollector asCollector} instead.
- * <p>
- * No method handle transformations produce new method handles with
- * variable arity, unless they are documented as doing so.
- * Therefore, besides {@code asVarargsCollector},
- * all methods in {@code MethodHandle} and {@code MethodHandles}
- * will return a method handle with fixed arity,
- * except in the cases where they are specified to return their original
- * operand (e.g., {@code asType} of the method handle's own type).
- * <p>
- * Calling {@code asVarargsCollector} on a method handle which is already
- * of variable arity will produce a method handle with the same type and behavior.
- * It may (or may not) return the original variable arity method handle.
- * <p>
- * Here is an example, of a list-making variable arity method handle:
- * <blockquote><pre>{@code
-MethodHandle deepToString = publicLookup()
- .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-MethodHandle ts1 = deepToString.asVarargsCollector(Object[].class);
-assertEquals("[won]", (String) ts1.invokeExact( new Object[]{"won"}));
-assertEquals("[won]", (String) ts1.invoke( new Object[]{"won"}));
-assertEquals("[won]", (String) ts1.invoke( "won" ));
-assertEquals("[[won]]", (String) ts1.invoke((Object) new Object[]{"won"}));
-// findStatic of Arrays.asList(...) produces a variable arity method handle:
-MethodHandle asList = publicLookup()
- .findStatic(Arrays.class, "asList", methodType(List.class, Object[].class));
-assertEquals(methodType(List.class, Object[].class), asList.type());
-assert(asList.isVarargsCollector());
-assertEquals("[]", asList.invoke().toString());
-assertEquals("[1]", asList.invoke(1).toString());
-assertEquals("[two, too]", asList.invoke("two", "too").toString());
-String[] argv = { "three", "thee", "tee" };
-assertEquals("[three, thee, tee]", asList.invoke(argv).toString());
-assertEquals("[three, thee, tee]", asList.invoke((Object[])argv).toString());
-List ls = (List) asList.invoke((Object)argv);
-assertEquals(1, ls.size());
-assertEquals("[three, thee, tee]", Arrays.toString((Object[])ls.get(0)));
- * }</pre></blockquote>
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * These rules are designed as a dynamically-typed variation
- * of the Java rules for variable arity methods.
- * In both cases, callers to a variable arity method or method handle
- * can either pass zero or more positional arguments, or else pass
- * pre-collected arrays of any length. Users should be aware of the
- * special role of the final argument, and of the effect of a
- * type match on that final argument, which determines whether
- * or not a single trailing argument is interpreted as a whole
- * array or a single element of an array to be collected.
- * Note that the dynamic type of the trailing argument has no
- * effect on this decision, only a comparison between the symbolic
- * type descriptor of the call site and the type descriptor of the method handle.)
- *
- * @param arrayType often {@code Object[]}, the type of the array argument which will collect the arguments
- * @return a new method handle which can collect any number of trailing arguments
- * into an array, before calling the original method handle
- * @throws NullPointerException if {@code arrayType} is a null reference
- * @throws IllegalArgumentException if {@code arrayType} is not an array type
- * or {@code arrayType} is not assignable to this method handle's trailing parameter type
- * @see #asCollector
- * @see #isVarargsCollector
- * @see #asFixedArity
- */
- public MethodHandle asVarargsCollector(Class<?> arrayType) {
- arrayType.getClass(); // explicit NPE
- boolean lastMatch = asCollectorChecks(arrayType, 0);
- if (isVarargsCollector() && lastMatch)
- return this;
- return new Transformers.VarargsCollector(this);
- }
+ public MethodType type() { return null; }
- /**
- * Determines if this method handle
- * supports {@linkplain #asVarargsCollector variable arity} calls.
- * Such method handles arise from the following sources:
- * <ul>
- * <li>a call to {@linkplain #asVarargsCollector asVarargsCollector}
- * <li>a call to a {@linkplain java.lang.invoke.MethodHandles.Lookup lookup method}
- * which resolves to a variable arity Java method or constructor
- * <li>an {@code ldc} instruction of a {@code CONSTANT_MethodHandle}
- * which resolves to a variable arity Java method or constructor
- * </ul>
- * @return true if this method handle accepts more than one arity of plain, inexact {@code invoke} calls
- * @see #asVarargsCollector
- * @see #asFixedArity
- */
- public boolean isVarargsCollector() {
- return false;
- }
+ public final Object invokeExact(Object... args) throws Throwable { return null; }
- /**
- * Makes a <em>fixed arity</em> method handle which is otherwise
- * equivalent to the current method handle.
- * <p>
- * If the current method handle is not of
- * {@linkplain #asVarargsCollector variable arity},
- * the current method handle is returned.
- * This is true even if the current method handle
- * could not be a valid input to {@code asVarargsCollector}.
- * <p>
- * Otherwise, the resulting fixed-arity method handle has the same
- * type and behavior of the current method handle,
- * except that {@link #isVarargsCollector isVarargsCollector}
- * will be false.
- * The fixed-arity method handle may (or may not) be the
- * a previous argument to {@code asVarargsCollector}.
- * <p>
- * Here is an example, of a list-making variable arity method handle:
- * <blockquote><pre>{@code
-MethodHandle asListVar = publicLookup()
- .findStatic(Arrays.class, "asList", methodType(List.class, Object[].class))
- .asVarargsCollector(Object[].class);
-MethodHandle asListFix = asListVar.asFixedArity();
-assertEquals("[1]", asListVar.invoke(1).toString());
-Exception caught = null;
-try { asListFix.invoke((Object)1); }
-catch (Exception ex) { caught = ex; }
-assert(caught instanceof ClassCastException);
-assertEquals("[two, too]", asListVar.invoke("two", "too").toString());
-try { asListFix.invoke("two", "too"); }
-catch (Exception ex) { caught = ex; }
-assert(caught instanceof WrongMethodTypeException);
-Object[] argv = { "three", "thee", "tee" };
-assertEquals("[three, thee, tee]", asListVar.invoke(argv).toString());
-assertEquals("[three, thee, tee]", asListFix.invoke(argv).toString());
-assertEquals(1, ((List) asListVar.invoke((Object)argv)).size());
-assertEquals("[three, thee, tee]", asListFix.invoke((Object)argv).toString());
- * }</pre></blockquote>
- *
- * @return a new method handle which accepts only a fixed number of arguments
- * @see #asVarargsCollector
- * @see #isVarargsCollector
- */
- public MethodHandle asFixedArity() {
- // Android-changed: implementation specific.
- MethodHandle mh = this;
- if (mh.isVarargsCollector()) {
- mh = ((Transformers.VarargsCollector) mh).asFixedArity();
- }
- assert(!mh.isVarargsCollector());
- return mh;
- }
+ public final Object invoke(Object... args) throws Throwable { return null; }
- /**
- * Binds a value {@code x} to the first argument of a method handle, without invoking it.
- * The new method handle adapts, as its <i>target</i>,
- * the current method handle by binding it to the given argument.
- * The type of the bound handle will be
- * the same as the type of the target, except that a single leading
- * reference parameter will be omitted.
- * <p>
- * When called, the bound handle inserts the given value {@code x}
- * as a new leading argument to the target. The other arguments are
- * also passed unchanged.
- * What the target eventually returns is returned unchanged by the bound handle.
- * <p>
- * The reference {@code x} must be convertible to the first parameter
- * type of the target.
- * <p>
- * (<em>Note:</em> Because method handles are immutable, the target method handle
- * retains its original type and behavior.)
- * @param x the value to bind to the first argument of the target
- * @return a new method handle which prepends the given value to the incoming
- * argument list, before calling the original method handle
- * @throws IllegalArgumentException if the target does not have a
- * leading parameter type that is a reference type
- * @throws ClassCastException if {@code x} cannot be converted
- * to the leading parameter type of the target
- * @see MethodHandles#insertArguments
- */
- public MethodHandle bindTo(Object x) {
- x = type.leadingReferenceParameter().cast(x); // throw CCE if needed
+ public Object invokeWithArguments(Object... arguments) throws Throwable { return null; }
- return new Transformers.BindTo(this, x);
- }
+ public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable { return null; }
- /**
- * Returns a string representation of the method handle,
- * starting with the string {@code "MethodHandle"} and
- * ending with the string representation of the method handle's type.
- * In other words, this method returns a string equal to the value of:
- * <blockquote><pre>{@code
- * "MethodHandle" + type().toString()
- * }</pre></blockquote>
- * <p>
- * (<em>Note:</em> Future releases of this API may add further information
- * to the string representation.
- * Therefore, the present syntax should not be parsed by applications.)
- *
- * @return a string representation of the method handle
- */
- @Override
- public String toString() {
- // Android-changed: Removed debugging support.
- return "MethodHandle"+type;
- }
+ public MethodHandle asType(MethodType newType) { return null; }
- /** @hide */
- public int getHandleKind() {
- return handleKind;
- }
+ public MethodHandle asCollector(Class<?> arrayType, int arrayLength) { return null; }
- /** @hide */
- protected void transform(EmulatedStackFrame arguments) throws Throwable {
- throw new AssertionError("MethodHandle.transform should never be called.");
- }
+ public MethodHandle asVarargsCollector(Class<?> arrayType) { return null; }
- /**
- * Creates a copy of this method handle, copying all relevant data.
- *
- * @hide
- */
- protected MethodHandle duplicate() {
- try {
- return (MethodHandle) this.clone();
- } catch (CloneNotSupportedException cnse) {
- throw new AssertionError("Subclass of Transformer is not cloneable");
- }
- }
+ public boolean isVarargsCollector() { return false; }
+ public MethodHandle asFixedArity() { return null; }
- /**
- * This is the entry point for all transform calls, and dispatches to the protected
- * transform method. This layer of indirection exists purely for convenience, because
- * we can invoke-direct on a fixed ArtMethod for all transform variants.
- *
- * NOTE: If this extra layer of indirection proves to be a problem, we can get rid
- * of this layer of indirection at the cost of some additional ugliness.
- */
- private void transformInternal(EmulatedStackFrame arguments) throws Throwable {
- transform(arguments);
- }
+ public MethodHandle bindTo(Object x) { return null; }
- // Android-changed: Removed implementation details :
- //
- // String standardString();
- // String debugString();
- //
- //// Implementation methods.
- //// Sub-classes can override these default implementations.
- //// All these methods assume arguments are already validated.
- //
- // Other transforms to do: convert, explicitCast, permute, drop, filter, fold, GWT, catch
- //
- // BoundMethodHandle bindArgumentL(int pos, Object value);
- // /*non-public*/ MethodHandle setVarargs(MemberName member);
- // /*non-public*/ MethodHandle viewAsType(MethodType newType, boolean strict);
- // /*non-public*/ boolean viewAsTypeChecks(MethodType newType, boolean strict);
- //
- // Decoding
- //
- // /*non-public*/ LambdaForm internalForm();
- // /*non-public*/ MemberName internalMemberName();
- // /*non-public*/ Class<?> internalCallerClass();
- // /*non-public*/ MethodHandleImpl.Intrinsic intrinsicName();
- // /*non-public*/ MethodHandle withInternalMemberName(MemberName member, boolean isInvokeSpecial);
- // /*non-public*/ boolean isInvokeSpecial();
- // /*non-public*/ Object internalValues();
- // /*non-public*/ Object internalProperties();
- //
- //// Method handle implementation methods.
- //// Sub-classes can override these default implementations.
- //// All these methods assume arguments are already validated.
- //
- // /*non-public*/ abstract MethodHandle copyWith(MethodType mt, LambdaForm lf);
- // abstract BoundMethodHandle rebind();
- // /*non-public*/ void updateForm(LambdaForm newForm);
- // /*non-public*/ void customize();
- // private static final long FORM_OFFSET;
}
diff --git a/java/lang/invoke/MethodHandles.java b/java/lang/invoke/MethodHandles.java
index bc1e9442..f27ad988 100644
--- a/java/lang/invoke/MethodHandles.java
+++ b/java/lang/invoke/MethodHandles.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2008, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,3479 +25,129 @@
package java.lang.invoke;
-import java.lang.reflect.*;
-import java.nio.ByteOrder;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
import java.util.List;
-import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.NoSuchElementException;
-import dalvik.system.VMStack;
-import sun.invoke.util.VerifyAccess;
-import sun.invoke.util.Wrapper;
-import static java.lang.invoke.MethodHandleStatics.*;
-
-/**
- * This class consists exclusively of static methods that operate on or return
- * method handles. They fall into several categories:
- * <ul>
- * <li>Lookup methods which help create method handles for methods and fields.
- * <li>Combinator methods, which combine or transform pre-existing method handles into new ones.
- * <li>Other factory methods to create method handles that emulate other common JVM operations or control flow patterns.
- * </ul>
- * <p>
- * @author John Rose, JSR 292 EG
- * @since 1.7
- */
public class MethodHandles {
- private MethodHandles() { } // do not instantiate
-
- // Android-changed: We do not use MemberName / MethodHandleImpl.
- //
- // private static final MemberName.Factory IMPL_NAMES = MemberName.getFactory();
- // static { MethodHandleImpl.initStatics(); }
- // See IMPL_LOOKUP below.
-
- //// Method handle creation from ordinary methods.
-
- /**
- * Returns a {@link Lookup lookup object} with
- * full capabilities to emulate all supported bytecode behaviors of the caller.
- * These capabilities include <a href="MethodHandles.Lookup.html#privacc">private access</a> to the caller.
- * Factory methods on the lookup object can create
- * <a href="MethodHandleInfo.html#directmh">direct method handles</a>
- * for any member that the caller has access to via bytecodes,
- * including protected and private fields and methods.
- * This lookup object is a <em>capability</em> which may be delegated to trusted agents.
- * Do not store it in place where untrusted code can access it.
- * <p>
- * This method is caller sensitive, which means that it may return different
- * values to different callers.
- * <p>
- * For any given caller class {@code C}, the lookup object returned by this call
- * has equivalent capabilities to any lookup object
- * supplied by the JVM to the bootstrap method of an
- * <a href="package-summary.html#indyinsn">invokedynamic instruction</a>
- * executing in the same caller class {@code C}.
- * @return a lookup object for the caller of this method, with private access
- */
- // Android-changed: Remove caller sensitive.
- // @CallerSensitive
- public static Lookup lookup() {
- // Android-changed: Do not use Reflection.getCallerClass().
- return new Lookup(VMStack.getStackClass1());
- }
+ public static Lookup lookup() { return null; }
- /**
- * Returns a {@link Lookup lookup object} which is trusted minimally.
- * It can only be used to create method handles to
- * publicly accessible fields and methods.
- * <p>
- * As a matter of pure convention, the {@linkplain Lookup#lookupClass lookup class}
- * of this lookup object will be {@link java.lang.Object}.
- *
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * The lookup class can be changed to any other class {@code C} using an expression of the form
- * {@link Lookup#in publicLookup().in(C.class)}.
- * Since all classes have equal access to public names,
- * such a change would confer no new access rights.
- * A public lookup object is always subject to
- * <a href="MethodHandles.Lookup.html#secmgr">security manager checks</a>.
- * Also, it cannot access
- * <a href="MethodHandles.Lookup.html#callsens">caller sensitive methods</a>.
- * @return a lookup object which is trusted minimally
- */
- public static Lookup publicLookup() {
- return Lookup.PUBLIC_LOOKUP;
- }
+ public static Lookup publicLookup() { return null; }
- /**
- * Performs an unchecked "crack" of a
- * <a href="MethodHandleInfo.html#directmh">direct method handle</a>.
- * The result is as if the user had obtained a lookup object capable enough
- * to crack the target method handle, called
- * {@link java.lang.invoke.MethodHandles.Lookup#revealDirect Lookup.revealDirect}
- * on the target to obtain its symbolic reference, and then called
- * {@link java.lang.invoke.MethodHandleInfo#reflectAs MethodHandleInfo.reflectAs}
- * to resolve the symbolic reference to a member.
- * <p>
- * If there is a security manager, its {@code checkPermission} method
- * is called with a {@code ReflectPermission("suppressAccessChecks")} permission.
- * @param <T> the desired type of the result, either {@link Member} or a subtype
- * @param target a direct method handle to crack into symbolic reference components
- * @param expected a class object representing the desired result type {@code T}
- * @return a reference to the method, constructor, or field object
- * @exception SecurityException if the caller is not privileged to call {@code setAccessible}
- * @exception NullPointerException if either argument is {@code null}
- * @exception IllegalArgumentException if the target is not a direct method handle
- * @exception ClassCastException if the member is not of the expected type
- * @since 1.8
- */
public static <T extends Member> T
- reflectAs(Class<T> expected, MethodHandle target) {
- MethodHandleImpl directTarget = getMethodHandleImpl(target);
- // Given that this is specified to be an "unchecked" crack, we can directly allocate
- // a member from the underlying ArtField / Method and bypass all associated access checks.
- return expected.cast(directTarget.getMemberInternal());
- }
+ reflectAs(Class<T> expected, MethodHandle target) { return null; }
- /**
- * A <em>lookup object</em> is a factory for creating method handles,
- * when the creation requires access checking.
- * Method handles do not perform
- * access checks when they are called, but rather when they are created.
- * Therefore, method handle access
- * restrictions must be enforced when a method handle is created.
- * The caller class against which those restrictions are enforced
- * is known as the {@linkplain #lookupClass lookup class}.
- * <p>
- * A lookup class which needs to create method handles will call
- * {@link #lookup MethodHandles.lookup} to create a factory for itself.
- * When the {@code Lookup} factory object is created, the identity of the lookup class is
- * determined, and securely stored in the {@code Lookup} object.
- * The lookup class (or its delegates) may then use factory methods
- * on the {@code Lookup} object to create method handles for access-checked members.
- * This includes all methods, constructors, and fields which are allowed to the lookup class,
- * even private ones.
- *
- * <h1><a name="lookups"></a>Lookup Factory Methods</h1>
- * The factory methods on a {@code Lookup} object correspond to all major
- * use cases for methods, constructors, and fields.
- * Each method handle created by a factory method is the functional
- * equivalent of a particular <em>bytecode behavior</em>.
- * (Bytecode behaviors are described in section 5.4.3.5 of the Java Virtual Machine Specification.)
- * Here is a summary of the correspondence between these factory methods and
- * the behavior the resulting method handles:
- * <table border=1 cellpadding=5 summary="lookup method behaviors">
- * <tr>
- * <th><a name="equiv"></a>lookup expression</th>
- * <th>member</th>
- * <th>bytecode behavior</th>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findGetter lookup.findGetter(C.class,"f",FT.class)}</td>
- * <td>{@code FT f;}</td><td>{@code (T) this.f;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findStaticGetter lookup.findStaticGetter(C.class,"f",FT.class)}</td>
- * <td>{@code static}<br>{@code FT f;}</td><td>{@code (T) C.f;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findSetter lookup.findSetter(C.class,"f",FT.class)}</td>
- * <td>{@code FT f;}</td><td>{@code this.f = x;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findStaticSetter lookup.findStaticSetter(C.class,"f",FT.class)}</td>
- * <td>{@code static}<br>{@code FT f;}</td><td>{@code C.f = arg;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findVirtual lookup.findVirtual(C.class,"m",MT)}</td>
- * <td>{@code T m(A*);}</td><td>{@code (T) this.m(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findStatic lookup.findStatic(C.class,"m",MT)}</td>
- * <td>{@code static}<br>{@code T m(A*);}</td><td>{@code (T) C.m(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findSpecial lookup.findSpecial(C.class,"m",MT,this.class)}</td>
- * <td>{@code T m(A*);}</td><td>{@code (T) super.m(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findConstructor lookup.findConstructor(C.class,MT)}</td>
- * <td>{@code C(A*);}</td><td>{@code new C(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectGetter lookup.unreflectGetter(aField)}</td>
- * <td>({@code static})?<br>{@code FT f;}</td><td>{@code (FT) aField.get(thisOrNull);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectSetter lookup.unreflectSetter(aField)}</td>
- * <td>({@code static})?<br>{@code FT f;}</td><td>{@code aField.set(thisOrNull, arg);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflect lookup.unreflect(aMethod)}</td>
- * <td>({@code static})?<br>{@code T m(A*);}</td><td>{@code (T) aMethod.invoke(thisOrNull, arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectConstructor lookup.unreflectConstructor(aConstructor)}</td>
- * <td>{@code C(A*);}</td><td>{@code (C) aConstructor.newInstance(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflect lookup.unreflect(aMethod)}</td>
- * <td>({@code static})?<br>{@code T m(A*);}</td><td>{@code (T) aMethod.invoke(thisOrNull, arg*);}</td>
- * </tr>
- * </table>
- *
- * Here, the type {@code C} is the class or interface being searched for a member,
- * documented as a parameter named {@code refc} in the lookup methods.
- * The method type {@code MT} is composed from the return type {@code T}
- * and the sequence of argument types {@code A*}.
- * The constructor also has a sequence of argument types {@code A*} and
- * is deemed to return the newly-created object of type {@code C}.
- * Both {@code MT} and the field type {@code FT} are documented as a parameter named {@code type}.
- * The formal parameter {@code this} stands for the self-reference of type {@code C};
- * if it is present, it is always the leading argument to the method handle invocation.
- * (In the case of some {@code protected} members, {@code this} may be
- * restricted in type to the lookup class; see below.)
- * The name {@code arg} stands for all the other method handle arguments.
- * In the code examples for the Core Reflection API, the name {@code thisOrNull}
- * stands for a null reference if the accessed method or field is static,
- * and {@code this} otherwise.
- * The names {@code aMethod}, {@code aField}, and {@code aConstructor} stand
- * for reflective objects corresponding to the given members.
- * <p>
- * In cases where the given member is of variable arity (i.e., a method or constructor)
- * the returned method handle will also be of {@linkplain MethodHandle#asVarargsCollector variable arity}.
- * In all other cases, the returned method handle will be of fixed arity.
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * The equivalence between looked-up method handles and underlying
- * class members and bytecode behaviors
- * can break down in a few ways:
- * <ul style="font-size:smaller;">
- * <li>If {@code C} is not symbolically accessible from the lookup class's loader,
- * the lookup can still succeed, even when there is no equivalent
- * Java expression or bytecoded constant.
- * <li>Likewise, if {@code T} or {@code MT}
- * is not symbolically accessible from the lookup class's loader,
- * the lookup can still succeed.
- * For example, lookups for {@code MethodHandle.invokeExact} and
- * {@code MethodHandle.invoke} will always succeed, regardless of requested type.
- * <li>If there is a security manager installed, it can forbid the lookup
- * on various grounds (<a href="MethodHandles.Lookup.html#secmgr">see below</a>).
- * By contrast, the {@code ldc} instruction on a {@code CONSTANT_MethodHandle}
- * constant is not subject to security manager checks.
- * <li>If the looked-up method has a
- * <a href="MethodHandle.html#maxarity">very large arity</a>,
- * the method handle creation may fail, due to the method handle
- * type having too many parameters.
- * </ul>
- *
- * <h1><a name="access"></a>Access checking</h1>
- * Access checks are applied in the factory methods of {@code Lookup},
- * when a method handle is created.
- * This is a key difference from the Core Reflection API, since
- * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
- * performs access checking against every caller, on every call.
- * <p>
- * All access checks start from a {@code Lookup} object, which
- * compares its recorded lookup class against all requests to
- * create method handles.
- * A single {@code Lookup} object can be used to create any number
- * of access-checked method handles, all checked against a single
- * lookup class.
- * <p>
- * A {@code Lookup} object can be shared with other trusted code,
- * such as a metaobject protocol.
- * A shared {@code Lookup} object delegates the capability
- * to create method handles on private members of the lookup class.
- * Even if privileged code uses the {@code Lookup} object,
- * the access checking is confined to the privileges of the
- * original lookup class.
- * <p>
- * A lookup can fail, because
- * the containing class is not accessible to the lookup class, or
- * because the desired class member is missing, or because the
- * desired class member is not accessible to the lookup class, or
- * because the lookup object is not trusted enough to access the member.
- * In any of these cases, a {@code ReflectiveOperationException} will be
- * thrown from the attempted lookup. The exact class will be one of
- * the following:
- * <ul>
- * <li>NoSuchMethodException &mdash; if a method is requested but does not exist
- * <li>NoSuchFieldException &mdash; if a field is requested but does not exist
- * <li>IllegalAccessException &mdash; if the member exists but an access check fails
- * </ul>
- * <p>
- * In general, the conditions under which a method handle may be
- * looked up for a method {@code M} are no more restrictive than the conditions
- * under which the lookup class could have compiled, verified, and resolved a call to {@code M}.
- * Where the JVM would raise exceptions like {@code NoSuchMethodError},
- * a method handle lookup will generally raise a corresponding
- * checked exception, such as {@code NoSuchMethodException}.
- * And the effect of invoking the method handle resulting from the lookup
- * is <a href="MethodHandles.Lookup.html#equiv">exactly equivalent</a>
- * to executing the compiled, verified, and resolved call to {@code M}.
- * The same point is true of fields and constructors.
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * Access checks only apply to named and reflected methods,
- * constructors, and fields.
- * Other method handle creation methods, such as
- * {@link MethodHandle#asType MethodHandle.asType},
- * do not require any access checks, and are used
- * independently of any {@code Lookup} object.
- * <p>
- * If the desired member is {@code protected}, the usual JVM rules apply,
- * including the requirement that the lookup class must be either be in the
- * same package as the desired member, or must inherit that member.
- * (See the Java Virtual Machine Specification, sections 4.9.2, 5.4.3.5, and 6.4.)
- * In addition, if the desired member is a non-static field or method
- * in a different package, the resulting method handle may only be applied
- * to objects of the lookup class or one of its subclasses.
- * This requirement is enforced by narrowing the type of the leading
- * {@code this} parameter from {@code C}
- * (which will necessarily be a superclass of the lookup class)
- * to the lookup class itself.
- * <p>
- * The JVM imposes a similar requirement on {@code invokespecial} instruction,
- * that the receiver argument must match both the resolved method <em>and</em>
- * the current class. Again, this requirement is enforced by narrowing the
- * type of the leading parameter to the resulting method handle.
- * (See the Java Virtual Machine Specification, section 4.10.1.9.)
- * <p>
- * The JVM represents constructors and static initializer blocks as internal methods
- * with special names ({@code "<init>"} and {@code "<clinit>"}).
- * The internal syntax of invocation instructions allows them to refer to such internal
- * methods as if they were normal methods, but the JVM bytecode verifier rejects them.
- * A lookup of such an internal method will produce a {@code NoSuchMethodException}.
- * <p>
- * In some cases, access between nested classes is obtained by the Java compiler by creating
- * an wrapper method to access a private method of another class
- * in the same top-level declaration.
- * For example, a nested class {@code C.D}
- * can access private members within other related classes such as
- * {@code C}, {@code C.D.E}, or {@code C.B},
- * but the Java compiler may need to generate wrapper methods in
- * those related classes. In such cases, a {@code Lookup} object on
- * {@code C.E} would be unable to those private members.
- * A workaround for this limitation is the {@link Lookup#in Lookup.in} method,
- * which can transform a lookup on {@code C.E} into one on any of those other
- * classes, without special elevation of privilege.
- * <p>
- * The accesses permitted to a given lookup object may be limited,
- * according to its set of {@link #lookupModes lookupModes},
- * to a subset of members normally accessible to the lookup class.
- * For example, the {@link #publicLookup publicLookup}
- * method produces a lookup object which is only allowed to access
- * public members in public classes.
- * The caller sensitive method {@link #lookup lookup}
- * produces a lookup object with full capabilities relative to
- * its caller class, to emulate all supported bytecode behaviors.
- * Also, the {@link Lookup#in Lookup.in} method may produce a lookup object
- * with fewer access modes than the original lookup object.
- *
- * <p style="font-size:smaller;">
- * <a name="privacc"></a>
- * <em>Discussion of private access:</em>
- * We say that a lookup has <em>private access</em>
- * if its {@linkplain #lookupModes lookup modes}
- * include the possibility of accessing {@code private} members.
- * As documented in the relevant methods elsewhere,
- * only lookups with private access possess the following capabilities:
- * <ul style="font-size:smaller;">
- * <li>access private fields, methods, and constructors of the lookup class
- * <li>create method handles which invoke <a href="MethodHandles.Lookup.html#callsens">caller sensitive</a> methods,
- * such as {@code Class.forName}
- * <li>create method handles which {@link Lookup#findSpecial emulate invokespecial} instructions
- * <li>avoid <a href="MethodHandles.Lookup.html#secmgr">package access checks</a>
- * for classes accessible to the lookup class
- * <li>create {@link Lookup#in delegated lookup objects} which have private access to other classes
- * within the same package member
- * </ul>
- * <p style="font-size:smaller;">
- * Each of these permissions is a consequence of the fact that a lookup object
- * with private access can be securely traced back to an originating class,
- * whose <a href="MethodHandles.Lookup.html#equiv">bytecode behaviors</a> and Java language access permissions
- * can be reliably determined and emulated by method handles.
- *
- * <h1><a name="secmgr"></a>Security manager interactions</h1>
- * Although bytecode instructions can only refer to classes in
- * a related class loader, this API can search for methods in any
- * class, as long as a reference to its {@code Class} object is
- * available. Such cross-loader references are also possible with the
- * Core Reflection API, and are impossible to bytecode instructions
- * such as {@code invokestatic} or {@code getfield}.
- * There is a {@linkplain java.lang.SecurityManager security manager API}
- * to allow applications to check such cross-loader references.
- * These checks apply to both the {@code MethodHandles.Lookup} API
- * and the Core Reflection API
- * (as found on {@link java.lang.Class Class}).
- * <p>
- * If a security manager is present, member lookups are subject to
- * additional checks.
- * From one to three calls are made to the security manager.
- * Any of these calls can refuse access by throwing a
- * {@link java.lang.SecurityException SecurityException}.
- * Define {@code smgr} as the security manager,
- * {@code lookc} as the lookup class of the current lookup object,
- * {@code refc} as the containing class in which the member
- * is being sought, and {@code defc} as the class in which the
- * member is actually defined.
- * The value {@code lookc} is defined as <em>not present</em>
- * if the current lookup object does not have
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>.
- * The calls are made according to the following rules:
- * <ul>
- * <li><b>Step 1:</b>
- * If {@code lookc} is not present, or if its class loader is not
- * the same as or an ancestor of the class loader of {@code refc},
- * then {@link SecurityManager#checkPackageAccess
- * smgr.checkPackageAccess(refcPkg)} is called,
- * where {@code refcPkg} is the package of {@code refc}.
- * <li><b>Step 2:</b>
- * If the retrieved member is not public and
- * {@code lookc} is not present, then
- * {@link SecurityManager#checkPermission smgr.checkPermission}
- * with {@code RuntimePermission("accessDeclaredMembers")} is called.
- * <li><b>Step 3:</b>
- * If the retrieved member is not public,
- * and if {@code lookc} is not present,
- * and if {@code defc} and {@code refc} are different,
- * then {@link SecurityManager#checkPackageAccess
- * smgr.checkPackageAccess(defcPkg)} is called,
- * where {@code defcPkg} is the package of {@code defc}.
- * </ul>
- * Security checks are performed after other access checks have passed.
- * Therefore, the above rules presuppose a member that is public,
- * or else that is being accessed from a lookup class that has
- * rights to access the member.
- *
- * <h1><a name="callsens"></a>Caller sensitive methods</h1>
- * A small number of Java methods have a special property called caller sensitivity.
- * A <em>caller-sensitive</em> method can behave differently depending on the
- * identity of its immediate caller.
- * <p>
- * If a method handle for a caller-sensitive method is requested,
- * the general rules for <a href="MethodHandles.Lookup.html#equiv">bytecode behaviors</a> apply,
- * but they take account of the lookup class in a special way.
- * The resulting method handle behaves as if it were called
- * from an instruction contained in the lookup class,
- * so that the caller-sensitive method detects the lookup class.
- * (By contrast, the invoker of the method handle is disregarded.)
- * Thus, in the case of caller-sensitive methods,
- * different lookup classes may give rise to
- * differently behaving method handles.
- * <p>
- * In cases where the lookup object is
- * {@link #publicLookup publicLookup()},
- * or some other lookup object without
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>,
- * the lookup class is disregarded.
- * In such cases, no caller-sensitive method handle can be created,
- * access is forbidden, and the lookup fails with an
- * {@code IllegalAccessException}.
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * For example, the caller-sensitive method
- * {@link java.lang.Class#forName(String) Class.forName(x)}
- * can return varying classes or throw varying exceptions,
- * depending on the class loader of the class that calls it.
- * A public lookup of {@code Class.forName} will fail, because
- * there is no reasonable way to determine its bytecode behavior.
- * <p style="font-size:smaller;">
- * If an application caches method handles for broad sharing,
- * it should use {@code publicLookup()} to create them.
- * If there is a lookup of {@code Class.forName}, it will fail,
- * and the application must take appropriate action in that case.
- * It may be that a later lookup, perhaps during the invocation of a
- * bootstrap method, can incorporate the specific identity
- * of the caller, making the method accessible.
- * <p style="font-size:smaller;">
- * The function {@code MethodHandles.lookup} is caller sensitive
- * so that there can be a secure foundation for lookups.
- * Nearly all other methods in the JSR 292 API rely on lookup
- * objects to check access requests.
- */
- // Android-changed: Change link targets from MethodHandles#[public]Lookup to
- // #[public]Lookup to work around complaints from javadoc.
public static final
class Lookup {
- /** The class on behalf of whom the lookup is being performed. */
- /* @NonNull */ private final Class<?> lookupClass;
-
- /** The allowed sorts of members which may be looked up (PUBLIC, etc.). */
- private final int allowedModes;
-
- /** A single-bit mask representing {@code public} access,
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value, {@code 0x01}, happens to be the same as the value of the
- * {@code public} {@linkplain java.lang.reflect.Modifier#PUBLIC modifier bit}.
- */
- public static final int PUBLIC = Modifier.PUBLIC;
-
- /** A single-bit mask representing {@code private} access,
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value, {@code 0x02}, happens to be the same as the value of the
- * {@code private} {@linkplain java.lang.reflect.Modifier#PRIVATE modifier bit}.
- */
- public static final int PRIVATE = Modifier.PRIVATE;
-
- /** A single-bit mask representing {@code protected} access,
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value, {@code 0x04}, happens to be the same as the value of the
- * {@code protected} {@linkplain java.lang.reflect.Modifier#PROTECTED modifier bit}.
- */
- public static final int PROTECTED = Modifier.PROTECTED;
+ public static final int PUBLIC = 0;
- /** A single-bit mask representing {@code package} access (default access),
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value is {@code 0x08}, which does not correspond meaningfully to
- * any particular {@linkplain java.lang.reflect.Modifier modifier bit}.
- */
- public static final int PACKAGE = Modifier.STATIC;
+ public static final int PRIVATE = 0;
- private static final int ALL_MODES = (PUBLIC | PRIVATE | PROTECTED | PACKAGE);
+ public static final int PROTECTED = 0;
- // Android-note: Android has no notion of a trusted lookup. If required, such lookups
- // are performed by the runtime. As a result, we always use lookupClass, which will always
- // be non-null in our implementation.
- //
- // private static final int TRUSTED = -1;
+ public static final int PACKAGE = 0;
- private static int fixmods(int mods) {
- mods &= (ALL_MODES - PACKAGE);
- return (mods != 0) ? mods : PACKAGE;
- }
+ public Class<?> lookupClass() { return null; }
- /** Tells which class is performing the lookup. It is this class against
- * which checks are performed for visibility and access permissions.
- * <p>
- * The class implies a maximum level of access permission,
- * but the permissions may be additionally limited by the bitmask
- * {@link #lookupModes lookupModes}, which controls whether non-public members
- * can be accessed.
- * @return the lookup class, on behalf of which this lookup object finds members
- */
- public Class<?> lookupClass() {
- return lookupClass;
- }
+ public int lookupModes() { return 0; }
- /** Tells which access-protection classes of members this lookup object can produce.
- * The result is a bit-mask of the bits
- * {@linkplain #PUBLIC PUBLIC (0x01)},
- * {@linkplain #PRIVATE PRIVATE (0x02)},
- * {@linkplain #PROTECTED PROTECTED (0x04)},
- * and {@linkplain #PACKAGE PACKAGE (0x08)}.
- * <p>
- * A freshly-created lookup object
- * on the {@linkplain java.lang.invoke.MethodHandles#lookup() caller's class}
- * has all possible bits set, since the caller class can access all its own members.
- * A lookup object on a new lookup class
- * {@linkplain java.lang.invoke.MethodHandles.Lookup#in created from a previous lookup object}
- * may have some mode bits set to zero.
- * The purpose of this is to restrict access via the new lookup object,
- * so that it can access only names which can be reached by the original
- * lookup object, and also by the new lookup class.
- * @return the lookup modes, which limit the kinds of access performed by this lookup object
- */
- public int lookupModes() {
- return allowedModes & ALL_MODES;
- }
+ public Lookup in(Class<?> requestedLookupClass) { return null; }
- /** Embody the current class (the lookupClass) as a lookup class
- * for method handle creation.
- * Must be called by from a method in this package,
- * which in turn is called by a method not in this package.
- */
- Lookup(Class<?> lookupClass) {
- this(lookupClass, ALL_MODES);
- // make sure we haven't accidentally picked up a privileged class:
- checkUnprivilegedlookupClass(lookupClass, ALL_MODES);
- }
-
- private Lookup(Class<?> lookupClass, int allowedModes) {
- this.lookupClass = lookupClass;
- this.allowedModes = allowedModes;
- }
-
- /**
- * Creates a lookup on the specified new lookup class.
- * The resulting object will report the specified
- * class as its own {@link #lookupClass lookupClass}.
- * <p>
- * However, the resulting {@code Lookup} object is guaranteed
- * to have no more access capabilities than the original.
- * In particular, access capabilities can be lost as follows:<ul>
- * <li>If the new lookup class differs from the old one,
- * protected members will not be accessible by virtue of inheritance.
- * (Protected members may continue to be accessible because of package sharing.)
- * <li>If the new lookup class is in a different package
- * than the old one, protected and default (package) members will not be accessible.
- * <li>If the new lookup class is not within the same package member
- * as the old one, private members will not be accessible.
- * <li>If the new lookup class is not accessible to the old lookup class,
- * then no members, not even public members, will be accessible.
- * (In all other cases, public members will continue to be accessible.)
- * </ul>
- *
- * @param requestedLookupClass the desired lookup class for the new lookup object
- * @return a lookup object which reports the desired lookup class
- * @throws NullPointerException if the argument is null
- */
- public Lookup in(Class<?> requestedLookupClass) {
- requestedLookupClass.getClass(); // null check
- // Android-changed: There's no notion of a trusted lookup.
- // if (allowedModes == TRUSTED) // IMPL_LOOKUP can make any lookup at all
- // return new Lookup(requestedLookupClass, ALL_MODES);
-
- if (requestedLookupClass == this.lookupClass)
- return this; // keep same capabilities
- int newModes = (allowedModes & (ALL_MODES & ~PROTECTED));
- if ((newModes & PACKAGE) != 0
- && !VerifyAccess.isSamePackage(this.lookupClass, requestedLookupClass)) {
- newModes &= ~(PACKAGE|PRIVATE);
- }
- // Allow nestmate lookups to be created without special privilege:
- if ((newModes & PRIVATE) != 0
- && !VerifyAccess.isSamePackageMember(this.lookupClass, requestedLookupClass)) {
- newModes &= ~PRIVATE;
- }
- if ((newModes & PUBLIC) != 0
- && !VerifyAccess.isClassAccessible(requestedLookupClass, this.lookupClass, allowedModes)) {
- // The requested class it not accessible from the lookup class.
- // No permissions.
- newModes = 0;
- }
- checkUnprivilegedlookupClass(requestedLookupClass, newModes);
- return new Lookup(requestedLookupClass, newModes);
- }
-
- // Make sure outer class is initialized first.
- //
- // Android-changed: Removed unnecessary reference to IMPL_NAMES.
- // static { IMPL_NAMES.getClass(); }
-
- /** Version of lookup which is trusted minimally.
- * It can only be used to create method handles to
- * publicly accessible members.
- */
- static final Lookup PUBLIC_LOOKUP = new Lookup(Object.class, PUBLIC);
-
- /** Package-private version of lookup which is trusted. */
- static final Lookup IMPL_LOOKUP = new Lookup(Object.class, ALL_MODES);
-
- private static void checkUnprivilegedlookupClass(Class<?> lookupClass, int allowedModes) {
- String name = lookupClass.getName();
- if (name.startsWith("java.lang.invoke."))
- throw newIllegalArgumentException("illegal lookupClass: "+lookupClass);
-
- // For caller-sensitive MethodHandles.lookup()
- // disallow lookup more restricted packages
- //
- // Android-changed: The bootstrap classloader isn't null.
- if (allowedModes == ALL_MODES &&
- lookupClass.getClassLoader() == Object.class.getClassLoader()) {
- if (name.startsWith("java.") ||
- (name.startsWith("sun.")
- && !name.startsWith("sun.invoke.")
- && !name.equals("sun.reflect.ReflectionFactory"))) {
- throw newIllegalArgumentException("illegal lookupClass: " + lookupClass);
- }
- }
- }
-
- /**
- * Displays the name of the class from which lookups are to be made.
- * (The name is the one reported by {@link java.lang.Class#getName() Class.getName}.)
- * If there are restrictions on the access permitted to this lookup,
- * this is indicated by adding a suffix to the class name, consisting
- * of a slash and a keyword. The keyword represents the strongest
- * allowed access, and is chosen as follows:
- * <ul>
- * <li>If no access is allowed, the suffix is "/noaccess".
- * <li>If only public access is allowed, the suffix is "/public".
- * <li>If only public and package access are allowed, the suffix is "/package".
- * <li>If only public, package, and private access are allowed, the suffix is "/private".
- * </ul>
- * If none of the above cases apply, it is the case that full
- * access (public, package, private, and protected) is allowed.
- * In this case, no suffix is added.
- * This is true only of an object obtained originally from
- * {@link java.lang.invoke.MethodHandles#lookup MethodHandles.lookup}.
- * Objects created by {@link java.lang.invoke.MethodHandles.Lookup#in Lookup.in}
- * always have restricted access, and will display a suffix.
- * <p>
- * (It may seem strange that protected access should be
- * stronger than private access. Viewed independently from
- * package access, protected access is the first to be lost,
- * because it requires a direct subclass relationship between
- * caller and callee.)
- * @see #in
- */
- @Override
- public String toString() {
- String cname = lookupClass.getName();
- switch (allowedModes) {
- case 0: // no privileges
- return cname + "/noaccess";
- case PUBLIC:
- return cname + "/public";
- case PUBLIC|PACKAGE:
- return cname + "/package";
- case ALL_MODES & ~PROTECTED:
- return cname + "/private";
- case ALL_MODES:
- return cname;
- // Android-changed: No support for TRUSTED callers.
- // case TRUSTED:
- // return "/trusted"; // internal only; not exported
- default: // Should not happen, but it's a bitfield...
- cname = cname + "/" + Integer.toHexString(allowedModes);
- assert(false) : cname;
- return cname;
- }
- }
-
- /**
- * Produces a method handle for a static method.
- * The type of the method handle will be that of the method.
- * (Since static methods do not take receivers, there is no
- * additional receiver argument inserted into the method handle type,
- * as there would be with {@link #findVirtual findVirtual} or {@link #findSpecial findSpecial}.)
- * The method and all its argument types must be accessible to the lookup object.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If the returned method handle is invoked, the method's class will
- * be initialized, if it has not already been initialized.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_asList = publicLookup().findStatic(Arrays.class,
- "asList", methodType(List.class, Object[].class));
-assertEquals("[x, y]", MH_asList.invoke("x", "y").toString());
- * }</pre></blockquote>
- * @param refc the class from which the method is accessed
- * @param name the name of the method
- * @param type the type of the method
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails,
- * or if the method is not {@code static},
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
public
- MethodHandle findStatic(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- Method method = refc.getDeclaredMethod(name, type.ptypes());
- final int modifiers = method.getModifiers();
- if (!Modifier.isStatic(modifiers)) {
- throw new IllegalAccessException("Method" + method + " is not static");
- }
- checkReturnType(method, type);
- checkAccess(refc, method.getDeclaringClass(), modifiers, method.getName());
- return createMethodHandle(method, MethodHandle.INVOKE_STATIC, type);
- }
-
- private MethodHandle findVirtualForMH(String name, MethodType type) {
- // these names require special lookups because of the implicit MethodType argument
- if ("invoke".equals(name))
- return invoker(type);
- if ("invokeExact".equals(name))
- return exactInvoker(type);
- return null;
- }
+ MethodHandle findStatic(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- private MethodHandle findVirtualForVH(String name, MethodType type) {
- VarHandle.AccessMode accessMode;
- try {
- accessMode = VarHandle.AccessMode.valueFromMethodName(name);
- } catch (IllegalArgumentException e) {
- return null;
- }
- return varHandleInvoker(accessMode, type);
- }
+ public MethodHandle findVirtual(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- private static MethodHandle createMethodHandle(Method method, int handleKind,
- MethodType methodType) {
- MethodHandle mh = new MethodHandleImpl(method.getArtMethod(), handleKind, methodType);
- if (method.isVarArgs()) {
- return new Transformers.VarargsCollector(mh);
- } else {
- return mh;
- }
- }
+ public MethodHandle findConstructor(Class<?> refc, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- /**
- * Produces a method handle for a virtual method.
- * The type of the method handle will be that of the method,
- * with the receiver type (usually {@code refc}) prepended.
- * The method and all its argument types must be accessible to the lookup object.
- * <p>
- * When called, the handle will treat the first argument as a receiver
- * and dispatch on the receiver's type to determine which method
- * implementation to enter.
- * (The dispatching action is identical with that performed by an
- * {@code invokevirtual} or {@code invokeinterface} instruction.)
- * <p>
- * The first argument will be of type {@code refc} if the lookup
- * class has full privileges to access the member. Otherwise
- * the member must be {@code protected} and the first argument
- * will be restricted in type to the lookup class.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * Because of the general <a href="MethodHandles.Lookup.html#equiv">equivalence</a> between {@code invokevirtual}
- * instructions and method handles produced by {@code findVirtual},
- * if the class is {@code MethodHandle} and the name string is
- * {@code invokeExact} or {@code invoke}, the resulting
- * method handle is equivalent to one produced by
- * {@link java.lang.invoke.MethodHandles#exactInvoker MethodHandles.exactInvoker} or
- * {@link java.lang.invoke.MethodHandles#invoker MethodHandles.invoker}
- * with the same {@code type} argument.
- *
- * <b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_concat = publicLookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-MethodHandle MH_hashCode = publicLookup().findVirtual(Object.class,
- "hashCode", methodType(int.class));
-MethodHandle MH_hashCode_String = publicLookup().findVirtual(String.class,
- "hashCode", methodType(int.class));
-assertEquals("xy", (String) MH_concat.invokeExact("x", "y"));
-assertEquals("xy".hashCode(), (int) MH_hashCode.invokeExact((Object)"xy"));
-assertEquals("xy".hashCode(), (int) MH_hashCode_String.invokeExact("xy"));
-// interface method:
-MethodHandle MH_subSequence = publicLookup().findVirtual(CharSequence.class,
- "subSequence", methodType(CharSequence.class, int.class, int.class));
-assertEquals("def", MH_subSequence.invoke("abcdefghi", 3, 6).toString());
-// constructor "internal method" must be accessed differently:
-MethodType MT_newString = methodType(void.class); //()V for new String()
-try { assertEquals("impossible", lookup()
- .findVirtual(String.class, "<init>", MT_newString));
- } catch (NoSuchMethodException ex) { } // OK
-MethodHandle MH_newString = publicLookup()
- .findConstructor(String.class, MT_newString);
-assertEquals("", (String) MH_newString.invokeExact());
- * }</pre></blockquote>
- *
- * @param refc the class or interface from which the method is accessed
- * @param name the name of the method
- * @param type the type of the method, with the receiver argument omitted
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails,
- * or if the method is {@code static}
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findVirtual(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- // Special case : when we're looking up a virtual method on the MethodHandles class
- // itself, we can return one of our specialized invokers.
- if (refc == MethodHandle.class) {
- MethodHandle mh = findVirtualForMH(name, type);
- if (mh != null) {
- return mh;
- }
- } else if (refc == VarHandle.class) {
- // Returns an non-exact invoker.
- MethodHandle mh = findVirtualForVH(name, type);
- if (mh != null) {
- return mh;
- }
- }
-
- Method method = refc.getInstanceMethod(name, type.ptypes());
- if (method == null) {
- // This is pretty ugly and a consequence of the MethodHandles API. We have to throw
- // an IAE and not an NSME if the method exists but is static (even though the RI's
- // IAE has a message that says "no such method"). We confine the ugliness and
- // slowness to the failure case, and allow getInstanceMethod to remain fairly
- // general.
- try {
- Method m = refc.getDeclaredMethod(name, type.ptypes());
- if (Modifier.isStatic(m.getModifiers())) {
- throw new IllegalAccessException("Method" + m + " is static");
- }
- } catch (NoSuchMethodException ignored) {
- }
-
- throw new NoSuchMethodException(name + " " + Arrays.toString(type.ptypes()));
- }
- checkReturnType(method, type);
-
- // We have a valid method, perform access checks.
- checkAccess(refc, method.getDeclaringClass(), method.getModifiers(), method.getName());
-
- // Insert the leading reference parameter.
- MethodType handleType = type.insertParameterTypes(0, refc);
- return createMethodHandle(method, MethodHandle.INVOKE_VIRTUAL, handleType);
- }
-
- /**
- * Produces a method handle which creates an object and initializes it, using
- * the constructor of the specified type.
- * The parameter types of the method handle will be those of the constructor,
- * while the return type will be a reference to the constructor's class.
- * The constructor and all its argument types must be accessible to the lookup object.
- * <p>
- * The requested type must have a return type of {@code void}.
- * (This is consistent with the JVM's treatment of constructor type descriptors.)
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the constructor's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If the returned method handle is invoked, the constructor's class will
- * be initialized, if it has not already been initialized.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_newArrayList = publicLookup().findConstructor(
- ArrayList.class, methodType(void.class, Collection.class));
-Collection orig = Arrays.asList("x", "y");
-Collection copy = (ArrayList) MH_newArrayList.invokeExact(orig);
-assert(orig != copy);
-assertEquals(orig, copy);
-// a variable-arity constructor:
-MethodHandle MH_newProcessBuilder = publicLookup().findConstructor(
- ProcessBuilder.class, methodType(void.class, String[].class));
-ProcessBuilder pb = (ProcessBuilder)
- MH_newProcessBuilder.invoke("x", "y", "z");
-assertEquals("[x, y, z]", pb.command().toString());
- * }</pre></blockquote>
- * @param refc the class or interface from which the method is accessed
- * @param type the type of the method, with the receiver argument omitted, and a void return type
- * @return the desired method handle
- * @throws NoSuchMethodException if the constructor does not exist
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findConstructor(Class<?> refc, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- if (refc.isArray()) {
- throw new NoSuchMethodException("no constructor for array class: " + refc.getName());
- }
- // The queried |type| is (PT1,PT2,..)V
- Constructor constructor = refc.getDeclaredConstructor(type.ptypes());
- if (constructor == null) {
- throw new NoSuchMethodException(
- "No constructor for " + constructor.getDeclaringClass() + " matching " + type);
- }
- checkAccess(refc, constructor.getDeclaringClass(), constructor.getModifiers(),
- constructor.getName());
-
- return createMethodHandleForConstructor(constructor);
- }
-
- private MethodHandle createMethodHandleForConstructor(Constructor constructor) {
- Class<?> refc = constructor.getDeclaringClass();
- MethodType constructorType =
- MethodType.methodType(refc, constructor.getParameterTypes());
- MethodHandle mh;
- if (refc == String.class) {
- // String constructors have optimized StringFactory methods
- // that matches returned type. These factory methods combine the
- // memory allocation and initialization calls for String objects.
- mh = new MethodHandleImpl(constructor.getArtMethod(), MethodHandle.INVOKE_DIRECT,
- constructorType);
- } else {
- // Constructors for all other classes use a Construct transformer to perform
- // their memory allocation and call to <init>.
- MethodType initType = initMethodType(constructorType);
- MethodHandle initHandle = new MethodHandleImpl(
- constructor.getArtMethod(), MethodHandle.INVOKE_DIRECT, initType);
- mh = new Transformers.Construct(initHandle, constructorType);
- }
-
- if (constructor.isVarArgs()) {
- mh = new Transformers.VarargsCollector(mh);
- }
- return mh;
- }
-
- private static MethodType initMethodType(MethodType constructorType) {
- // Returns a MethodType appropriate for class <init>
- // methods. Constructor MethodTypes have the form
- // (PT1,PT2,...)C and class <init> MethodTypes have the
- // form (C,PT1,PT2,...)V.
- assert constructorType.rtype() != void.class;
-
- // Insert constructorType C as the first parameter type in
- // the MethodType for <init>.
- Class<?> [] initPtypes = new Class<?> [constructorType.ptypes().length + 1];
- initPtypes[0] = constructorType.rtype();
- System.arraycopy(constructorType.ptypes(), 0, initPtypes, 1,
- constructorType.ptypes().length);
-
- // Set the return type for the <init> MethodType to be void.
- return MethodType.methodType(void.class, initPtypes);
- }
-
- /**
- * Produces an early-bound method handle for a virtual method.
- * It will bypass checks for overriding methods on the receiver,
- * <a href="MethodHandles.Lookup.html#equiv">as if called</a> from an {@code invokespecial}
- * instruction from within the explicitly specified {@code specialCaller}.
- * The type of the method handle will be that of the method,
- * with a suitably restricted receiver type prepended.
- * (The receiver type will be {@code specialCaller} or a subtype.)
- * The method and all its argument types must be accessible
- * to the lookup object.
- * <p>
- * Before method resolution,
- * if the explicitly specified caller class is not identical with the
- * lookup class, or if this lookup object does not have
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>
- * privileges, the access fails.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p style="font-size:smaller;">
- * <em>(Note: JVM internal methods named {@code "<init>"} are not visible to this API,
- * even though the {@code invokespecial} instruction can refer to them
- * in special circumstances. Use {@link #findConstructor findConstructor}
- * to access instance initialization methods in a safe manner.)</em>
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-static class Listie extends ArrayList {
- public String toString() { return "[wee Listie]"; }
- static Lookup lookup() { return MethodHandles.lookup(); }
-}
-...
-// no access to constructor via invokeSpecial:
-MethodHandle MH_newListie = Listie.lookup()
- .findConstructor(Listie.class, methodType(void.class));
-Listie l = (Listie) MH_newListie.invokeExact();
-try { assertEquals("impossible", Listie.lookup().findSpecial(
- Listie.class, "<init>", methodType(void.class), Listie.class));
- } catch (NoSuchMethodException ex) { } // OK
-// access to super and self methods via invokeSpecial:
-MethodHandle MH_super = Listie.lookup().findSpecial(
- ArrayList.class, "toString" , methodType(String.class), Listie.class);
-MethodHandle MH_this = Listie.lookup().findSpecial(
- Listie.class, "toString" , methodType(String.class), Listie.class);
-MethodHandle MH_duper = Listie.lookup().findSpecial(
- Object.class, "toString" , methodType(String.class), Listie.class);
-assertEquals("[]", (String) MH_super.invokeExact(l));
-assertEquals(""+l, (String) MH_this.invokeExact(l));
-assertEquals("[]", (String) MH_duper.invokeExact(l)); // ArrayList method
-try { assertEquals("inaccessible", Listie.lookup().findSpecial(
- String.class, "toString", methodType(String.class), Listie.class));
- } catch (IllegalAccessException ex) { } // OK
-Listie subl = new Listie() { public String toString() { return "[subclass]"; } };
-assertEquals(""+l, (String) MH_this.invokeExact(subl)); // Listie method
- * }</pre></blockquote>
- *
- * @param refc the class or interface from which the method is accessed
- * @param name the name of the method (which must not be "&lt;init&gt;")
- * @param type the type of the method, with the receiver argument omitted
- * @param specialCaller the proposed calling class to perform the {@code invokespecial}
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
public MethodHandle findSpecial(Class<?> refc, String name, MethodType type,
- Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException {
- if (specialCaller == null) {
- throw new NullPointerException("specialCaller == null");
- }
-
- if (type == null) {
- throw new NullPointerException("type == null");
- }
-
- if (name == null) {
- throw new NullPointerException("name == null");
- }
-
- if (refc == null) {
- throw new NullPointerException("ref == null");
- }
-
- // Make sure that the special caller is identical to the lookup class or that we have
- // private access.
- checkSpecialCaller(specialCaller);
-
- // Even though constructors are invoked using a "special" invoke, handles to them can't
- // be created using findSpecial. Callers must use findConstructor instead. Similarly,
- // there is no path for calling static class initializers.
- if (name.startsWith("<")) {
- throw new NoSuchMethodException(name + " is not a valid method name.");
- }
-
- Method method = refc.getDeclaredMethod(name, type.ptypes());
- checkReturnType(method, type);
- return findSpecial(method, type, refc, specialCaller);
- }
-
- private MethodHandle findSpecial(Method method, MethodType type,
- Class<?> refc, Class<?> specialCaller)
- throws IllegalAccessException {
- if (Modifier.isStatic(method.getModifiers())) {
- throw new IllegalAccessException("expected a non-static method:" + method);
- }
-
- if (Modifier.isPrivate(method.getModifiers())) {
- // Since this is a private method, we'll need to also make sure that the
- // lookup class is the same as the refering class. We've already checked that
- // the specialCaller is the same as the special lookup class, both of these must
- // be the same as the declaring class(*) in order to access the private method.
- //
- // (*) Well, this isn't true for nested classes but OpenJDK doesn't support those
- // either.
- if (refc != lookupClass()) {
- throw new IllegalAccessException("no private access for invokespecial : "
- + refc + ", from" + this);
- }
-
- // This is a private method, so there's nothing special to do.
- MethodType handleType = type.insertParameterTypes(0, refc);
- return createMethodHandle(method, MethodHandle.INVOKE_DIRECT, handleType);
- }
-
- // This is a public, protected or package-private method, which means we're expecting
- // invoke-super semantics. We'll have to restrict the receiver type appropriately on the
- // handle once we check that there really is a "super" relationship between them.
- if (!method.getDeclaringClass().isAssignableFrom(specialCaller)) {
- throw new IllegalAccessException(refc + "is not assignable from " + specialCaller);
- }
-
- // Note that we restrict the receiver to "specialCaller" instances.
- MethodType handleType = type.insertParameterTypes(0, specialCaller);
- return createMethodHandle(method, MethodHandle.INVOKE_SUPER, handleType);
- }
-
- /**
- * Produces a method handle giving read access to a non-static field.
- * The type of the method handle will have a return type of the field's
- * value type.
- * The method handle's single argument will be the instance containing
- * the field.
- * Access checking is performed immediately on behalf of the lookup class.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can load values from the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.IGET);
- }
-
- private MethodHandle findAccessor(Class<?> refc, String name, Class<?> type, int kind)
- throws NoSuchFieldException, IllegalAccessException {
- final Field field = findFieldOfType(refc, name, type);
- return findAccessor(field, refc, type, kind, true /* performAccessChecks */);
- }
-
- private MethodHandle findAccessor(Field field, Class<?> refc, Class<?> type, int kind,
- boolean performAccessChecks)
- throws IllegalAccessException {
- final boolean isSetterKind = kind == MethodHandle.IPUT || kind == MethodHandle.SPUT;
- final boolean isStaticKind = kind == MethodHandle.SGET || kind == MethodHandle.SPUT;
- commonFieldChecks(field, refc, type, isStaticKind, performAccessChecks);
- if (performAccessChecks) {
- final int modifiers = field.getModifiers();
- if (isSetterKind && Modifier.isFinal(modifiers)) {
- throw new IllegalAccessException("Field " + field + " is final");
- }
- }
-
- final MethodType methodType;
- switch (kind) {
- case MethodHandle.SGET:
- methodType = MethodType.methodType(type);
- break;
- case MethodHandle.SPUT:
- methodType = MethodType.methodType(void.class, type);
- break;
- case MethodHandle.IGET:
- methodType = MethodType.methodType(type, refc);
- break;
- case MethodHandle.IPUT:
- methodType = MethodType.methodType(void.class, refc, type);
- break;
- default:
- throw new IllegalArgumentException("Invalid kind " + kind);
- }
- return new MethodHandleImpl(field.getArtField(), kind, methodType);
- }
-
- /**
- * Produces a method handle giving write access to a non-static field.
- * The type of the method handle will have a void return type.
- * The method handle will take two arguments, the instance containing
- * the field, and the value to be stored.
- * The second argument will be of the field's value type.
- * Access checking is performed immediately on behalf of the lookup class.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can store values into the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.IPUT);
- }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
- /**
- * Produces a VarHandle giving access to a non-static field {@code name}
- * of type {@code type} declared in a class of type {@code recv}.
- * The VarHandle's variable type is {@code type} and it has one
- * coordinate type, {@code recv}.
- * <p>
- * Access checking is performed immediately on behalf of the lookup
- * class.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the field is declared {@code final}, then the write, atomic
- * update, numeric atomic update, and bitwise atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double} then numeric atomic update
- * access modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the field is declared {@code volatile} then the returned VarHandle
- * will override access to the field (effectively ignore the
- * {@code volatile} declaration) in accordance to its specified
- * access modes.
- * <p>
- * If the field type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param recv the receiver class, of type {@code R}, that declares the
- * non-static field
- * @param name the field's name
- * @param type the field's type, of type {@code T}
- * @return a VarHandle giving access to non-static fields.
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- * @since 9
- * @hide
- */
- public VarHandle findVarHandle(Class<?> recv, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- final Field field = findFieldOfType(recv, name, type);
- final boolean isStatic = false;
- final boolean performAccessChecks = true;
- commonFieldChecks(field, recv, type, isStatic, performAccessChecks);
- return FieldVarHandle.create(field);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
- // BEGIN Android-added: Common field resolution and access check methods.
- private Field findFieldOfType(final Class<?> refc, String name, Class<?> type)
- throws NoSuchFieldException {
- Field field = null;
-
- // Search refc and super classes for the field.
- for (Class<?> cls = refc; cls != null; cls = cls.getSuperclass()) {
- try {
- field = cls.getDeclaredField(name);
- break;
- } catch (NoSuchFieldException e) {
- }
- }
-
- if (field == null) {
- // Force failure citing refc.
- field = refc.getDeclaredField(name);
- }
-
- final Class<?> fieldType = field.getType();
- if (fieldType != type) {
- throw new NoSuchFieldException(name);
- }
- return field;
- }
-
- private void commonFieldChecks(Field field, Class<?> refc, Class<?> type,
- boolean isStatic, boolean performAccessChecks)
- throws IllegalAccessException {
- final int modifiers = field.getModifiers();
- if (performAccessChecks) {
- checkAccess(refc, field.getDeclaringClass(), modifiers, field.getName());
- }
- if (Modifier.isStatic(modifiers) != isStatic) {
- String reason = "Field " + field + " is " +
- (isStatic ? "not " : "") + "static";
- throw new IllegalAccessException(reason);
- }
- }
- // END Android-added: Common field resolution and access check methods.
-
- /**
- * Produces a method handle giving read access to a static field.
- * The type of the method handle will have a return type of the field's
- * value type.
- * The method handle will take no arguments.
- * Access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can load values from the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findStaticGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.SGET);
- }
-
- /**
- * Produces a method handle giving write access to a static field.
- * The type of the method handle will have a void return type.
- * The method handle will take a single
- * argument, of the field's value type, the value to be stored.
- * Access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can store values into the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findStaticSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.SPUT);
- }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
- /**
- * Produces a VarHandle giving access to a static field {@code name} of
- * type {@code type} declared in a class of type {@code decl}.
- * The VarHandle's variable type is {@code type} and it has no
- * coordinate types.
- * <p>
- * Access checking is performed immediately on behalf of the lookup
- * class.
- * <p>
- * If the returned VarHandle is operated on, the declaring class will be
- * initialized, if it has not already been initialized.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the field is declared {@code final}, then the write, atomic
- * update, numeric atomic update, and bitwise atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double}, then numeric atomic update
- * access modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the field is declared {@code volatile} then the returned VarHandle
- * will override access to the field (effectively ignore the
- * {@code volatile} declaration) in accordance to its specified
- * access modes.
- * <p>
- * If the field type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param decl the class that declares the static field
- * @param name the field's name
- * @param type the field's type, of type {@code T}
- * @return a VarHandle giving access to a static field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- * @since 9
- * @hide
- */
- public VarHandle findStaticVarHandle(Class<?> decl, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- final Field field = findFieldOfType(decl, name, type);
- final boolean isStatic = true;
- final boolean performAccessChecks = true;
- commonFieldChecks(field, decl, type, isStatic, performAccessChecks);
- return FieldVarHandle.create(field);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
- /**
- * Produces an early-bound method handle for a non-static method.
- * The receiver must have a supertype {@code defc} in which a method
- * of the given name and type is accessible to the lookup class.
- * The method and all its argument types must be accessible to the lookup object.
- * The type of the method handle will be that of the method,
- * without any insertion of an additional receiver parameter.
- * The given receiver will be bound into the method handle,
- * so that every call to the method handle will invoke the
- * requested method on the given receiver.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set
- * <em>and</em> the trailing array argument is not the only argument.
- * (If the trailing array argument is the only argument,
- * the given receiver value will be bound to it.)
- * <p>
- * This is equivalent to the following code:
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle mh0 = lookup().findVirtual(defc, name, type);
-MethodHandle mh1 = mh0.bindTo(receiver);
-MethodType mt1 = mh1.type();
-if (mh0.isVarargsCollector())
- mh1 = mh1.asVarargsCollector(mt1.parameterType(mt1.parameterCount()-1));
-return mh1;
- * }</pre></blockquote>
- * where {@code defc} is either {@code receiver.getClass()} or a super
- * type of that class, in which the requested method is accessible
- * to the lookup class.
- * (Note that {@code bindTo} does not preserve variable arity.)
- * @param receiver the object from which the method is accessed
- * @param name the name of the method
- * @param type the type of the method, with the receiver argument omitted
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- * @see MethodHandle#bindTo
- * @see #findVirtual
- */
- public MethodHandle bind(Object receiver, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- MethodHandle handle = findVirtual(receiver.getClass(), name, type);
- MethodHandle adapter = handle.bindTo(receiver);
- MethodType adapterType = adapter.type();
- if (handle.isVarargsCollector()) {
- adapter = adapter.asVarargsCollector(
- adapterType.parameterType(adapterType.parameterCount() - 1));
- }
-
- return adapter;
- }
-
- /**
- * Makes a <a href="MethodHandleInfo.html#directmh">direct method handle</a>
- * to <i>m</i>, if the lookup class has permission.
- * If <i>m</i> is non-static, the receiver argument is treated as an initial argument.
- * If <i>m</i> is virtual, overriding is respected on every call.
- * Unlike the Core Reflection API, exceptions are <em>not</em> wrapped.
- * The type of the method handle will be that of the method,
- * with the receiver type prepended (but only if it is non-static).
- * If the method's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * If <i>m</i> is not public, do not share the resulting handle with untrusted parties.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If <i>m</i> is static, and
- * if the returned method handle is invoked, the method's class will
- * be initialized, if it has not already been initialized.
- * @param m the reflected method
- * @return a method handle which can invoke the reflected method
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflect(Method m) throws IllegalAccessException {
- if (m == null) {
- throw new NullPointerException("m == null");
- }
-
- MethodType methodType = MethodType.methodType(m.getReturnType(),
- m.getParameterTypes());
-
- // We should only perform access checks if setAccessible hasn't been called yet.
- if (!m.isAccessible()) {
- checkAccess(m.getDeclaringClass(), m.getDeclaringClass(), m.getModifiers(),
- m.getName());
- }
-
- if (Modifier.isStatic(m.getModifiers())) {
- return createMethodHandle(m, MethodHandle.INVOKE_STATIC, methodType);
- } else {
- methodType = methodType.insertParameterTypes(0, m.getDeclaringClass());
- return createMethodHandle(m, MethodHandle.INVOKE_VIRTUAL, methodType);
- }
- }
-
- /**
- * Produces a method handle for a reflected method.
- * It will bypass checks for overriding methods on the receiver,
- * <a href="MethodHandles.Lookup.html#equiv">as if called</a> from an {@code invokespecial}
- * instruction from within the explicitly specified {@code specialCaller}.
- * The type of the method handle will be that of the method,
- * with a suitably restricted receiver type prepended.
- * (The receiver type will be {@code specialCaller} or a subtype.)
- * If the method's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class,
- * as if {@code invokespecial} instruction were being linked.
- * <p>
- * Before method resolution,
- * if the explicitly specified caller class is not identical with the
- * lookup class, or if this lookup object does not have
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>
- * privileges, the access fails.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * @param m the reflected method
- * @param specialCaller the class nominally calling the method
- * @return a method handle which can invoke the reflected method
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle unreflectSpecial(Method m, Class<?> specialCaller) throws IllegalAccessException {
- if (m == null) {
- throw new NullPointerException("m == null");
- }
-
- if (specialCaller == null) {
- throw new NullPointerException("specialCaller == null");
- }
-
- if (!m.isAccessible()) {
- checkSpecialCaller(specialCaller);
- }
-
- final MethodType methodType = MethodType.methodType(m.getReturnType(),
- m.getParameterTypes());
- return findSpecial(m, methodType, m.getDeclaringClass() /* refc */, specialCaller);
- }
-
- /**
- * Produces a method handle for a reflected constructor.
- * The type of the method handle will be that of the constructor,
- * with the return type changed to the declaring class.
- * The method handle will perform a {@code newInstance} operation,
- * creating a new instance of the constructor's class on the
- * arguments passed to the method handle.
- * <p>
- * If the constructor's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the constructor's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If the returned method handle is invoked, the constructor's class will
- * be initialized, if it has not already been initialized.
- * @param c the reflected constructor
- * @return a method handle which can invoke the reflected constructor
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflectConstructor(Constructor<?> c) throws IllegalAccessException {
- if (c == null) {
- throw new NullPointerException("c == null");
- }
-
- if (!c.isAccessible()) {
- checkAccess(c.getDeclaringClass(), c.getDeclaringClass(), c.getModifiers(),
- c.getName());
- }
-
- return createMethodHandleForConstructor(c);
- }
-
- /**
- * Produces a method handle giving read access to a reflected field.
- * The type of the method handle will have a return type of the field's
- * value type.
- * If the field is static, the method handle will take no arguments.
- * Otherwise, its single argument will be the instance containing
- * the field.
- * If the field's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the field is static, and
- * if the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param f the reflected field
- * @return a method handle which can load values from the reflected field
- * @throws IllegalAccessException if access checking fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflectGetter(Field f) throws IllegalAccessException {
- return findAccessor(f, f.getDeclaringClass(), f.getType(),
- Modifier.isStatic(f.getModifiers()) ? MethodHandle.SGET : MethodHandle.IGET,
- !f.isAccessible() /* performAccessChecks */);
- }
-
- /**
- * Produces a method handle giving write access to a reflected field.
- * The type of the method handle will have a void return type.
- * If the field is static, the method handle will take a single
- * argument, of the field's value type, the value to be stored.
- * Otherwise, the two arguments will be the instance containing
- * the field, and the value to be stored.
- * If the field's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the field is static, and
- * if the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param f the reflected field
- * @return a method handle which can store values into the reflected field
- * @throws IllegalAccessException if access checking fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflectSetter(Field f) throws IllegalAccessException {
- return findAccessor(f, f.getDeclaringClass(), f.getType(),
- Modifier.isStatic(f.getModifiers()) ? MethodHandle.SPUT : MethodHandle.IPUT,
- !f.isAccessible() /* performAccessChecks */);
- }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
- /**
- * Produces a VarHandle giving access to a reflected field {@code f}
- * of type {@code T} declared in a class of type {@code R}.
- * The VarHandle's variable type is {@code T}.
- * If the field is non-static the VarHandle has one coordinate type,
- * {@code R}. Otherwise, the field is static, and the VarHandle has no
- * coordinate types.
- * <p>
- * Access checking is performed immediately on behalf of the lookup
- * class, regardless of the value of the field's {@code accessible}
- * flag.
- * <p>
- * If the field is static, and if the returned VarHandle is operated
- * on, the field's declaring class will be initialized, if it has not
- * already been initialized.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the field is declared {@code final}, then the write, atomic
- * update, numeric atomic update, and bitwise atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double} then numeric atomic update
- * access modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the field is declared {@code volatile} then the returned VarHandle
- * will override access to the field (effectively ignore the
- * {@code volatile} declaration) in accordance to its specified
- * access modes.
- * <p>
- * If the field type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param f the reflected field, with a field of type {@code T}, and
- * a declaring class of type {@code R}
- * @return a VarHandle giving access to non-static fields or a static
- * field
- * @throws IllegalAccessException if access checking fails
- * @throws NullPointerException if the argument is null
- * @since 9
- * @hide
- */
- public VarHandle unreflectVarHandle(Field f) throws IllegalAccessException {
- final boolean isStatic = Modifier.isStatic(f.getModifiers());
- final boolean performAccessChecks = true;
- commonFieldChecks(f, f.getDeclaringClass(), f.getType(), isStatic, performAccessChecks);
- return FieldVarHandle.create(f);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
- /**
- * Cracks a <a href="MethodHandleInfo.html#directmh">direct method handle</a>
- * created by this lookup object or a similar one.
- * Security and access checks are performed to ensure that this lookup object
- * is capable of reproducing the target method handle.
- * This means that the cracking may fail if target is a direct method handle
- * but was created by an unrelated lookup object.
- * This can happen if the method handle is <a href="MethodHandles.Lookup.html#callsens">caller sensitive</a>
- * and was created by a lookup object for a different class.
- * @param target a direct method handle to crack into symbolic reference components
- * @return a symbolic reference which can be used to reconstruct this method handle from this lookup object
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws IllegalArgumentException if the target is not a direct method handle or if access checking fails
- * @exception NullPointerException if the target is {@code null}
- * @see MethodHandleInfo
- * @since 1.8
- */
- public MethodHandleInfo revealDirect(MethodHandle target) {
- MethodHandleImpl directTarget = getMethodHandleImpl(target);
- MethodHandleInfo info = directTarget.reveal();
-
- try {
- checkAccess(lookupClass(), info.getDeclaringClass(), info.getModifiers(),
- info.getName());
- } catch (IllegalAccessException exception) {
- throw new IllegalArgumentException("Unable to access memeber.", exception);
- }
-
- return info;
- }
-
- private boolean hasPrivateAccess() {
- return (allowedModes & PRIVATE) != 0;
- }
-
- /** Check public/protected/private bits on the symbolic reference class and its member. */
- void checkAccess(Class<?> refc, Class<?> defc, int mods, String methName)
- throws IllegalAccessException {
- int allowedModes = this.allowedModes;
-
- if (Modifier.isProtected(mods) &&
- defc == Object.class &&
- "clone".equals(methName) &&
- refc.isArray()) {
- // The JVM does this hack also.
- // (See ClassVerifier::verify_invoke_instructions
- // and LinkResolver::check_method_accessability.)
- // Because the JVM does not allow separate methods on array types,
- // there is no separate method for int[].clone.
- // All arrays simply inherit Object.clone.
- // But for access checking logic, we make Object.clone
- // (normally protected) appear to be public.
- // Later on, when the DirectMethodHandle is created,
- // its leading argument will be restricted to the
- // requested array type.
- // N.B. The return type is not adjusted, because
- // that is *not* the bytecode behavior.
- mods ^= Modifier.PROTECTED | Modifier.PUBLIC;
- }
-
- if (Modifier.isProtected(mods) && Modifier.isConstructor(mods)) {
- // cannot "new" a protected ctor in a different package
- mods ^= Modifier.PROTECTED;
- }
-
- if (Modifier.isPublic(mods) && Modifier.isPublic(refc.getModifiers()) && allowedModes != 0)
- return; // common case
- int requestedModes = fixmods(mods); // adjust 0 => PACKAGE
- if ((requestedModes & allowedModes) != 0) {
- if (VerifyAccess.isMemberAccessible(refc, defc, mods, lookupClass(), allowedModes))
- return;
- } else {
- // Protected members can also be checked as if they were package-private.
- if ((requestedModes & PROTECTED) != 0 && (allowedModes & PACKAGE) != 0
- && VerifyAccess.isSamePackage(defc, lookupClass()))
- return;
- }
-
- throwMakeAccessException(accessFailedMessage(refc, defc, mods), this);
- }
-
- String accessFailedMessage(Class<?> refc, Class<?> defc, int mods) {
- // check the class first:
- boolean classOK = (Modifier.isPublic(defc.getModifiers()) &&
- (defc == refc ||
- Modifier.isPublic(refc.getModifiers())));
- if (!classOK && (allowedModes & PACKAGE) != 0) {
- classOK = (VerifyAccess.isClassAccessible(defc, lookupClass(), ALL_MODES) &&
- (defc == refc ||
- VerifyAccess.isClassAccessible(refc, lookupClass(), ALL_MODES)));
- }
- if (!classOK)
- return "class is not public";
- if (Modifier.isPublic(mods))
- return "access to public member failed"; // (how?)
- if (Modifier.isPrivate(mods))
- return "member is private";
- if (Modifier.isProtected(mods))
- return "member is protected";
- return "member is private to package";
- }
-
- // Android-changed: checkSpecialCaller assumes that ALLOW_NESTMATE_ACCESS = false,
- // as in upstream OpenJDK.
- //
- // private static final boolean ALLOW_NESTMATE_ACCESS = false;
-
- private void checkSpecialCaller(Class<?> specialCaller) throws IllegalAccessException {
- // Android-changed: No support for TRUSTED lookups. Also construct the
- // IllegalAccessException by hand because the upstream code implicitly assumes
- // that the lookupClass == specialCaller.
- //
- // if (allowedModes == TRUSTED) return;
- if (!hasPrivateAccess() || (specialCaller != lookupClass())) {
- throw new IllegalAccessException("no private access for invokespecial : "
- + specialCaller + ", from" + this);
- }
- }
+ Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException { return null; }
- private void throwMakeAccessException(String message, Object from) throws
- IllegalAccessException{
- message = message + ": "+ toString();
- if (from != null) message += ", from " + from;
- throw new IllegalAccessException(message);
- }
+ public MethodHandle findGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- private void checkReturnType(Method method, MethodType methodType)
- throws NoSuchMethodException {
- if (method.getReturnType() != methodType.rtype()) {
- throw new NoSuchMethodException(method.getName() + methodType);
- }
- }
- }
-
- /**
- * "Cracks" {@code target} to reveal the underlying {@code MethodHandleImpl}.
- */
- private static MethodHandleImpl getMethodHandleImpl(MethodHandle target) {
- // Special case : We implement handles to constructors as transformers,
- // so we must extract the underlying handle from the transformer.
- if (target instanceof Transformers.Construct) {
- target = ((Transformers.Construct) target).getConstructorHandle();
- }
+ public MethodHandle findSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- // Special case: Var-args methods are also implemented as Transformers,
- // so we should get the underlying handle in that case as well.
- if (target instanceof Transformers.VarargsCollector) {
- target = target.asFixedArity();
- }
+ public MethodHandle findStaticGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- if (target instanceof MethodHandleImpl) {
- return (MethodHandleImpl) target;
- }
+ public MethodHandle findStaticSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- throw new IllegalArgumentException(target + " is not a direct handle");
- }
+ public MethodHandle bind(Object receiver, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- // BEGIN Android-added: method to check if a class is an array.
- private static void checkClassIsArray(Class<?> c) {
- if (!c.isArray()) {
- throw new IllegalArgumentException("Not an array type: " + c);
- }
- }
+ public MethodHandle unreflect(Method m) throws IllegalAccessException { return null; }
- private static void checkTypeIsViewable(Class<?> componentType) {
- if (componentType == short.class ||
- componentType == char.class ||
- componentType == int.class ||
- componentType == long.class ||
- componentType == float.class ||
- componentType == double.class) {
- return;
- }
- throw new UnsupportedOperationException("Component type not supported: " + componentType);
- }
- // END Android-added: method to check if a class is an array.
+ public MethodHandle unreflectSpecial(Method m, Class<?> specialCaller) throws IllegalAccessException { return null; }
- /**
- * Produces a method handle giving read access to elements of an array.
- * The type of the method handle will have a return type of the array's
- * element type. Its first argument will be the array type,
- * and the second will be {@code int}.
- * @param arrayClass an array type
- * @return a method handle which can load values from the given array type
- * @throws NullPointerException if the argument is null
- * @throws IllegalArgumentException if arrayClass is not an array type
- */
- public static
- MethodHandle arrayElementGetter(Class<?> arrayClass) throws IllegalArgumentException {
- checkClassIsArray(arrayClass);
- final Class<?> componentType = arrayClass.getComponentType();
- if (componentType.isPrimitive()) {
- try {
- return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class,
- "arrayElementGetter",
- MethodType.methodType(componentType, arrayClass, int.class));
- } catch (NoSuchMethodException | IllegalAccessException exception) {
- throw new AssertionError(exception);
- }
- }
+ public MethodHandle unreflectConstructor(Constructor<?> c) throws IllegalAccessException { return null; }
- return new Transformers.ReferenceArrayElementGetter(arrayClass);
- }
+ public MethodHandle unreflectGetter(Field f) throws IllegalAccessException { return null; }
- /** @hide */ public static byte arrayElementGetter(byte[] array, int i) { return array[i]; }
- /** @hide */ public static boolean arrayElementGetter(boolean[] array, int i) { return array[i]; }
- /** @hide */ public static char arrayElementGetter(char[] array, int i) { return array[i]; }
- /** @hide */ public static short arrayElementGetter(short[] array, int i) { return array[i]; }
- /** @hide */ public static int arrayElementGetter(int[] array, int i) { return array[i]; }
- /** @hide */ public static long arrayElementGetter(long[] array, int i) { return array[i]; }
- /** @hide */ public static float arrayElementGetter(float[] array, int i) { return array[i]; }
- /** @hide */ public static double arrayElementGetter(double[] array, int i) { return array[i]; }
+ public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { return null; }
- /**
- * Produces a method handle giving write access to elements of an array.
- * The type of the method handle will have a void return type.
- * Its last argument will be the array's element type.
- * The first and second arguments will be the array type and int.
- * @param arrayClass the class of an array
- * @return a method handle which can store values into the array type
- * @throws NullPointerException if the argument is null
- * @throws IllegalArgumentException if arrayClass is not an array type
- */
- public static
- MethodHandle arrayElementSetter(Class<?> arrayClass) throws IllegalArgumentException {
- checkClassIsArray(arrayClass);
- final Class<?> componentType = arrayClass.getComponentType();
- if (componentType.isPrimitive()) {
- try {
- return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class,
- "arrayElementSetter",
- MethodType.methodType(void.class, arrayClass, int.class, componentType));
- } catch (NoSuchMethodException | IllegalAccessException exception) {
- throw new AssertionError(exception);
- }
- }
+ public MethodHandleInfo revealDirect(MethodHandle target) { return null; }
- return new Transformers.ReferenceArrayElementSetter(arrayClass);
}
- /** @hide */
- public static void arrayElementSetter(byte[] array, int i, byte val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(boolean[] array, int i, boolean val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(char[] array, int i, char val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(short[] array, int i, short val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(int[] array, int i, int val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(long[] array, int i, long val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(float[] array, int i, float val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(double[] array, int i, double val) { array[i] = val; }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory methods.
- /**
- * Produces a VarHandle giving access to elements of an array of type
- * {@code arrayClass}. The VarHandle's variable type is the component type
- * of {@code arrayClass} and the list of coordinate types is
- * {@code (arrayClass, int)}, where the {@code int} coordinate type
- * corresponds to an argument that is an index into an array.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the component type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double} then numeric atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the component type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param arrayClass the class of an array, of type {@code T[]}
- * @return a VarHandle giving access to elements of an array
- * @throws NullPointerException if the arrayClass is null
- * @throws IllegalArgumentException if arrayClass is not an array type
- * @since 9
- * @hide
- */
public static
- VarHandle arrayElementVarHandle(Class<?> arrayClass) throws IllegalArgumentException {
- checkClassIsArray(arrayClass);
- return ArrayElementVarHandle.create(arrayClass);
- }
+ MethodHandle arrayElementGetter(Class<?> arrayClass) throws IllegalArgumentException { return null; }
- /**
- * Produces a VarHandle giving access to elements of a {@code byte[]} array
- * viewed as if it were a different primitive array type, such as
- * {@code int[]} or {@code long[]}.
- * The VarHandle's variable type is the component type of
- * {@code viewArrayClass} and the list of coordinate types is
- * {@code (byte[], int)}, where the {@code int} coordinate type
- * corresponds to an argument that is an index into a {@code byte[]} array.
- * The returned VarHandle accesses bytes at an index in a {@code byte[]}
- * array, composing bytes to or from a value of the component type of
- * {@code viewArrayClass} according to the given endianness.
- * <p>
- * The supported component types (variables types) are {@code short},
- * {@code char}, {@code int}, {@code long}, {@code float} and
- * {@code double}.
- * <p>
- * Access of bytes at a given index will result in an
- * {@code IndexOutOfBoundsException} if the index is less than {@code 0}
- * or greater than the {@code byte[]} array length minus the size (in bytes)
- * of {@code T}.
- * <p>
- * Access of bytes at an index may be aligned or misaligned for {@code T},
- * with respect to the underlying memory address, {@code A} say, associated
- * with the array and index.
- * If access is misaligned then access for anything other than the
- * {@code get} and {@code set} access modes will result in an
- * {@code IllegalStateException}. In such cases atomic access is only
- * guaranteed with respect to the largest power of two that divides the GCD
- * of {@code A} and the size (in bytes) of {@code T}.
- * If access is aligned then following access modes are supported and are
- * guaranteed to support atomic access:
- * <ul>
- * <li>read write access modes for all {@code T}, with the exception of
- * access modes {@code get} and {@code set} for {@code long} and
- * {@code double} on 32-bit platforms.
- * <li>atomic update access modes for {@code int}, {@code long},
- * {@code float} or {@code double}.
- * (Future major platform releases of the JDK may support additional
- * types for certain currently unsupported access modes.)
- * <li>numeric atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * <li>bitwise atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * </ul>
- * <p>
- * Misaligned access, and therefore atomicity guarantees, may be determined
- * for {@code byte[]} arrays without operating on a specific array. Given
- * an {@code index}, {@code T} and it's corresponding boxed type,
- * {@code T_BOX}, misalignment may be determined as follows:
- * <pre>{@code
- * int sizeOfT = T_BOX.BYTES; // size in bytes of T
- * int misalignedAtZeroIndex = ByteBuffer.wrap(new byte[0]).
- * alignmentOffset(0, sizeOfT);
- * int misalignedAtIndex = (misalignedAtZeroIndex + index) % sizeOfT;
- * boolean isMisaligned = misalignedAtIndex != 0;
- * }</pre>
- * <p>
- * If the variable type is {@code float} or {@code double} then atomic
- * update access modes compare values using their bitwise representation
- * (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @param viewArrayClass the view array class, with a component type of
- * type {@code T}
- * @param byteOrder the endianness of the view array elements, as
- * stored in the underlying {@code byte} array
- * @return a VarHandle giving access to elements of a {@code byte[]} array
- * viewed as if elements corresponding to the components type of the view
- * array class
- * @throws NullPointerException if viewArrayClass or byteOrder is null
- * @throws IllegalArgumentException if viewArrayClass is not an array type
- * @throws UnsupportedOperationException if the component type of
- * viewArrayClass is not supported as a variable type
- * @since 9
- * @hide
- */
public static
- VarHandle byteArrayViewVarHandle(Class<?> viewArrayClass,
- ByteOrder byteOrder) throws IllegalArgumentException {
- checkClassIsArray(viewArrayClass);
- checkTypeIsViewable(viewArrayClass.getComponentType());
- return ByteArrayViewVarHandle.create(viewArrayClass, byteOrder);
- }
+ MethodHandle arrayElementSetter(Class<?> arrayClass) throws IllegalArgumentException { return null; }
- /**
- * Produces a VarHandle giving access to elements of a {@code ByteBuffer}
- * viewed as if it were an array of elements of a different primitive
- * component type to that of {@code byte}, such as {@code int[]} or
- * {@code long[]}.
- * The VarHandle's variable type is the component type of
- * {@code viewArrayClass} and the list of coordinate types is
- * {@code (ByteBuffer, int)}, where the {@code int} coordinate type
- * corresponds to an argument that is an index into a {@code byte[]} array.
- * The returned VarHandle accesses bytes at an index in a
- * {@code ByteBuffer}, composing bytes to or from a value of the component
- * type of {@code viewArrayClass} according to the given endianness.
- * <p>
- * The supported component types (variables types) are {@code short},
- * {@code char}, {@code int}, {@code long}, {@code float} and
- * {@code double}.
- * <p>
- * Access will result in a {@code ReadOnlyBufferException} for anything
- * other than the read access modes if the {@code ByteBuffer} is read-only.
- * <p>
- * Access of bytes at a given index will result in an
- * {@code IndexOutOfBoundsException} if the index is less than {@code 0}
- * or greater than the {@code ByteBuffer} limit minus the size (in bytes) of
- * {@code T}.
- * <p>
- * Access of bytes at an index may be aligned or misaligned for {@code T},
- * with respect to the underlying memory address, {@code A} say, associated
- * with the {@code ByteBuffer} and index.
- * If access is misaligned then access for anything other than the
- * {@code get} and {@code set} access modes will result in an
- * {@code IllegalStateException}. In such cases atomic access is only
- * guaranteed with respect to the largest power of two that divides the GCD
- * of {@code A} and the size (in bytes) of {@code T}.
- * If access is aligned then following access modes are supported and are
- * guaranteed to support atomic access:
- * <ul>
- * <li>read write access modes for all {@code T}, with the exception of
- * access modes {@code get} and {@code set} for {@code long} and
- * {@code double} on 32-bit platforms.
- * <li>atomic update access modes for {@code int}, {@code long},
- * {@code float} or {@code double}.
- * (Future major platform releases of the JDK may support additional
- * types for certain currently unsupported access modes.)
- * <li>numeric atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * <li>bitwise atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * </ul>
- * <p>
- * Misaligned access, and therefore atomicity guarantees, may be determined
- * for a {@code ByteBuffer}, {@code bb} (direct or otherwise), an
- * {@code index}, {@code T} and it's corresponding boxed type,
- * {@code T_BOX}, as follows:
- * <pre>{@code
- * int sizeOfT = T_BOX.BYTES; // size in bytes of T
- * ByteBuffer bb = ...
- * int misalignedAtIndex = bb.alignmentOffset(index, sizeOfT);
- * boolean isMisaligned = misalignedAtIndex != 0;
- * }</pre>
- * <p>
- * If the variable type is {@code float} or {@code double} then atomic
- * update access modes compare values using their bitwise representation
- * (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @param viewArrayClass the view array class, with a component type of
- * type {@code T}
- * @param byteOrder the endianness of the view array elements, as
- * stored in the underlying {@code ByteBuffer} (Note this overrides the
- * endianness of a {@code ByteBuffer})
- * @return a VarHandle giving access to elements of a {@code ByteBuffer}
- * viewed as if elements corresponding to the components type of the view
- * array class
- * @throws NullPointerException if viewArrayClass or byteOrder is null
- * @throws IllegalArgumentException if viewArrayClass is not an array type
- * @throws UnsupportedOperationException if the component type of
- * viewArrayClass is not supported as a variable type
- * @since 9
- * @hide
- */
- public static
- VarHandle byteBufferViewVarHandle(Class<?> viewArrayClass,
- ByteOrder byteOrder) throws IllegalArgumentException {
- checkClassIsArray(viewArrayClass);
- checkTypeIsViewable(viewArrayClass.getComponentType());
- return ByteBufferViewVarHandle.create(viewArrayClass, byteOrder);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory methods.
-
- /// method handle invocation (reflective style)
-
- /**
- * Produces a method handle which will invoke any method handle of the
- * given {@code type}, with a given number of trailing arguments replaced by
- * a single trailing {@code Object[]} array.
- * The resulting invoker will be a method handle with the following
- * arguments:
- * <ul>
- * <li>a single {@code MethodHandle} target
- * <li>zero or more leading values (counted by {@code leadingArgCount})
- * <li>an {@code Object[]} array containing trailing arguments
- * </ul>
- * <p>
- * The invoker will invoke its target like a call to {@link MethodHandle#invoke invoke} with
- * the indicated {@code type}.
- * That is, if the target is exactly of the given {@code type}, it will behave
- * like {@code invokeExact}; otherwise it behave as if {@link MethodHandle#asType asType}
- * is used to convert the target to the required {@code type}.
- * <p>
- * The type of the returned invoker will not be the given {@code type}, but rather
- * will have all parameters except the first {@code leadingArgCount}
- * replaced by a single array of type {@code Object[]}, which will be
- * the final parameter.
- * <p>
- * Before invoking its target, the invoker will spread the final array, apply
- * reference casts as necessary, and unbox and widen primitive arguments.
- * If, when the invoker is called, the supplied array argument does
- * not have the correct number of elements, the invoker will throw
- * an {@link IllegalArgumentException} instead of invoking the target.
- * <p>
- * This method is equivalent to the following code (though it may be more efficient):
- * <blockquote><pre>{@code
-MethodHandle invoker = MethodHandles.invoker(type);
-int spreadArgCount = type.parameterCount() - leadingArgCount;
-invoker = invoker.asSpreader(Object[].class, spreadArgCount);
-return invoker;
- * }</pre></blockquote>
- * This method throws no reflective or security exceptions.
- * @param type the desired target type
- * @param leadingArgCount number of fixed arguments, to be passed unchanged to the target
- * @return a method handle suitable for invoking any method handle of the given type
- * @throws NullPointerException if {@code type} is null
- * @throws IllegalArgumentException if {@code leadingArgCount} is not in
- * the range from 0 to {@code type.parameterCount()} inclusive,
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
static public
- MethodHandle spreadInvoker(MethodType type, int leadingArgCount) {
- if (leadingArgCount < 0 || leadingArgCount > type.parameterCount())
- throw newIllegalArgumentException("bad argument count", leadingArgCount);
+ MethodHandle spreadInvoker(MethodType type, int leadingArgCount) { return null; }
- MethodHandle invoker = MethodHandles.invoker(type);
- int spreadArgCount = type.parameterCount() - leadingArgCount;
- invoker = invoker.asSpreader(Object[].class, spreadArgCount);
- return invoker;
- }
-
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke any method handle of the given type, as if by {@link MethodHandle#invokeExact invokeExact}.
- * The resulting invoker will have a type which is
- * exactly equal to the desired type, except that it will accept
- * an additional leading argument of type {@code MethodHandle}.
- * <p>
- * This method is equivalent to the following code (though it may be more efficient):
- * {@code publicLookup().findVirtual(MethodHandle.class, "invokeExact", type)}
- *
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * Invoker method handles can be useful when working with variable method handles
- * of unknown types.
- * For example, to emulate an {@code invokeExact} call to a variable method
- * handle {@code M}, extract its type {@code T},
- * look up the invoker method {@code X} for {@code T},
- * and call the invoker method, as {@code X.invoke(T, A...)}.
- * (It would not work to call {@code X.invokeExact}, since the type {@code T}
- * is unknown.)
- * If spreading, collecting, or other argument transformations are required,
- * they can be applied once to the invoker {@code X} and reused on many {@code M}
- * method handle values, as long as they are compatible with the type of {@code X}.
- * <p style="font-size:smaller;">
- * <em>(Note: The invoker method is not available via the Core Reflection API.
- * An attempt to call {@linkplain java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
- * on the declared {@code invokeExact} or {@code invoke} method will raise an
- * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}.)</em>
- * <p>
- * This method throws no reflective or security exceptions.
- * @param type the desired target type
- * @return a method handle suitable for invoking any method handle of the given type
- * @throws IllegalArgumentException if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
static public
- MethodHandle exactInvoker(MethodType type) {
- return new Transformers.Invoker(type, true /* isExactInvoker */);
- }
+ MethodHandle exactInvoker(MethodType type) { return null; }
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke any method handle compatible with the given type, as if by {@link MethodHandle#invoke invoke}.
- * The resulting invoker will have a type which is
- * exactly equal to the desired type, except that it will accept
- * an additional leading argument of type {@code MethodHandle}.
- * <p>
- * Before invoking its target, if the target differs from the expected type,
- * the invoker will apply reference casts as
- * necessary and box, unbox, or widen primitive values, as if by {@link MethodHandle#asType asType}.
- * Similarly, the return value will be converted as necessary.
- * If the target is a {@linkplain MethodHandle#asVarargsCollector variable arity method handle},
- * the required arity conversion will be made, again as if by {@link MethodHandle#asType asType}.
- * <p>
- * This method is equivalent to the following code (though it may be more efficient):
- * {@code publicLookup().findVirtual(MethodHandle.class, "invoke", type)}
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * A {@linkplain MethodType#genericMethodType general method type} is one which
- * mentions only {@code Object} arguments and return values.
- * An invoker for such a type is capable of calling any method handle
- * of the same arity as the general type.
- * <p style="font-size:smaller;">
- * <em>(Note: The invoker method is not available via the Core Reflection API.
- * An attempt to call {@linkplain java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
- * on the declared {@code invokeExact} or {@code invoke} method will raise an
- * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}.)</em>
- * <p>
- * This method throws no reflective or security exceptions.
- * @param type the desired target type
- * @return a method handle suitable for invoking any method handle convertible to the given type
- * @throws IllegalArgumentException if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
static public
- MethodHandle invoker(MethodType type) {
- return new Transformers.Invoker(type, false /* isExactInvoker */);
- }
-
- // BEGIN Android-added: resolver for VarHandle accessor methods.
- static private MethodHandle methodHandleForVarHandleAccessor(VarHandle.AccessMode accessMode,
- MethodType type,
- boolean isExactInvoker) {
- Class<?> refc = VarHandle.class;
- Method method;
- try {
- method = refc.getDeclaredMethod(accessMode.methodName(), Object[].class);
- } catch (NoSuchMethodException e) {
- throw new InternalError("No method for AccessMode " + accessMode, e);
- }
- MethodType methodType = type.insertParameterTypes(0, VarHandle.class);
- int kind = isExactInvoker ? MethodHandle.INVOKE_VAR_HANDLE_EXACT
- : MethodHandle.INVOKE_VAR_HANDLE;
- return new MethodHandleImpl(method.getArtMethod(), kind, methodType);
- }
- // END Android-added: resolver for VarHandle accessor methods.
+ MethodHandle invoker(MethodType type) { return null; }
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke a signature-polymorphic access mode method on any VarHandle whose
- * associated access mode type is compatible with the given type.
- * The resulting invoker will have a type which is exactly equal to the
- * desired given type, except that it will accept an additional leading
- * argument of type {@code VarHandle}.
- *
- * @param accessMode the VarHandle access mode
- * @param type the desired target type
- * @return a method handle suitable for invoking an access mode method of
- * any VarHandle whose access mode type is of the given type.
- * @since 9
- * @hide
- */
- static public
- MethodHandle varHandleExactInvoker(VarHandle.AccessMode accessMode, MethodType type) {
- return methodHandleForVarHandleAccessor(accessMode, type, true /* isExactInvoker */);
- }
-
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke a signature-polymorphic access mode method on any VarHandle whose
- * associated access mode type is compatible with the given type.
- * The resulting invoker will have a type which is exactly equal to the
- * desired given type, except that it will accept an additional leading
- * argument of type {@code VarHandle}.
- * <p>
- * Before invoking its target, if the access mode type differs from the
- * desired given type, the invoker will apply reference casts as necessary
- * and box, unbox, or widen primitive values, as if by
- * {@link MethodHandle#asType asType}. Similarly, the return value will be
- * converted as necessary.
- * <p>
- * This method is equivalent to the following code (though it may be more
- * efficient): {@code publicLookup().findVirtual(VarHandle.class, accessMode.name(), type)}
- *
- * @param accessMode the VarHandle access mode
- * @param type the desired target type
- * @return a method handle suitable for invoking an access mode method of
- * any VarHandle whose access mode type is convertible to the given
- * type.
- * @since 9
- * @hide
- */
- static public
- MethodHandle varHandleInvoker(VarHandle.AccessMode accessMode, MethodType type) {
- return methodHandleForVarHandleAccessor(accessMode, type, false /* isExactInvoker */);
- }
-
- // Android-changed: Basic invokers are not supported.
- //
- // static /*non-public*/
- // MethodHandle basicInvoker(MethodType type) {
- // return type.invokers().basicInvoker();
- // }
-
- /// method handle modification (creation from other method handles)
-
- /**
- * Produces a method handle which adapts the type of the
- * given method handle to a new type by pairwise argument and return type conversion.
- * The original type and new type must have the same number of arguments.
- * The resulting method handle is guaranteed to report a type
- * which is equal to the desired new type.
- * <p>
- * If the original type and new type are equal, returns target.
- * <p>
- * The same conversions are allowed as for {@link MethodHandle#asType MethodHandle.asType},
- * and some additional conversions are also applied if those conversions fail.
- * Given types <em>T0</em>, <em>T1</em>, one of the following conversions is applied
- * if possible, before or instead of any conversions done by {@code asType}:
- * <ul>
- * <li>If <em>T0</em> and <em>T1</em> are references, and <em>T1</em> is an interface type,
- * then the value of type <em>T0</em> is passed as a <em>T1</em> without a cast.
- * (This treatment of interfaces follows the usage of the bytecode verifier.)
- * <li>If <em>T0</em> is boolean and <em>T1</em> is another primitive,
- * the boolean is converted to a byte value, 1 for true, 0 for false.
- * (This treatment follows the usage of the bytecode verifier.)
- * <li>If <em>T1</em> is boolean and <em>T0</em> is another primitive,
- * <em>T0</em> is converted to byte via Java casting conversion (JLS 5.5),
- * and the low order bit of the result is tested, as if by {@code (x & 1) != 0}.
- * <li>If <em>T0</em> and <em>T1</em> are primitives other than boolean,
- * then a Java casting conversion (JLS 5.5) is applied.
- * (Specifically, <em>T0</em> will convert to <em>T1</em> by
- * widening and/or narrowing.)
- * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive, an unboxing
- * conversion will be applied at runtime, possibly followed
- * by a Java casting conversion (JLS 5.5) on the primitive value,
- * possibly followed by a conversion from byte to boolean by testing
- * the low-order bit.
- * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive,
- * and if the reference is null at runtime, a zero value is introduced.
- * </ul>
- * @param target the method handle to invoke after arguments are retyped
- * @param newType the expected type of the new method handle
- * @return a method handle which delegates to the target after performing
- * any necessary argument conversions, and arranges for any
- * necessary return value conversions
- * @throws NullPointerException if either argument is null
- * @throws WrongMethodTypeException if the conversion cannot be made
- * @see MethodHandle#asType
- */
public static
- MethodHandle explicitCastArguments(MethodHandle target, MethodType newType) {
- explicitCastArgumentsChecks(target, newType);
- // use the asTypeCache when possible:
- MethodType oldType = target.type();
- if (oldType == newType) return target;
- if (oldType.explicitCastEquivalentToAsType(newType)) {
- return target.asFixedArity().asType(newType);
- }
+ MethodHandle explicitCastArguments(MethodHandle target, MethodType newType) { return null; }
- return new Transformers.ExplicitCastArguments(target, newType);
- }
-
- private static void explicitCastArgumentsChecks(MethodHandle target, MethodType newType) {
- if (target.type().parameterCount() != newType.parameterCount()) {
- throw new WrongMethodTypeException("cannot explicitly cast " + target + " to " + newType);
- }
- }
-
- /**
- * Produces a method handle which adapts the calling sequence of the
- * given method handle to a new type, by reordering the arguments.
- * The resulting method handle is guaranteed to report a type
- * which is equal to the desired new type.
- * <p>
- * The given array controls the reordering.
- * Call {@code #I} the number of incoming parameters (the value
- * {@code newType.parameterCount()}, and call {@code #O} the number
- * of outgoing parameters (the value {@code target.type().parameterCount()}).
- * Then the length of the reordering array must be {@code #O},
- * and each element must be a non-negative number less than {@code #I}.
- * For every {@code N} less than {@code #O}, the {@code N}-th
- * outgoing argument will be taken from the {@code I}-th incoming
- * argument, where {@code I} is {@code reorder[N]}.
- * <p>
- * No argument or return value conversions are applied.
- * The type of each incoming argument, as determined by {@code newType},
- * must be identical to the type of the corresponding outgoing parameter
- * or parameters in the target method handle.
- * The return type of {@code newType} must be identical to the return
- * type of the original target.
- * <p>
- * The reordering array need not specify an actual permutation.
- * An incoming argument will be duplicated if its index appears
- * more than once in the array, and an incoming argument will be dropped
- * if its index does not appear in the array.
- * As in the case of {@link #dropArguments(MethodHandle,int,List) dropArguments},
- * incoming arguments which are not mentioned in the reordering array
- * are may be any type, as determined only by {@code newType}.
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodType intfn1 = methodType(int.class, int.class);
-MethodType intfn2 = methodType(int.class, int.class, int.class);
-MethodHandle sub = ... (int x, int y) -> (x-y) ...;
-assert(sub.type().equals(intfn2));
-MethodHandle sub1 = permuteArguments(sub, intfn2, 0, 1);
-MethodHandle rsub = permuteArguments(sub, intfn2, 1, 0);
-assert((int)rsub.invokeExact(1, 100) == 99);
-MethodHandle add = ... (int x, int y) -> (x+y) ...;
-assert(add.type().equals(intfn2));
-MethodHandle twice = permuteArguments(add, intfn1, 0, 0);
-assert(twice.type().equals(intfn1));
-assert((int)twice.invokeExact(21) == 42);
- * }</pre></blockquote>
- * @param target the method handle to invoke after arguments are reordered
- * @param newType the expected type of the new method handle
- * @param reorder an index array which controls the reordering
- * @return a method handle which delegates to the target after it
- * drops unused arguments and moves and/or duplicates the other arguments
- * @throws NullPointerException if any argument is null
- * @throws IllegalArgumentException if the index array length is not equal to
- * the arity of the target, or if any index array element
- * not a valid index for a parameter of {@code newType},
- * or if two corresponding parameter types in
- * {@code target.type()} and {@code newType} are not identical,
- */
public static
- MethodHandle permuteArguments(MethodHandle target, MethodType newType, int... reorder) {
- reorder = reorder.clone(); // get a private copy
- MethodType oldType = target.type();
- permuteArgumentChecks(reorder, newType, oldType);
-
- return new Transformers.PermuteArguments(newType, target, reorder);
- }
-
- // Android-changed: findFirstDupOrDrop is unused and removed.
- // private static int findFirstDupOrDrop(int[] reorder, int newArity);
-
- private static boolean permuteArgumentChecks(int[] reorder, MethodType newType, MethodType oldType) {
- if (newType.returnType() != oldType.returnType())
- throw newIllegalArgumentException("return types do not match",
- oldType, newType);
- if (reorder.length == oldType.parameterCount()) {
- int limit = newType.parameterCount();
- boolean bad = false;
- for (int j = 0; j < reorder.length; j++) {
- int i = reorder[j];
- if (i < 0 || i >= limit) {
- bad = true; break;
- }
- Class<?> src = newType.parameterType(i);
- Class<?> dst = oldType.parameterType(j);
- if (src != dst)
- throw newIllegalArgumentException("parameter types do not match after reorder",
- oldType, newType);
- }
- if (!bad) return true;
- }
- throw newIllegalArgumentException("bad reorder array: "+Arrays.toString(reorder));
- }
+ MethodHandle permuteArguments(MethodHandle target, MethodType newType, int... reorder) { return null; }
- /**
- * Produces a method handle of the requested return type which returns the given
- * constant value every time it is invoked.
- * <p>
- * Before the method handle is returned, the passed-in value is converted to the requested type.
- * If the requested type is primitive, widening primitive conversions are attempted,
- * else reference conversions are attempted.
- * <p>The returned method handle is equivalent to {@code identity(type).bindTo(value)}.
- * @param type the return type of the desired method handle
- * @param value the value to return
- * @return a method handle of the given return type and no arguments, which always returns the given value
- * @throws NullPointerException if the {@code type} argument is null
- * @throws ClassCastException if the value cannot be converted to the required return type
- * @throws IllegalArgumentException if the given type is {@code void.class}
- */
public static
- MethodHandle constant(Class<?> type, Object value) {
- if (type.isPrimitive()) {
- if (type == void.class)
- throw newIllegalArgumentException("void type");
- Wrapper w = Wrapper.forPrimitiveType(type);
- value = w.convert(value, type);
- }
-
- return new Transformers.Constant(type, value);
- }
+ MethodHandle constant(Class<?> type, Object value) { return null; }
- /**
- * Produces a method handle which returns its sole argument when invoked.
- * @param type the type of the sole parameter and return value of the desired method handle
- * @return a unary method handle which accepts and returns the given type
- * @throws NullPointerException if the argument is null
- * @throws IllegalArgumentException if the given type is {@code void.class}
- */
public static
- MethodHandle identity(Class<?> type) {
- if (type == null) {
- throw new NullPointerException("type == null");
- }
+ MethodHandle identity(Class<?> type) { return null; }
- if (type.isPrimitive()) {
- try {
- return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class, "identity",
- MethodType.methodType(type, type));
- } catch (NoSuchMethodException | IllegalAccessException e) {
- throw new AssertionError(e);
- }
- }
-
- return new Transformers.ReferenceIdentity(type);
- }
-
- /** @hide */ public static byte identity(byte val) { return val; }
- /** @hide */ public static boolean identity(boolean val) { return val; }
- /** @hide */ public static char identity(char val) { return val; }
- /** @hide */ public static short identity(short val) { return val; }
- /** @hide */ public static int identity(int val) { return val; }
- /** @hide */ public static long identity(long val) { return val; }
- /** @hide */ public static float identity(float val) { return val; }
- /** @hide */ public static double identity(double val) { return val; }
-
- /**
- * Provides a target method handle with one or more <em>bound arguments</em>
- * in advance of the method handle's invocation.
- * The formal parameters to the target corresponding to the bound
- * arguments are called <em>bound parameters</em>.
- * Returns a new method handle which saves away the bound arguments.
- * When it is invoked, it receives arguments for any non-bound parameters,
- * binds the saved arguments to their corresponding parameters,
- * and calls the original target.
- * <p>
- * The type of the new method handle will drop the types for the bound
- * parameters from the original target type, since the new method handle
- * will no longer require those arguments to be supplied by its callers.
- * <p>
- * Each given argument object must match the corresponding bound parameter type.
- * If a bound parameter type is a primitive, the argument object
- * must be a wrapper, and will be unboxed to produce the primitive value.
- * <p>
- * The {@code pos} argument selects which parameters are to be bound.
- * It may range between zero and <i>N-L</i> (inclusively),
- * where <i>N</i> is the arity of the target method handle
- * and <i>L</i> is the length of the values array.
- * @param target the method handle to invoke after the argument is inserted
- * @param pos where to insert the argument (zero for the first)
- * @param values the series of arguments to insert
- * @return a method handle which inserts an additional argument,
- * before calling the original method handle
- * @throws NullPointerException if the target or the {@code values} array is null
- * @see MethodHandle#bindTo
- */
public static
- MethodHandle insertArguments(MethodHandle target, int pos, Object... values) {
- int insCount = values.length;
- Class<?>[] ptypes = insertArgumentsChecks(target, insCount, pos);
- if (insCount == 0) {
- return target;
- }
-
- // Throw ClassCastExceptions early if we can't cast any of the provided values
- // to the required type.
- for (int i = 0; i < insCount; i++) {
- final Class<?> ptype = ptypes[pos + i];
- if (!ptype.isPrimitive()) {
- ptypes[pos + i].cast(values[i]);
- } else {
- // Will throw a ClassCastException if something terrible happens.
- values[i] = Wrapper.forPrimitiveType(ptype).convert(values[i], ptype);
- }
- }
+ MethodHandle insertArguments(MethodHandle target, int pos, Object... values) { return null; }
- return new Transformers.InsertArguments(target, pos, values);
- }
-
- // Android-changed: insertArgumentPrimitive is unused.
- //
- // private static BoundMethodHandle insertArgumentPrimitive(BoundMethodHandle result, int pos,
- // Class<?> ptype, Object value) {
- // Wrapper w = Wrapper.forPrimitiveType(ptype);
- // // perform unboxing and/or primitive conversion
- // value = w.convert(value, ptype);
- // switch (w) {
- // case INT: return result.bindArgumentI(pos, (int)value);
- // case LONG: return result.bindArgumentJ(pos, (long)value);
- // case FLOAT: return result.bindArgumentF(pos, (float)value);
- // case DOUBLE: return result.bindArgumentD(pos, (double)value);
- // default: return result.bindArgumentI(pos, ValueConversions.widenSubword(value));
- // }
- // }
-
- private static Class<?>[] insertArgumentsChecks(MethodHandle target, int insCount, int pos) throws RuntimeException {
- MethodType oldType = target.type();
- int outargs = oldType.parameterCount();
- int inargs = outargs - insCount;
- if (inargs < 0)
- throw newIllegalArgumentException("too many values to insert");
- if (pos < 0 || pos > inargs)
- throw newIllegalArgumentException("no argument type to append");
- return oldType.ptypes();
- }
-
- /**
- * Produces a method handle which will discard some dummy arguments
- * before calling some other specified <i>target</i> method handle.
- * The type of the new method handle will be the same as the target's type,
- * except it will also include the dummy argument types,
- * at some given position.
- * <p>
- * The {@code pos} argument may range between zero and <i>N</i>,
- * where <i>N</i> is the arity of the target.
- * If {@code pos} is zero, the dummy arguments will precede
- * the target's real arguments; if {@code pos} is <i>N</i>
- * they will come after.
- * <p>
- * <b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodType bigType = cat.type().insertParameterTypes(0, int.class, String.class);
-MethodHandle d0 = dropArguments(cat, 0, bigType.parameterList().subList(0,2));
-assertEquals(bigType, d0.type());
-assertEquals("yz", (String) d0.invokeExact(123, "x", "y", "z"));
- * }</pre></blockquote>
- * <p>
- * This method is also equivalent to the following code:
- * <blockquote><pre>
- * {@link #dropArguments(MethodHandle,int,Class...) dropArguments}{@code (target, pos, valueTypes.toArray(new Class[0]))}
- * </pre></blockquote>
- * @param target the method handle to invoke after the arguments are dropped
- * @param valueTypes the type(s) of the argument(s) to drop
- * @param pos position of first argument to drop (zero for the leftmost)
- * @return a method handle which drops arguments of the given types,
- * before calling the original method handle
- * @throws NullPointerException if the target is null,
- * or if the {@code valueTypes} list or any of its elements is null
- * @throws IllegalArgumentException if any element of {@code valueTypes} is {@code void.class},
- * or if {@code pos} is negative or greater than the arity of the target,
- * or if the new method handle's type would have too many parameters
- */
public static
- MethodHandle dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes) {
- valueTypes = copyTypes(valueTypes);
- MethodType oldType = target.type(); // get NPE
- int dropped = dropArgumentChecks(oldType, pos, valueTypes);
-
- MethodType newType = oldType.insertParameterTypes(pos, valueTypes);
- if (dropped == 0) {
- return target;
- }
-
- return new Transformers.DropArguments(newType, target, pos, valueTypes.size());
- }
-
- private static List<Class<?>> copyTypes(List<Class<?>> types) {
- Object[] a = types.toArray();
- return Arrays.asList(Arrays.copyOf(a, a.length, Class[].class));
- }
-
- private static int dropArgumentChecks(MethodType oldType, int pos, List<Class<?>> valueTypes) {
- int dropped = valueTypes.size();
- MethodType.checkSlotCount(dropped);
- int outargs = oldType.parameterCount();
- int inargs = outargs + dropped;
- if (pos < 0 || pos > outargs)
- throw newIllegalArgumentException("no argument type to remove"
- + Arrays.asList(oldType, pos, valueTypes, inargs, outargs)
- );
- return dropped;
- }
+ MethodHandle dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes) { return null; }
- /**
- * Produces a method handle which will discard some dummy arguments
- * before calling some other specified <i>target</i> method handle.
- * The type of the new method handle will be the same as the target's type,
- * except it will also include the dummy argument types,
- * at some given position.
- * <p>
- * The {@code pos} argument may range between zero and <i>N</i>,
- * where <i>N</i> is the arity of the target.
- * If {@code pos} is zero, the dummy arguments will precede
- * the target's real arguments; if {@code pos} is <i>N</i>
- * they will come after.
- * <p>
- * <b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodHandle d0 = dropArguments(cat, 0, String.class);
-assertEquals("yz", (String) d0.invokeExact("x", "y", "z"));
-MethodHandle d1 = dropArguments(cat, 1, String.class);
-assertEquals("xz", (String) d1.invokeExact("x", "y", "z"));
-MethodHandle d2 = dropArguments(cat, 2, String.class);
-assertEquals("xy", (String) d2.invokeExact("x", "y", "z"));
-MethodHandle d12 = dropArguments(cat, 1, int.class, boolean.class);
-assertEquals("xz", (String) d12.invokeExact("x", 12, true, "z"));
- * }</pre></blockquote>
- * <p>
- * This method is also equivalent to the following code:
- * <blockquote><pre>
- * {@link #dropArguments(MethodHandle,int,List) dropArguments}{@code (target, pos, Arrays.asList(valueTypes))}
- * </pre></blockquote>
- * @param target the method handle to invoke after the arguments are dropped
- * @param valueTypes the type(s) of the argument(s) to drop
- * @param pos position of first argument to drop (zero for the leftmost)
- * @return a method handle which drops arguments of the given types,
- * before calling the original method handle
- * @throws NullPointerException if the target is null,
- * or if the {@code valueTypes} array or any of its elements is null
- * @throws IllegalArgumentException if any element of {@code valueTypes} is {@code void.class},
- * or if {@code pos} is negative or greater than the arity of the target,
- * or if the new method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
public static
- MethodHandle dropArguments(MethodHandle target, int pos, Class<?>... valueTypes) {
- return dropArguments(target, pos, Arrays.asList(valueTypes));
- }
+ MethodHandle dropArguments(MethodHandle target, int pos, Class<?>... valueTypes) { return null; }
- /**
- * Adapts a target method handle by pre-processing
- * one or more of its arguments, each with its own unary filter function,
- * and then calling the target with each pre-processed argument
- * replaced by the result of its corresponding filter function.
- * <p>
- * The pre-processing is performed by one or more method handles,
- * specified in the elements of the {@code filters} array.
- * The first element of the filter array corresponds to the {@code pos}
- * argument of the target, and so on in sequence.
- * <p>
- * Null arguments in the array are treated as identity functions,
- * and the corresponding arguments left unchanged.
- * (If there are no non-null elements in the array, the original target is returned.)
- * Each filter is applied to the corresponding argument of the adapter.
- * <p>
- * If a filter {@code F} applies to the {@code N}th argument of
- * the target, then {@code F} must be a method handle which
- * takes exactly one argument. The type of {@code F}'s sole argument
- * replaces the corresponding argument type of the target
- * in the resulting adapted method handle.
- * The return type of {@code F} must be identical to the corresponding
- * parameter type of the target.
- * <p>
- * It is an error if there are elements of {@code filters}
- * (null or not)
- * which do not correspond to argument positions in the target.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-MethodHandle upcase = lookup().findVirtual(String.class,
- "toUpperCase", methodType(String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodHandle f0 = filterArguments(cat, 0, upcase);
-assertEquals("Xy", (String) f0.invokeExact("x", "y")); // Xy
-MethodHandle f1 = filterArguments(cat, 1, upcase);
-assertEquals("xY", (String) f1.invokeExact("x", "y")); // xY
-MethodHandle f2 = filterArguments(cat, 0, upcase, upcase);
-assertEquals("XY", (String) f2.invokeExact("x", "y")); // XY
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * V target(P... p, A[i]... a[i], B... b);
- * A[i] filter[i](V[i]);
- * T adapter(P... p, V[i]... v[i], B... b) {
- * return target(p..., f[i](v[i])..., b...);
- * }
- * }</pre></blockquote>
- *
- * @param target the method handle to invoke after arguments are filtered
- * @param pos the position of the first argument to filter
- * @param filters method handles to call initially on filtered arguments
- * @return method handle which incorporates the specified argument filtering logic
- * @throws NullPointerException if the target is null
- * or if the {@code filters} array is null
- * @throws IllegalArgumentException if a non-null element of {@code filters}
- * does not match a corresponding argument type of target as described above,
- * or if the {@code pos+filters.length} is greater than {@code target.type().parameterCount()},
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
public static
- MethodHandle filterArguments(MethodHandle target, int pos, MethodHandle... filters) {
- filterArgumentsCheckArity(target, pos, filters);
-
- for (int i = 0; i < filters.length; ++i) {
- filterArgumentChecks(target, i + pos, filters[i]);
- }
-
- return new Transformers.FilterArguments(target, pos, filters);
- }
-
- private static void filterArgumentsCheckArity(MethodHandle target, int pos, MethodHandle[] filters) {
- MethodType targetType = target.type();
- int maxPos = targetType.parameterCount();
- if (pos + filters.length > maxPos)
- throw newIllegalArgumentException("too many filters");
- }
-
- private static void filterArgumentChecks(MethodHandle target, int pos, MethodHandle filter) throws RuntimeException {
- MethodType targetType = target.type();
- MethodType filterType = filter.type();
- if (filterType.parameterCount() != 1
- || filterType.returnType() != targetType.parameterType(pos))
- throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
- }
-
- /**
- * Adapts a target method handle by pre-processing
- * a sub-sequence of its arguments with a filter (another method handle).
- * The pre-processed arguments are replaced by the result (if any) of the
- * filter function.
- * The target is then called on the modified (usually shortened) argument list.
- * <p>
- * If the filter returns a value, the target must accept that value as
- * its argument in position {@code pos}, preceded and/or followed by
- * any arguments not passed to the filter.
- * If the filter returns void, the target must accept all arguments
- * not passed to the filter.
- * No arguments are reordered, and a result returned from the filter
- * replaces (in order) the whole subsequence of arguments originally
- * passed to the adapter.
- * <p>
- * The argument types (if any) of the filter
- * replace zero or one argument types of the target, at position {@code pos},
- * in the resulting adapted method handle.
- * The return type of the filter (if any) must be identical to the
- * argument type of the target at position {@code pos}, and that target argument
- * is supplied by the return value of the filter.
- * <p>
- * In all cases, {@code pos} must be greater than or equal to zero, and
- * {@code pos} must also be less than or equal to the target's arity.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle deepToString = publicLookup()
- .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-
-MethodHandle ts1 = deepToString.asCollector(String[].class, 1);
-assertEquals("[strange]", (String) ts1.invokeExact("strange"));
+ MethodHandle filterArguments(MethodHandle target, int pos, MethodHandle... filters) { return null; }
-MethodHandle ts2 = deepToString.asCollector(String[].class, 2);
-assertEquals("[up, down]", (String) ts2.invokeExact("up", "down"));
-
-MethodHandle ts3 = deepToString.asCollector(String[].class, 3);
-MethodHandle ts3_ts2 = collectArguments(ts3, 1, ts2);
-assertEquals("[top, [up, down], strange]",
- (String) ts3_ts2.invokeExact("top", "up", "down", "strange"));
-
-MethodHandle ts3_ts2_ts1 = collectArguments(ts3_ts2, 3, ts1);
-assertEquals("[top, [up, down], [strange]]",
- (String) ts3_ts2_ts1.invokeExact("top", "up", "down", "strange"));
-
-MethodHandle ts3_ts2_ts3 = collectArguments(ts3_ts2, 1, ts3);
-assertEquals("[top, [[up, down, strange], charm], bottom]",
- (String) ts3_ts2_ts3.invokeExact("top", "up", "down", "strange", "charm", "bottom"));
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * T target(A...,V,C...);
- * V filter(B...);
- * T adapter(A... a,B... b,C... c) {
- * V v = filter(b...);
- * return target(a...,v,c...);
- * }
- * // and if the filter has no arguments:
- * T target2(A...,V,C...);
- * V filter2();
- * T adapter2(A... a,C... c) {
- * V v = filter2();
- * return target2(a...,v,c...);
- * }
- * // and if the filter has a void return:
- * T target3(A...,C...);
- * void filter3(B...);
- * void adapter3(A... a,B... b,C... c) {
- * filter3(b...);
- * return target3(a...,c...);
- * }
- * }</pre></blockquote>
- * <p>
- * A collection adapter {@code collectArguments(mh, 0, coll)} is equivalent to
- * one which first "folds" the affected arguments, and then drops them, in separate
- * steps as follows:
- * <blockquote><pre>{@code
- * mh = MethodHandles.dropArguments(mh, 1, coll.type().parameterList()); //step 2
- * mh = MethodHandles.foldArguments(mh, coll); //step 1
- * }</pre></blockquote>
- * If the target method handle consumes no arguments besides than the result
- * (if any) of the filter {@code coll}, then {@code collectArguments(mh, 0, coll)}
- * is equivalent to {@code filterReturnValue(coll, mh)}.
- * If the filter method handle {@code coll} consumes one argument and produces
- * a non-void result, then {@code collectArguments(mh, N, coll)}
- * is equivalent to {@code filterArguments(mh, N, coll)}.
- * Other equivalences are possible but would require argument permutation.
- *
- * @param target the method handle to invoke after filtering the subsequence of arguments
- * @param pos the position of the first adapter argument to pass to the filter,
- * and/or the target argument which receives the result of the filter
- * @param filter method handle to call on the subsequence of arguments
- * @return method handle which incorporates the specified argument subsequence filtering logic
- * @throws NullPointerException if either argument is null
- * @throws IllegalArgumentException if the return type of {@code filter}
- * is non-void and is not the same as the {@code pos} argument of the target,
- * or if {@code pos} is not between 0 and the target's arity, inclusive,
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- * @see MethodHandles#foldArguments
- * @see MethodHandles#filterArguments
- * @see MethodHandles#filterReturnValue
- */
public static
- MethodHandle collectArguments(MethodHandle target, int pos, MethodHandle filter) {
- MethodType newType = collectArgumentsChecks(target, pos, filter);
- return new Transformers.CollectArguments(target, filter, pos, newType);
- }
-
- private static MethodType collectArgumentsChecks(MethodHandle target, int pos, MethodHandle filter) throws RuntimeException {
- MethodType targetType = target.type();
- MethodType filterType = filter.type();
- Class<?> rtype = filterType.returnType();
- List<Class<?>> filterArgs = filterType.parameterList();
- if (rtype == void.class) {
- return targetType.insertParameterTypes(pos, filterArgs);
- }
- if (rtype != targetType.parameterType(pos)) {
- throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
- }
- return targetType.dropParameterTypes(pos, pos+1).insertParameterTypes(pos, filterArgs);
- }
+ MethodHandle collectArguments(MethodHandle target, int pos, MethodHandle filter) { return null; }
- /**
- * Adapts a target method handle by post-processing
- * its return value (if any) with a filter (another method handle).
- * The result of the filter is returned from the adapter.
- * <p>
- * If the target returns a value, the filter must accept that value as
- * its only argument.
- * If the target returns void, the filter must accept no arguments.
- * <p>
- * The return type of the filter
- * replaces the return type of the target
- * in the resulting adapted method handle.
- * The argument type of the filter (if any) must be identical to the
- * return type of the target.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-MethodHandle length = lookup().findVirtual(String.class,
- "length", methodType(int.class));
-System.out.println((String) cat.invokeExact("x", "y")); // xy
-MethodHandle f0 = filterReturnValue(cat, length);
-System.out.println((int) f0.invokeExact("x", "y")); // 2
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * V target(A...);
- * T filter(V);
- * T adapter(A... a) {
- * V v = target(a...);
- * return filter(v);
- * }
- * // and if the target has a void return:
- * void target2(A...);
- * T filter2();
- * T adapter2(A... a) {
- * target2(a...);
- * return filter2();
- * }
- * // and if the filter has a void return:
- * V target3(A...);
- * void filter3(V);
- * void adapter3(A... a) {
- * V v = target3(a...);
- * filter3(v);
- * }
- * }</pre></blockquote>
- * @param target the method handle to invoke before filtering the return value
- * @param filter method handle to call on the return value
- * @return method handle which incorporates the specified return value filtering logic
- * @throws NullPointerException if either argument is null
- * @throws IllegalArgumentException if the argument list of {@code filter}
- * does not match the return type of target as described above
- */
public static
- MethodHandle filterReturnValue(MethodHandle target, MethodHandle filter) {
- MethodType targetType = target.type();
- MethodType filterType = filter.type();
- filterReturnValueChecks(targetType, filterType);
+ MethodHandle filterReturnValue(MethodHandle target, MethodHandle filter) { return null; }
- return new Transformers.FilterReturnValue(target, filter);
- }
-
- private static void filterReturnValueChecks(MethodType targetType, MethodType filterType) throws RuntimeException {
- Class<?> rtype = targetType.returnType();
- int filterValues = filterType.parameterCount();
- if (filterValues == 0
- ? (rtype != void.class)
- : (rtype != filterType.parameterType(0) || filterValues != 1))
- throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
- }
-
- /**
- * Adapts a target method handle by pre-processing
- * some of its arguments, and then calling the target with
- * the result of the pre-processing, inserted into the original
- * sequence of arguments.
- * <p>
- * The pre-processing is performed by {@code combiner}, a second method handle.
- * Of the arguments passed to the adapter, the first {@code N} arguments
- * are copied to the combiner, which is then called.
- * (Here, {@code N} is defined as the parameter count of the combiner.)
- * After this, control passes to the target, with any result
- * from the combiner inserted before the original {@code N} incoming
- * arguments.
- * <p>
- * If the combiner returns a value, the first parameter type of the target
- * must be identical with the return type of the combiner, and the next
- * {@code N} parameter types of the target must exactly match the parameters
- * of the combiner.
- * <p>
- * If the combiner has a void return, no result will be inserted,
- * and the first {@code N} parameter types of the target
- * must exactly match the parameters of the combiner.
- * <p>
- * The resulting adapter is the same type as the target, except that the
- * first parameter type is dropped,
- * if it corresponds to the result of the combiner.
- * <p>
- * (Note that {@link #dropArguments(MethodHandle,int,List) dropArguments} can be used to remove any arguments
- * that either the combiner or the target does not wish to receive.
- * If some of the incoming arguments are destined only for the combiner,
- * consider using {@link MethodHandle#asCollector asCollector} instead, since those
- * arguments will not need to be live on the stack on entry to the
- * target.)
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle trace = publicLookup().findVirtual(java.io.PrintStream.class,
- "println", methodType(void.class, String.class))
- .bindTo(System.out);
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-assertEquals("boojum", (String) cat.invokeExact("boo", "jum"));
-MethodHandle catTrace = foldArguments(cat, trace);
-// also prints "boo":
-assertEquals("boojum", (String) catTrace.invokeExact("boo", "jum"));
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * // there are N arguments in A...
- * T target(V, A[N]..., B...);
- * V combiner(A...);
- * T adapter(A... a, B... b) {
- * V v = combiner(a...);
- * return target(v, a..., b...);
- * }
- * // and if the combiner has a void return:
- * T target2(A[N]..., B...);
- * void combiner2(A...);
- * T adapter2(A... a, B... b) {
- * combiner2(a...);
- * return target2(a..., b...);
- * }
- * }</pre></blockquote>
- * @param target the method handle to invoke after arguments are combined
- * @param combiner method handle to call initially on the incoming arguments
- * @return method handle which incorporates the specified argument folding logic
- * @throws NullPointerException if either argument is null
- * @throws IllegalArgumentException if {@code combiner}'s return type
- * is non-void and not the same as the first argument type of
- * the target, or if the initial {@code N} argument types
- * of the target
- * (skipping one matching the {@code combiner}'s return type)
- * are not identical with the argument types of {@code combiner}
- */
public static
- MethodHandle foldArguments(MethodHandle target, MethodHandle combiner) {
- int foldPos = 0;
- MethodType targetType = target.type();
- MethodType combinerType = combiner.type();
- Class<?> rtype = foldArgumentChecks(foldPos, targetType, combinerType);
-
- return new Transformers.FoldArguments(target, combiner);
- }
-
- private static Class<?> foldArgumentChecks(int foldPos, MethodType targetType, MethodType combinerType) {
- int foldArgs = combinerType.parameterCount();
- Class<?> rtype = combinerType.returnType();
- int foldVals = rtype == void.class ? 0 : 1;
- int afterInsertPos = foldPos + foldVals;
- boolean ok = (targetType.parameterCount() >= afterInsertPos + foldArgs);
- if (ok && !(combinerType.parameterList()
- .equals(targetType.parameterList().subList(afterInsertPos,
- afterInsertPos + foldArgs))))
- ok = false;
- if (ok && foldVals != 0 && combinerType.returnType() != targetType.parameterType(0))
- ok = false;
- if (!ok)
- throw misMatchedTypes("target and combiner types", targetType, combinerType);
- return rtype;
- }
+ MethodHandle foldArguments(MethodHandle target, MethodHandle combiner) { return null; }
- /**
- * Makes a method handle which adapts a target method handle,
- * by guarding it with a test, a boolean-valued method handle.
- * If the guard fails, a fallback handle is called instead.
- * All three method handles must have the same corresponding
- * argument and return types, except that the return type
- * of the test must be boolean, and the test is allowed
- * to have fewer arguments than the other two method handles.
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * boolean test(A...);
- * T target(A...,B...);
- * T fallback(A...,B...);
- * T adapter(A... a,B... b) {
- * if (test(a...))
- * return target(a..., b...);
- * else
- * return fallback(a..., b...);
- * }
- * }</pre></blockquote>
- * Note that the test arguments ({@code a...} in the pseudocode) cannot
- * be modified by execution of the test, and so are passed unchanged
- * from the caller to the target or fallback as appropriate.
- * @param test method handle used for test, must return boolean
- * @param target method handle to call if test passes
- * @param fallback method handle to call if test fails
- * @return method handle which incorporates the specified if/then/else logic
- * @throws NullPointerException if any argument is null
- * @throws IllegalArgumentException if {@code test} does not return boolean,
- * or if all three method types do not match (with the return
- * type of {@code test} changed to match that of the target).
- */
public static
MethodHandle guardWithTest(MethodHandle test,
MethodHandle target,
- MethodHandle fallback) {
- MethodType gtype = test.type();
- MethodType ttype = target.type();
- MethodType ftype = fallback.type();
- if (!ttype.equals(ftype))
- throw misMatchedTypes("target and fallback types", ttype, ftype);
- if (gtype.returnType() != boolean.class)
- throw newIllegalArgumentException("guard type is not a predicate "+gtype);
- List<Class<?>> targs = ttype.parameterList();
- List<Class<?>> gargs = gtype.parameterList();
- if (!targs.equals(gargs)) {
- int gpc = gargs.size(), tpc = targs.size();
- if (gpc >= tpc || !targs.subList(0, gpc).equals(gargs))
- throw misMatchedTypes("target and test types", ttype, gtype);
- test = dropArguments(test, gpc, targs.subList(gpc, tpc));
- gtype = test.type();
- }
+ MethodHandle fallback) { return null; }
- return new Transformers.GuardWithTest(test, target, fallback);
- }
-
- static RuntimeException misMatchedTypes(String what, MethodType t1, MethodType t2) {
- return newIllegalArgumentException(what + " must match: " + t1 + " != " + t2);
- }
-
- /**
- * Makes a method handle which adapts a target method handle,
- * by running it inside an exception handler.
- * If the target returns normally, the adapter returns that value.
- * If an exception matching the specified type is thrown, the fallback
- * handle is called instead on the exception, plus the original arguments.
- * <p>
- * The target and handler must have the same corresponding
- * argument and return types, except that handler may omit trailing arguments
- * (similarly to the predicate in {@link #guardWithTest guardWithTest}).
- * Also, the handler must have an extra leading parameter of {@code exType} or a supertype.
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * T target(A..., B...);
- * T handler(ExType, A...);
- * T adapter(A... a, B... b) {
- * try {
- * return target(a..., b...);
- * } catch (ExType ex) {
- * return handler(ex, a...);
- * }
- * }
- * }</pre></blockquote>
- * Note that the saved arguments ({@code a...} in the pseudocode) cannot
- * be modified by execution of the target, and so are passed unchanged
- * from the caller to the handler, if the handler is invoked.
- * <p>
- * The target and handler must return the same type, even if the handler
- * always throws. (This might happen, for instance, because the handler
- * is simulating a {@code finally} clause).
- * To create such a throwing handler, compose the handler creation logic
- * with {@link #throwException throwException},
- * in order to create a method handle of the correct return type.
- * @param target method handle to call
- * @param exType the type of exception which the handler will catch
- * @param handler method handle to call if a matching exception is thrown
- * @return method handle which incorporates the specified try/catch logic
- * @throws NullPointerException if any argument is null
- * @throws IllegalArgumentException if {@code handler} does not accept
- * the given exception type, or if the method handle types do
- * not match in their return types and their
- * corresponding parameters
- */
public static
MethodHandle catchException(MethodHandle target,
Class<? extends Throwable> exType,
- MethodHandle handler) {
- MethodType ttype = target.type();
- MethodType htype = handler.type();
- if (htype.parameterCount() < 1 ||
- !htype.parameterType(0).isAssignableFrom(exType))
- throw newIllegalArgumentException("handler does not accept exception type "+exType);
- if (htype.returnType() != ttype.returnType())
- throw misMatchedTypes("target and handler return types", ttype, htype);
- List<Class<?>> targs = ttype.parameterList();
- List<Class<?>> hargs = htype.parameterList();
- hargs = hargs.subList(1, hargs.size()); // omit leading parameter from handler
- if (!targs.equals(hargs)) {
- int hpc = hargs.size(), tpc = targs.size();
- if (hpc >= tpc || !targs.subList(0, hpc).equals(hargs))
- throw misMatchedTypes("target and handler types", ttype, htype);
- }
-
- return new Transformers.CatchException(target, handler, exType);
- }
+ MethodHandle handler) { return null; }
- /**
- * Produces a method handle which will throw exceptions of the given {@code exType}.
- * The method handle will accept a single argument of {@code exType},
- * and immediately throw it as an exception.
- * The method type will nominally specify a return of {@code returnType}.
- * The return type may be anything convenient: It doesn't matter to the
- * method handle's behavior, since it will never return normally.
- * @param returnType the return type of the desired method handle
- * @param exType the parameter type of the desired method handle
- * @return method handle which can throw the given exceptions
- * @throws NullPointerException if either argument is null
- */
public static
- MethodHandle throwException(Class<?> returnType, Class<? extends Throwable> exType) {
- if (!Throwable.class.isAssignableFrom(exType))
- throw new ClassCastException(exType.getName());
-
- return new Transformers.AlwaysThrow(returnType, exType);
- }
+ MethodHandle throwException(Class<?> returnType, Class<? extends Throwable> exType) { return null; }
}
diff --git a/java/lang/invoke/MethodType.java b/java/lang/invoke/MethodType.java
index bfa7ccd5..4cb5c226 100644
--- a/java/lang/invoke/MethodType.java
+++ b/java/lang/invoke/MethodType.java
@@ -25,1227 +25,78 @@
package java.lang.invoke;
-import sun.invoke.util.Wrapper;
-import java.lang.ref.WeakReference;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentHashMap;
-import sun.invoke.util.BytecodeDescriptor;
-import static java.lang.invoke.MethodHandleStatics.*;
-/**
- * A method type represents the arguments and return type accepted and
- * returned by a method handle, or the arguments and return type passed
- * and expected by a method handle caller. Method types must be properly
- * matched between a method handle and all its callers,
- * and the JVM's operations enforce this matching at, specifically
- * during calls to {@link MethodHandle#invokeExact MethodHandle.invokeExact}
- * and {@link MethodHandle#invoke MethodHandle.invoke}, and during execution
- * of {@code invokedynamic} instructions.
- * <p>
- * The structure is a return type accompanied by any number of parameter types.
- * The types (primitive, {@code void}, and reference) are represented by {@link Class} objects.
- * (For ease of exposition, we treat {@code void} as if it were a type.
- * In fact, it denotes the absence of a return type.)
- * <p>
- * All instances of {@code MethodType} are immutable.
- * Two instances are completely interchangeable if they compare equal.
- * Equality depends on pairwise correspondence of the return and parameter types and on nothing else.
- * <p>
- * This type can be created only by factory methods.
- * All factory methods may cache values, though caching is not guaranteed.
- * Some factory methods are static, while others are virtual methods which
- * modify precursor method types, e.g., by changing a selected parameter.
- * <p>
- * Factory methods which operate on groups of parameter types
- * are systematically presented in two versions, so that both Java arrays and
- * Java lists can be used to work with groups of parameter types.
- * The query methods {@code parameterArray} and {@code parameterList}
- * also provide a choice between arrays and lists.
- * <p>
- * {@code MethodType} objects are sometimes derived from bytecode instructions
- * such as {@code invokedynamic}, specifically from the type descriptor strings associated
- * with the instructions in a class file's constant pool.
- * <p>
- * Like classes and strings, method types can also be represented directly
- * in a class file's constant pool as constants.
- * A method type may be loaded by an {@code ldc} instruction which refers
- * to a suitable {@code CONSTANT_MethodType} constant pool entry.
- * The entry refers to a {@code CONSTANT_Utf8} spelling for the descriptor string.
- * (For full details on method type constants,
- * see sections 4.4.8 and 5.4.3.5 of the Java Virtual Machine Specification.)
- * <p>
- * When the JVM materializes a {@code MethodType} from a descriptor string,
- * all classes named in the descriptor must be accessible, and will be loaded.
- * (But the classes need not be initialized, as is the case with a {@code CONSTANT_Class}.)
- * This loading may occur at any time before the {@code MethodType} object is first derived.
- * @author John Rose, JSR 292 EG
- */
public final
class MethodType implements java.io.Serializable {
- private static final long serialVersionUID = 292L; // {rtype, {ptype...}}
-
- // The rtype and ptypes fields define the structural identity of the method type:
- private final Class<?> rtype;
- private final Class<?>[] ptypes;
-
- // The remaining fields are caches of various sorts:
- private @Stable MethodTypeForm form; // erased form, plus cached data about primitives
- private @Stable MethodType wrapAlt; // alternative wrapped/unwrapped version
- // Android-changed: Remove adapter cache. We're not dynamically generating any
- // adapters at this point.
- // private @Stable Invokers invokers; // cache of handy higher-order adapters
- private @Stable String methodDescriptor; // cache for toMethodDescriptorString
-
- /**
- * Check the given parameters for validity and store them into the final fields.
- */
- private MethodType(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
- checkRtype(rtype);
- checkPtypes(ptypes);
- this.rtype = rtype;
- // defensively copy the array passed in by the user
- this.ptypes = trusted ? ptypes : Arrays.copyOf(ptypes, ptypes.length);
- }
-
- /**
- * Construct a temporary unchecked instance of MethodType for use only as a key to the intern table.
- * Does not check the given parameters for validity, and must be discarded after it is used as a searching key.
- * The parameters are reversed for this constructor, so that is is not accidentally used.
- */
- private MethodType(Class<?>[] ptypes, Class<?> rtype) {
- this.rtype = rtype;
- this.ptypes = ptypes;
- }
-
- /*trusted*/ MethodTypeForm form() { return form; }
- /*trusted*/ /** @hide */ public Class<?> rtype() { return rtype; }
- /*trusted*/ /** @hide */ public Class<?>[] ptypes() { return ptypes; }
- // Android-changed: Removed method setForm. It's unused in the JDK and there's no
- // good reason to allow the form to be set externally.
- //
- // void setForm(MethodTypeForm f) { form = f; }
-
- /** This number, mandated by the JVM spec as 255,
- * is the maximum number of <em>slots</em>
- * that any Java method can receive in its argument list.
- * It limits both JVM signatures and method type objects.
- * The longest possible invocation will look like
- * {@code staticMethod(arg1, arg2, ..., arg255)} or
- * {@code x.virtualMethod(arg1, arg2, ..., arg254)}.
- */
- /*non-public*/ static final int MAX_JVM_ARITY = 255; // this is mandated by the JVM spec.
-
- /** This number is the maximum arity of a method handle, 254.
- * It is derived from the absolute JVM-imposed arity by subtracting one,
- * which is the slot occupied by the method handle itself at the
- * beginning of the argument list used to invoke the method handle.
- * The longest possible invocation will look like
- * {@code mh.invoke(arg1, arg2, ..., arg254)}.
- */
- // Issue: Should we allow MH.invokeWithArguments to go to the full 255?
- /*non-public*/ static final int MAX_MH_ARITY = MAX_JVM_ARITY-1; // deduct one for mh receiver
-
- /** This number is the maximum arity of a method handle invoker, 253.
- * It is derived from the absolute JVM-imposed arity by subtracting two,
- * which are the slots occupied by invoke method handle, and the
- * target method handle, which are both at the beginning of the argument
- * list used to invoke the target method handle.
- * The longest possible invocation will look like
- * {@code invokermh.invoke(targetmh, arg1, arg2, ..., arg253)}.
- */
- /*non-public*/ static final int MAX_MH_INVOKER_ARITY = MAX_MH_ARITY-1; // deduct one more for invoker
-
- private static void checkRtype(Class<?> rtype) {
- Objects.requireNonNull(rtype);
- }
- private static void checkPtype(Class<?> ptype) {
- Objects.requireNonNull(ptype);
- if (ptype == void.class)
- throw newIllegalArgumentException("parameter type cannot be void");
- }
- /** Return number of extra slots (count of long/double args). */
- private static int checkPtypes(Class<?>[] ptypes) {
- int slots = 0;
- for (Class<?> ptype : ptypes) {
- checkPtype(ptype);
- if (ptype == double.class || ptype == long.class) {
- slots++;
- }
- }
- checkSlotCount(ptypes.length + slots);
- return slots;
- }
- static void checkSlotCount(int count) {
- assert((MAX_JVM_ARITY & (MAX_JVM_ARITY+1)) == 0);
- // MAX_JVM_ARITY must be power of 2 minus 1 for following code trick to work:
- if ((count & MAX_JVM_ARITY) != count)
- throw newIllegalArgumentException("bad parameter count "+count);
- }
- private static IndexOutOfBoundsException newIndexOutOfBoundsException(Object num) {
- if (num instanceof Integer) num = "bad index: "+num;
- return new IndexOutOfBoundsException(num.toString());
- }
-
- static final ConcurrentWeakInternSet<MethodType> internTable = new ConcurrentWeakInternSet<>();
-
- static final Class<?>[] NO_PTYPES = {};
-
- /**
- * Finds or creates an instance of the given method type.
- * @param rtype the return type
- * @param ptypes the parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptypes} or any element of {@code ptypes} is null
- * @throws IllegalArgumentException if any element of {@code ptypes} is {@code void.class}
- */
public static
MethodType methodType(Class<?> rtype, Class<?>[] ptypes) {
- return makeImpl(rtype, ptypes, false);
+ return null;
}
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param rtype the return type
- * @param ptypes the parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptypes} or any element of {@code ptypes} is null
- * @throws IllegalArgumentException if any element of {@code ptypes} is {@code void.class}
- */
public static
MethodType methodType(Class<?> rtype, List<Class<?>> ptypes) {
- boolean notrust = false; // random List impl. could return evil ptypes array
- return makeImpl(rtype, listToArray(ptypes), notrust);
+ return null;
}
- private static Class<?>[] listToArray(List<Class<?>> ptypes) {
- // sanity check the size before the toArray call, since size might be huge
- checkSlotCount(ptypes.size());
- return ptypes.toArray(NO_PTYPES);
- }
-
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The leading parameter type is prepended to the remaining array.
- * @param rtype the return type
- * @param ptype0 the first parameter type
- * @param ptypes the remaining parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptype0} or {@code ptypes} or any element of {@code ptypes} is null
- * @throws IllegalArgumentException if {@code ptype0} or {@code ptypes} or any element of {@code ptypes} is {@code void.class}
- */
public static
- MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) {
- Class<?>[] ptypes1 = new Class<?>[1+ptypes.length];
- ptypes1[0] = ptype0;
- System.arraycopy(ptypes, 0, ptypes1, 1, ptypes.length);
- return makeImpl(rtype, ptypes1, true);
- }
+ MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) { return null; }
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The resulting method has no parameter types.
- * @param rtype the return type
- * @return a method type with the given return value
- * @throws NullPointerException if {@code rtype} is null
- */
public static
- MethodType methodType(Class<?> rtype) {
- return makeImpl(rtype, NO_PTYPES, true);
- }
+ MethodType methodType(Class<?> rtype) { return null; }
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The resulting method has the single given parameter type.
- * @param rtype the return type
- * @param ptype0 the parameter type
- * @return a method type with the given return value and parameter type
- * @throws NullPointerException if {@code rtype} or {@code ptype0} is null
- * @throws IllegalArgumentException if {@code ptype0} is {@code void.class}
- */
public static
- MethodType methodType(Class<?> rtype, Class<?> ptype0) {
- return makeImpl(rtype, new Class<?>[]{ ptype0 }, true);
- }
+ MethodType methodType(Class<?> rtype, Class<?> ptype0) { return null; }
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The resulting method has the same parameter types as {@code ptypes},
- * and the specified return type.
- * @param rtype the return type
- * @param ptypes the method type which supplies the parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptypes} is null
- */
public static
- MethodType methodType(Class<?> rtype, MethodType ptypes) {
- return makeImpl(rtype, ptypes.ptypes, true);
- }
-
- /**
- * Sole factory method to find or create an interned method type.
- * @param rtype desired return type
- * @param ptypes desired parameter types
- * @param trusted whether the ptypes can be used without cloning
- * @return the unique method type of the desired structure
- */
- /*trusted*/ static
- MethodType makeImpl(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
- MethodType mt = internTable.get(new MethodType(ptypes, rtype));
- if (mt != null)
- return mt;
- if (ptypes.length == 0) {
- ptypes = NO_PTYPES; trusted = true;
- }
- mt = new MethodType(rtype, ptypes, trusted);
- // promote the object to the Real Thing, and reprobe
- mt.form = MethodTypeForm.findForm(mt);
- return internTable.add(mt);
- }
- private static final MethodType[] objectOnlyTypes = new MethodType[20];
+ MethodType methodType(Class<?> rtype, MethodType ptypes) { return null; }
- /**
- * Finds or creates a method type whose components are {@code Object} with an optional trailing {@code Object[]} array.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All parameters and the return type will be {@code Object},
- * except the final array parameter if any, which will be {@code Object[]}.
- * @param objectArgCount number of parameters (excluding the final array parameter if any)
- * @param finalArray whether there will be a trailing array parameter, of type {@code Object[]}
- * @return a generally applicable method type, for all calls of the given fixed argument count and a collected array of further arguments
- * @throws IllegalArgumentException if {@code objectArgCount} is negative or greater than 255 (or 254, if {@code finalArray} is true)
- * @see #genericMethodType(int)
- */
public static
- MethodType genericMethodType(int objectArgCount, boolean finalArray) {
- MethodType mt;
- checkSlotCount(objectArgCount);
- int ivarargs = (!finalArray ? 0 : 1);
- int ootIndex = objectArgCount*2 + ivarargs;
- if (ootIndex < objectOnlyTypes.length) {
- mt = objectOnlyTypes[ootIndex];
- if (mt != null) return mt;
- }
- Class<?>[] ptypes = new Class<?>[objectArgCount + ivarargs];
- Arrays.fill(ptypes, Object.class);
- if (ivarargs != 0) ptypes[objectArgCount] = Object[].class;
- mt = makeImpl(Object.class, ptypes, true);
- if (ootIndex < objectOnlyTypes.length) {
- objectOnlyTypes[ootIndex] = mt; // cache it here also!
- }
- return mt;
- }
+ MethodType genericMethodType(int objectArgCount, boolean finalArray) { return null; }
- /**
- * Finds or creates a method type whose components are all {@code Object}.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All parameters and the return type will be Object.
- * @param objectArgCount number of parameters
- * @return a generally applicable method type, for all calls of the given argument count
- * @throws IllegalArgumentException if {@code objectArgCount} is negative or greater than 255
- * @see #genericMethodType(int, boolean)
- */
public static
- MethodType genericMethodType(int objectArgCount) {
- return genericMethodType(objectArgCount, false);
- }
-
- /**
- * Finds or creates a method type with a single different parameter type.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param num the index (zero-based) of the parameter type to change
- * @param nptype a new parameter type to replace the old one with
- * @return the same type, except with the selected parameter changed
- * @throws IndexOutOfBoundsException if {@code num} is not a valid index into {@code parameterArray()}
- * @throws IllegalArgumentException if {@code nptype} is {@code void.class}
- * @throws NullPointerException if {@code nptype} is null
- */
- public MethodType changeParameterType(int num, Class<?> nptype) {
- if (parameterType(num) == nptype) return this;
- checkPtype(nptype);
- Class<?>[] nptypes = ptypes.clone();
- nptypes[num] = nptype;
- return makeImpl(rtype, nptypes, true);
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param num the position (zero-based) of the inserted parameter type(s)
- * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
- * @return the same type, except with the selected parameter(s) inserted
- * @throws IndexOutOfBoundsException if {@code num} is negative or greater than {@code parameterCount()}
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType insertParameterTypes(int num, Class<?>... ptypesToInsert) {
- int len = ptypes.length;
- if (num < 0 || num > len)
- throw newIndexOutOfBoundsException(num);
- int ins = checkPtypes(ptypesToInsert);
- checkSlotCount(parameterSlotCount() + ptypesToInsert.length + ins);
- int ilen = ptypesToInsert.length;
- if (ilen == 0) return this;
- Class<?>[] nptypes = Arrays.copyOfRange(ptypes, 0, len+ilen);
- System.arraycopy(nptypes, num, nptypes, num+ilen, len-num);
- System.arraycopy(ptypesToInsert, 0, nptypes, num, ilen);
- return makeImpl(rtype, nptypes, true);
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param ptypesToInsert zero or more new parameter types to insert after the end of the parameter list
- * @return the same type, except with the selected parameter(s) appended
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType appendParameterTypes(Class<?>... ptypesToInsert) {
- return insertParameterTypes(parameterCount(), ptypesToInsert);
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param num the position (zero-based) of the inserted parameter type(s)
- * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
- * @return the same type, except with the selected parameter(s) inserted
- * @throws IndexOutOfBoundsException if {@code num} is negative or greater than {@code parameterCount()}
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType insertParameterTypes(int num, List<Class<?>> ptypesToInsert) {
- return insertParameterTypes(num, listToArray(ptypesToInsert));
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param ptypesToInsert zero or more new parameter types to insert after the end of the parameter list
- * @return the same type, except with the selected parameter(s) appended
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType appendParameterTypes(List<Class<?>> ptypesToInsert) {
- return insertParameterTypes(parameterCount(), ptypesToInsert);
- }
-
- /**
- * Finds or creates a method type with modified parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param start the position (zero-based) of the first replaced parameter type(s)
- * @param end the position (zero-based) after the last replaced parameter type(s)
- * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
- * @return the same type, except with the selected parameter(s) replaced
- * @throws IndexOutOfBoundsException if {@code start} is negative or greater than {@code parameterCount()}
- * or if {@code end} is negative or greater than {@code parameterCount()}
- * or if {@code start} is greater than {@code end}
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- /*non-public*/ MethodType replaceParameterTypes(int start, int end, Class<?>... ptypesToInsert) {
- if (start == end)
- return insertParameterTypes(start, ptypesToInsert);
- int len = ptypes.length;
- if (!(0 <= start && start <= end && end <= len))
- throw newIndexOutOfBoundsException("start="+start+" end="+end);
- int ilen = ptypesToInsert.length;
- if (ilen == 0)
- return dropParameterTypes(start, end);
- return dropParameterTypes(start, end).insertParameterTypes(start, ptypesToInsert);
- }
-
- /** Replace the last arrayLength parameter types with the component type of arrayType.
- * @param arrayType any array type
- * @param arrayLength the number of parameter types to change
- * @return the resulting type
- */
- /*non-public*/ MethodType asSpreaderType(Class<?> arrayType, int arrayLength) {
- assert(parameterCount() >= arrayLength);
- int spreadPos = ptypes.length - arrayLength;
- if (arrayLength == 0) return this; // nothing to change
- if (arrayType == Object[].class) {
- if (isGeneric()) return this; // nothing to change
- if (spreadPos == 0) {
- // no leading arguments to preserve; go generic
- MethodType res = genericMethodType(arrayLength);
- if (rtype != Object.class) {
- res = res.changeReturnType(rtype);
- }
- return res;
- }
- }
- Class<?> elemType = arrayType.getComponentType();
- assert(elemType != null);
- for (int i = spreadPos; i < ptypes.length; i++) {
- if (ptypes[i] != elemType) {
- Class<?>[] fixedPtypes = ptypes.clone();
- Arrays.fill(fixedPtypes, i, ptypes.length, elemType);
- return methodType(rtype, fixedPtypes);
- }
- }
- return this; // arguments check out; no change
- }
-
- /** Return the leading parameter type, which must exist and be a reference.
- * @return the leading parameter type, after error checks
- */
- /*non-public*/ Class<?> leadingReferenceParameter() {
- Class<?> ptype;
- if (ptypes.length == 0 ||
- (ptype = ptypes[0]).isPrimitive())
- throw newIllegalArgumentException("no leading reference parameter");
- return ptype;
- }
-
- /** Delete the last parameter type and replace it with arrayLength copies of the component type of arrayType.
- * @param arrayType any array type
- * @param arrayLength the number of parameter types to insert
- * @return the resulting type
- */
- /*non-public*/ MethodType asCollectorType(Class<?> arrayType, int arrayLength) {
- assert(parameterCount() >= 1);
- assert(lastParameterType().isAssignableFrom(arrayType));
- MethodType res;
- if (arrayType == Object[].class) {
- res = genericMethodType(arrayLength);
- if (rtype != Object.class) {
- res = res.changeReturnType(rtype);
- }
- } else {
- Class<?> elemType = arrayType.getComponentType();
- assert(elemType != null);
- res = methodType(rtype, Collections.nCopies(arrayLength, elemType));
- }
- if (ptypes.length == 1) {
- return res;
- } else {
- return res.insertParameterTypes(0, parameterList().subList(0, ptypes.length-1));
- }
- }
-
- /**
- * Finds or creates a method type with some parameter types omitted.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param start the index (zero-based) of the first parameter type to remove
- * @param end the index (greater than {@code start}) of the first parameter type after not to remove
- * @return the same type, except with the selected parameter(s) removed
- * @throws IndexOutOfBoundsException if {@code start} is negative or greater than {@code parameterCount()}
- * or if {@code end} is negative or greater than {@code parameterCount()}
- * or if {@code start} is greater than {@code end}
- */
- public MethodType dropParameterTypes(int start, int end) {
- int len = ptypes.length;
- if (!(0 <= start && start <= end && end <= len))
- throw newIndexOutOfBoundsException("start="+start+" end="+end);
- if (start == end) return this;
- Class<?>[] nptypes;
- if (start == 0) {
- if (end == len) {
- // drop all parameters
- nptypes = NO_PTYPES;
- } else {
- // drop initial parameter(s)
- nptypes = Arrays.copyOfRange(ptypes, end, len);
- }
- } else {
- if (end == len) {
- // drop trailing parameter(s)
- nptypes = Arrays.copyOfRange(ptypes, 0, start);
- } else {
- int tail = len - end;
- nptypes = Arrays.copyOfRange(ptypes, 0, start + tail);
- System.arraycopy(ptypes, end, nptypes, start, tail);
- }
- }
- return makeImpl(rtype, nptypes, true);
- }
-
- /**
- * Finds or creates a method type with a different return type.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param nrtype a return parameter type to replace the old one with
- * @return the same type, except with the return type change
- * @throws NullPointerException if {@code nrtype} is null
- */
- public MethodType changeReturnType(Class<?> nrtype) {
- if (returnType() == nrtype) return this;
- return makeImpl(nrtype, ptypes, true);
- }
-
- /**
- * Reports if this type contains a primitive argument or return value.
- * The return type {@code void} counts as a primitive.
- * @return true if any of the types are primitives
- */
- public boolean hasPrimitives() {
- return form.hasPrimitives();
- }
-
- /**
- * Reports if this type contains a wrapper argument or return value.
- * Wrappers are types which box primitive values, such as {@link Integer}.
- * The reference type {@code java.lang.Void} counts as a wrapper,
- * if it occurs as a return type.
- * @return true if any of the types are wrappers
- */
- public boolean hasWrappers() {
- return unwrap() != this;
- }
-
- /**
- * Erases all reference types to {@code Object}.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All primitive types (including {@code void}) will remain unchanged.
- * @return a version of the original type with all reference types replaced
- */
- public MethodType erase() {
- return form.erasedType();
- }
-
- /**
- * Erases all reference types to {@code Object}, and all subword types to {@code int}.
- * This is the reduced type polymorphism used by private methods
- * such as {@link MethodHandle#invokeBasic invokeBasic}.
- * @return a version of the original type with all reference and subword types replaced
- */
- /*non-public*/ MethodType basicType() {
- return form.basicType();
- }
-
- /**
- * @return a version of the original type with MethodHandle prepended as the first argument
- */
- /*non-public*/ MethodType invokerType() {
- return insertParameterTypes(0, MethodHandle.class);
- }
+ MethodType genericMethodType(int objectArgCount) { return null; }
- /**
- * Converts all types, both reference and primitive, to {@code Object}.
- * Convenience method for {@link #genericMethodType(int) genericMethodType}.
- * The expression {@code type.wrap().erase()} produces the same value
- * as {@code type.generic()}.
- * @return a version of the original type with all types replaced
- */
- public MethodType generic() {
- return genericMethodType(parameterCount());
- }
+ public MethodType changeParameterType(int num, Class<?> nptype) { return null; }
- /*non-public*/ boolean isGeneric() {
- return this == erase() && !hasPrimitives();
- }
+ public MethodType insertParameterTypes(int num, Class<?>... ptypesToInsert) { return null; }
- /**
- * Converts all primitive types to their corresponding wrapper types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All reference types (including wrapper types) will remain unchanged.
- * A {@code void} return type is changed to the type {@code java.lang.Void}.
- * The expression {@code type.wrap().erase()} produces the same value
- * as {@code type.generic()}.
- * @return a version of the original type with all primitive types replaced
- */
- public MethodType wrap() {
- return hasPrimitives() ? wrapWithPrims(this) : this;
- }
+ public MethodType appendParameterTypes(Class<?>... ptypesToInsert) { return null; }
- /**
- * Converts all wrapper types to their corresponding primitive types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All primitive types (including {@code void}) will remain unchanged.
- * A return type of {@code java.lang.Void} is changed to {@code void}.
- * @return a version of the original type with all wrapper types replaced
- */
- public MethodType unwrap() {
- MethodType noprims = !hasPrimitives() ? this : wrapWithPrims(this);
- return unwrapWithNoPrims(noprims);
- }
+ public MethodType insertParameterTypes(int num, List<Class<?>> ptypesToInsert) { return null; }
- private static MethodType wrapWithPrims(MethodType pt) {
- assert(pt.hasPrimitives());
- MethodType wt = pt.wrapAlt;
- if (wt == null) {
- // fill in lazily
- wt = MethodTypeForm.canonicalize(pt, MethodTypeForm.WRAP, MethodTypeForm.WRAP);
- assert(wt != null);
- pt.wrapAlt = wt;
- }
- return wt;
- }
+ public MethodType appendParameterTypes(List<Class<?>> ptypesToInsert) { return null; }
- private static MethodType unwrapWithNoPrims(MethodType wt) {
- assert(!wt.hasPrimitives());
- MethodType uwt = wt.wrapAlt;
- if (uwt == null) {
- // fill in lazily
- uwt = MethodTypeForm.canonicalize(wt, MethodTypeForm.UNWRAP, MethodTypeForm.UNWRAP);
- if (uwt == null)
- uwt = wt; // type has no wrappers or prims at all
- wt.wrapAlt = uwt;
- }
- return uwt;
- }
+ public MethodType dropParameterTypes(int start, int end) { return null; }
- /**
- * Returns the parameter type at the specified index, within this method type.
- * @param num the index (zero-based) of the desired parameter type
- * @return the selected parameter type
- * @throws IndexOutOfBoundsException if {@code num} is not a valid index into {@code parameterArray()}
- */
- public Class<?> parameterType(int num) {
- return ptypes[num];
- }
- /**
- * Returns the number of parameter types in this method type.
- * @return the number of parameter types
- */
- public int parameterCount() {
- return ptypes.length;
- }
- /**
- * Returns the return type of this method type.
- * @return the return type
- */
- public Class<?> returnType() {
- return rtype;
- }
+ public MethodType changeReturnType(Class<?> nrtype) { return null; }
- /**
- * Presents the parameter types as a list (a convenience method).
- * The list will be immutable.
- * @return the parameter types (as an immutable list)
- */
- public List<Class<?>> parameterList() {
- return Collections.unmodifiableList(Arrays.asList(ptypes.clone()));
- }
+ public boolean hasPrimitives() { return false; }
- /*non-public*/ Class<?> lastParameterType() {
- int len = ptypes.length;
- return len == 0 ? void.class : ptypes[len-1];
- }
+ public boolean hasWrappers() { return false; }
- /**
- * Presents the parameter types as an array (a convenience method).
- * Changes to the array will not result in changes to the type.
- * @return the parameter types (as a fresh copy if necessary)
- */
- public Class<?>[] parameterArray() {
- return ptypes.clone();
- }
+ public MethodType erase() { return null; }
- /**
- * Compares the specified object with this type for equality.
- * That is, it returns <tt>true</tt> if and only if the specified object
- * is also a method type with exactly the same parameters and return type.
- * @param x object to compare
- * @see Object#equals(Object)
- */
- @Override
- public boolean equals(Object x) {
- return this == x || x instanceof MethodType && equals((MethodType)x);
- }
+ public MethodType generic() { return null; }
- private boolean equals(MethodType that) {
- return this.rtype == that.rtype
- && Arrays.equals(this.ptypes, that.ptypes);
- }
+ public MethodType wrap() { return null; }
- /**
- * Returns the hash code value for this method type.
- * It is defined to be the same as the hashcode of a List
- * whose elements are the return type followed by the
- * parameter types.
- * @return the hash code value for this method type
- * @see Object#hashCode()
- * @see #equals(Object)
- * @see List#hashCode()
- */
- @Override
- public int hashCode() {
- int hashCode = 31 + rtype.hashCode();
- for (Class<?> ptype : ptypes)
- hashCode = 31*hashCode + ptype.hashCode();
- return hashCode;
- }
+ public MethodType unwrap() { return null; }
- /**
- * Returns a string representation of the method type,
- * of the form {@code "(PT0,PT1...)RT"}.
- * The string representation of a method type is a
- * parenthesis enclosed, comma separated list of type names,
- * followed immediately by the return type.
- * <p>
- * Each type is represented by its
- * {@link java.lang.Class#getSimpleName simple name}.
- */
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("(");
- for (int i = 0; i < ptypes.length; i++) {
- if (i > 0) sb.append(",");
- sb.append(ptypes[i].getSimpleName());
- }
- sb.append(")");
- sb.append(rtype.getSimpleName());
- return sb.toString();
- }
+ public Class<?> parameterType(int num) { return null; }
- /** True if the old return type can always be viewed (w/o casting) under new return type,
- * and the new parameters can be viewed (w/o casting) under the old parameter types.
- */
- // Android-changed: Removed implementation details.
- // boolean isViewableAs(MethodType newType, boolean keepInterfaces);
- // boolean parametersAreViewableAs(MethodType newType, boolean keepInterfaces);
- /*non-public*/
- boolean isConvertibleTo(MethodType newType) {
- MethodTypeForm oldForm = this.form();
- MethodTypeForm newForm = newType.form();
- if (oldForm == newForm)
- // same parameter count, same primitive/object mix
- return true;
- if (!canConvert(returnType(), newType.returnType()))
- return false;
- Class<?>[] srcTypes = newType.ptypes;
- Class<?>[] dstTypes = ptypes;
- if (srcTypes == dstTypes)
- return true;
- int argc;
- if ((argc = srcTypes.length) != dstTypes.length)
- return false;
- if (argc <= 1) {
- if (argc == 1 && !canConvert(srcTypes[0], dstTypes[0]))
- return false;
- return true;
- }
- if ((oldForm.primitiveParameterCount() == 0 && oldForm.erasedType == this) ||
- (newForm.primitiveParameterCount() == 0 && newForm.erasedType == newType)) {
- // Somewhat complicated test to avoid a loop of 2 or more trips.
- // If either type has only Object parameters, we know we can convert.
- assert(canConvertParameters(srcTypes, dstTypes));
- return true;
- }
- return canConvertParameters(srcTypes, dstTypes);
- }
-
- /** Returns true if MHs.explicitCastArguments produces the same result as MH.asType.
- * If the type conversion is impossible for either, the result should be false.
- */
- /*non-public*/
- boolean explicitCastEquivalentToAsType(MethodType newType) {
- if (this == newType) return true;
- if (!explicitCastEquivalentToAsType(rtype, newType.rtype)) {
- return false;
- }
- Class<?>[] srcTypes = newType.ptypes;
- Class<?>[] dstTypes = ptypes;
- if (dstTypes == srcTypes) {
- return true;
- }
- assert(dstTypes.length == srcTypes.length);
- for (int i = 0; i < dstTypes.length; i++) {
- if (!explicitCastEquivalentToAsType(srcTypes[i], dstTypes[i])) {
- return false;
- }
- }
- return true;
- }
-
- /** Reports true if the src can be converted to the dst, by both asType and MHs.eCE,
- * and with the same effect.
- * MHs.eCA has the following "upgrades" to MH.asType:
- * 1. interfaces are unchecked (that is, treated as if aliased to Object)
- * Therefore, {@code Object->CharSequence} is possible in both cases but has different semantics
- * 2a. the full matrix of primitive-to-primitive conversions is supported
- * Narrowing like {@code long->byte} and basic-typing like {@code boolean->int}
- * are not supported by asType, but anything supported by asType is equivalent
- * with MHs.eCE.
- * 2b. conversion of void->primitive means explicit cast has to insert zero/false/null.
- * 3a. unboxing conversions can be followed by the full matrix of primitive conversions
- * 3b. unboxing of null is permitted (creates a zero primitive value)
- * Other than interfaces, reference-to-reference conversions are the same.
- * Boxing primitives to references is the same for both operators.
- */
- private static boolean explicitCastEquivalentToAsType(Class<?> src, Class<?> dst) {
- if (src == dst || dst == Object.class || dst == void.class) {
- return true;
- } else if (src.isPrimitive() && src != void.class) {
- // Could be a prim/prim conversion, where casting is a strict superset.
- // Or a boxing conversion, which is always to an exact wrapper class.
- return canConvert(src, dst);
- } else if (dst.isPrimitive()) {
- // Unboxing behavior is different between MHs.eCA & MH.asType (see 3b).
- return false;
- } else {
- // R->R always works, but we have to avoid a check-cast to an interface.
- return !dst.isInterface() || dst.isAssignableFrom(src);
- }
- }
+ public int parameterCount() { return 0; }
- private boolean canConvertParameters(Class<?>[] srcTypes, Class<?>[] dstTypes) {
- for (int i = 0; i < srcTypes.length; i++) {
- if (!canConvert(srcTypes[i], dstTypes[i])) {
- return false;
- }
- }
- return true;
- }
+ public Class<?> returnType() { return null; }
- /*non-public*/
- static boolean canConvert(Class<?> src, Class<?> dst) {
- // short-circuit a few cases:
- if (src == dst || src == Object.class || dst == Object.class) return true;
- // the remainder of this logic is documented in MethodHandle.asType
- if (src.isPrimitive()) {
- // can force void to an explicit null, a la reflect.Method.invoke
- // can also force void to a primitive zero, by analogy
- if (src == void.class) return true; //or !dst.isPrimitive()?
- Wrapper sw = Wrapper.forPrimitiveType(src);
- if (dst.isPrimitive()) {
- // P->P must widen
- return Wrapper.forPrimitiveType(dst).isConvertibleFrom(sw);
- } else {
- // P->R must box and widen
- return dst.isAssignableFrom(sw.wrapperType());
- }
- } else if (dst.isPrimitive()) {
- // any value can be dropped
- if (dst == void.class) return true;
- Wrapper dw = Wrapper.forPrimitiveType(dst);
- // R->P must be able to unbox (from a dynamically chosen type) and widen
- // For example:
- // Byte/Number/Comparable/Object -> dw:Byte -> byte.
- // Character/Comparable/Object -> dw:Character -> char
- // Boolean/Comparable/Object -> dw:Boolean -> boolean
- // This means that dw must be cast-compatible with src.
- if (src.isAssignableFrom(dw.wrapperType())) {
- return true;
- }
- // The above does not work if the source reference is strongly typed
- // to a wrapper whose primitive must be widened. For example:
- // Byte -> unbox:byte -> short/int/long/float/double
- // Character -> unbox:char -> int/long/float/double
- if (Wrapper.isWrapperType(src) &&
- dw.isConvertibleFrom(Wrapper.forWrapperType(src))) {
- // can unbox from src and then widen to dst
- return true;
- }
- // We have already covered cases which arise due to runtime unboxing
- // of a reference type which covers several wrapper types:
- // Object -> cast:Integer -> unbox:int -> long/float/double
- // Serializable -> cast:Byte -> unbox:byte -> byte/short/int/long/float/double
- // An marginal case is Number -> dw:Character -> char, which would be OK if there were a
- // subclass of Number which wraps a value that can convert to char.
- // Since there is none, we don't need an extra check here to cover char or boolean.
- return false;
- } else {
- // R->R always works, since null is always valid dynamically
- return true;
- }
- }
+ public List<Class<?>> parameterList() { return null; }
- /** Reports the number of JVM stack slots required to invoke a method
- * of this type. Note that (for historical reasons) the JVM requires
- * a second stack slot to pass long and double arguments.
- * So this method returns {@link #parameterCount() parameterCount} plus the
- * number of long and double parameters (if any).
- * <p>
- * This method is included for the benefit of applications that must
- * generate bytecodes that process method handles and invokedynamic.
- * @return the number of JVM stack slots for this type's parameters
- */
- /*non-public*/ int parameterSlotCount() {
- return form.parameterSlotCount();
- }
+ public Class<?>[] parameterArray() { return null; }
- /// Queries which have to do with the bytecode architecture
-
- // Android-changed: These methods aren't needed on Android and are unused within the JDK.
- //
- // int parameterSlotDepth(int num);
- // int returnSlotCount();
- //
- // Android-changed: Removed cache of higher order adapters.
- //
- // Invokers invokers();
-
- /**
- * Finds or creates an instance of a method type, given the spelling of its bytecode descriptor.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * Any class or interface name embedded in the descriptor string
- * will be resolved by calling {@link ClassLoader#loadClass(java.lang.String)}
- * on the given loader (or if it is null, on the system class loader).
- * <p>
- * Note that it is possible to encounter method types which cannot be
- * constructed by this method, because their component types are
- * not all reachable from a common class loader.
- * <p>
- * This method is included for the benefit of applications that must
- * generate bytecodes that process method handles and {@code invokedynamic}.
- * @param descriptor a bytecode-level type descriptor string "(T...)T"
- * @param loader the class loader in which to look up the types
- * @return a method type matching the bytecode-level type descriptor
- * @throws NullPointerException if the string is null
- * @throws IllegalArgumentException if the string is not well-formed
- * @throws TypeNotPresentException if a named type cannot be found
- */
public static MethodType fromMethodDescriptorString(String descriptor, ClassLoader loader)
- throws IllegalArgumentException, TypeNotPresentException
- {
- if (!descriptor.startsWith("(") || // also generates NPE if needed
- descriptor.indexOf(')') < 0 ||
- descriptor.indexOf('.') >= 0)
- throw newIllegalArgumentException("not a method descriptor: "+descriptor);
- List<Class<?>> types = BytecodeDescriptor.parseMethod(descriptor, loader);
- Class<?> rtype = types.remove(types.size() - 1);
- checkSlotCount(types.size());
- Class<?>[] ptypes = listToArray(types);
- return makeImpl(rtype, ptypes, true);
- }
-
- /**
- * Produces a bytecode descriptor representation of the method type.
- * <p>
- * Note that this is not a strict inverse of {@link #fromMethodDescriptorString fromMethodDescriptorString}.
- * Two distinct classes which share a common name but have different class loaders
- * will appear identical when viewed within descriptor strings.
- * <p>
- * This method is included for the benefit of applications that must
- * generate bytecodes that process method handles and {@code invokedynamic}.
- * {@link #fromMethodDescriptorString(java.lang.String, java.lang.ClassLoader) fromMethodDescriptorString},
- * because the latter requires a suitable class loader argument.
- * @return the bytecode type descriptor representation
- */
- public String toMethodDescriptorString() {
- String desc = methodDescriptor;
- if (desc == null) {
- desc = BytecodeDescriptor.unparse(this);
- methodDescriptor = desc;
- }
- return desc;
- }
-
- /*non-public*/ static String toFieldDescriptorString(Class<?> cls) {
- return BytecodeDescriptor.unparse(cls);
- }
+ throws IllegalArgumentException, TypeNotPresentException { return null; }
- /// Serialization.
-
- /**
- * There are no serializable fields for {@code MethodType}.
- */
- private static final java.io.ObjectStreamField[] serialPersistentFields = { };
-
- /**
- * Save the {@code MethodType} instance to a stream.
- *
- * @serialData
- * For portability, the serialized format does not refer to named fields.
- * Instead, the return type and parameter type arrays are written directly
- * from the {@code writeObject} method, using two calls to {@code s.writeObject}
- * as follows:
- * <blockquote><pre>{@code
-s.writeObject(this.returnType());
-s.writeObject(this.parameterArray());
- * }</pre></blockquote>
- * <p>
- * The deserialized field values are checked as if they were
- * provided to the factory method {@link #methodType(Class,Class[]) methodType}.
- * For example, null values, or {@code void} parameter types,
- * will lead to exceptions during deserialization.
- * @param s the stream to write the object to
- * @throws java.io.IOException if there is a problem writing the object
- */
- private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
- s.defaultWriteObject(); // requires serialPersistentFields to be an empty array
- s.writeObject(returnType());
- s.writeObject(parameterArray());
- }
-
- /**
- * Reconstitute the {@code MethodType} instance from a stream (that is,
- * deserialize it).
- * This instance is a scratch object with bogus final fields.
- * It provides the parameters to the factory method called by
- * {@link #readResolve readResolve}.
- * After that call it is discarded.
- * @param s the stream to read the object from
- * @throws java.io.IOException if there is a problem reading the object
- * @throws ClassNotFoundException if one of the component classes cannot be resolved
- * @see #MethodType()
- * @see #readResolve
- * @see #writeObject
- */
- private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
- s.defaultReadObject(); // requires serialPersistentFields to be an empty array
-
- Class<?> returnType = (Class<?>) s.readObject();
- Class<?>[] parameterArray = (Class<?>[]) s.readObject();
-
- // Probably this object will never escape, but let's check
- // the field values now, just to be sure.
- checkRtype(returnType);
- checkPtypes(parameterArray);
-
- parameterArray = parameterArray.clone(); // make sure it is unshared
- MethodType_init(returnType, parameterArray);
- }
-
- /**
- * For serialization only.
- * Sets the final fields to null, pending {@code Unsafe.putObject}.
- */
- private MethodType() {
- this.rtype = null;
- this.ptypes = null;
- }
- private void MethodType_init(Class<?> rtype, Class<?>[] ptypes) {
- // In order to communicate these values to readResolve, we must
- // store them into the implementation-specific final fields.
- checkRtype(rtype);
- checkPtypes(ptypes);
- UNSAFE.putObject(this, rtypeOffset, rtype);
- UNSAFE.putObject(this, ptypesOffset, ptypes);
- }
-
- // Support for resetting final fields while deserializing
- private static final long rtypeOffset, ptypesOffset;
- static {
- try {
- rtypeOffset = UNSAFE.objectFieldOffset
- (MethodType.class.getDeclaredField("rtype"));
- ptypesOffset = UNSAFE.objectFieldOffset
- (MethodType.class.getDeclaredField("ptypes"));
- } catch (Exception ex) {
- throw new Error(ex);
- }
- }
-
- /**
- * Resolves and initializes a {@code MethodType} object
- * after serialization.
- * @return the fully initialized {@code MethodType} object
- */
- private Object readResolve() {
- // Do not use a trusted path for deserialization:
- //return makeImpl(rtype, ptypes, true);
- // Verify all operands, and make sure ptypes is unshared:
- return methodType(rtype, ptypes);
- }
-
- /**
- * Simple implementation of weak concurrent intern set.
- *
- * @param <T> interned type
- */
- private static class ConcurrentWeakInternSet<T> {
-
- private final ConcurrentMap<WeakEntry<T>, WeakEntry<T>> map;
- private final ReferenceQueue<T> stale;
-
- public ConcurrentWeakInternSet() {
- this.map = new ConcurrentHashMap<>();
- this.stale = new ReferenceQueue<>();
- }
-
- /**
- * Get the existing interned element.
- * This method returns null if no element is interned.
- *
- * @param elem element to look up
- * @return the interned element
- */
- public T get(T elem) {
- if (elem == null) throw new NullPointerException();
- expungeStaleElements();
-
- WeakEntry<T> value = map.get(new WeakEntry<>(elem));
- if (value != null) {
- T res = value.get();
- if (res != null) {
- return res;
- }
- }
- return null;
- }
-
- /**
- * Interns the element.
- * Always returns non-null element, matching the one in the intern set.
- * Under the race against another add(), it can return <i>different</i>
- * element, if another thread beats us to interning it.
- *
- * @param elem element to add
- * @return element that was actually added
- */
- public T add(T elem) {
- if (elem == null) throw new NullPointerException();
-
- // Playing double race here, and so spinloop is required.
- // First race is with two concurrent updaters.
- // Second race is with GC purging weak ref under our feet.
- // Hopefully, we almost always end up with a single pass.
- T interned;
- WeakEntry<T> e = new WeakEntry<>(elem, stale);
- do {
- expungeStaleElements();
- WeakEntry<T> exist = map.putIfAbsent(e, e);
- interned = (exist == null) ? elem : exist.get();
- } while (interned == null);
- return interned;
- }
-
- private void expungeStaleElements() {
- Reference<? extends T> reference;
- while ((reference = stale.poll()) != null) {
- map.remove(reference);
- }
- }
-
- private static class WeakEntry<T> extends WeakReference<T> {
-
- public final int hashcode;
-
- public WeakEntry(T key, ReferenceQueue<T> queue) {
- super(key, queue);
- hashcode = key.hashCode();
- }
-
- public WeakEntry(T key) {
- super(key);
- hashcode = key.hashCode();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof WeakEntry) {
- Object that = ((WeakEntry) obj).get();
- Object mine = get();
- return (that == null || mine == null) ? (this == obj) : mine.equals(that);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return hashcode;
- }
-
- }
- }
+ public String toMethodDescriptorString() { return null; }
}
diff --git a/java/lang/ref/Reference.java b/java/lang/ref/Reference.java
index 06b517ec..4d51b18f 100644
--- a/java/lang/ref/Reference.java
+++ b/java/lang/ref/Reference.java
@@ -282,16 +282,26 @@ public abstract class Reference<T> {
// Android-changed: reachabilityFence implementation differs from OpenJDK9.
public static void reachabilityFence(Object ref) {
+ // This code is usually replaced by much faster intrinsic implementations.
+ // It will be executed for tests run with the access checks interpreter in
+ // ART, e.g. with --verify-soft-fail. Since this is a volatile store, it
+ // cannot easily be moved up past prior accesses, even if this method is
+ // inlined.
SinkHolder.sink = ref;
- // TODO: This is a horrible implementation. Fix it. Remove SinkHolder.
- // b/72698200 .
+ // Leaving SinkHolder set to ref is unpleasant, since it keeps ref live
+ // until the next reachabilityFence call. This causes e.g. 036-finalizer
+ // to fail. Clear it again in a way that's unlikely to be optimizable.
+ // The fact that finalize_count is volatile makes it hard to move the test up.
+ if (SinkHolder.finalize_count == 0) {
+ SinkHolder.sink = null;
+ }
}
private static class SinkHolder {
static volatile Object sink;
// Ensure that sink looks live to even a reasonably clever compiler.
- private static int finalize_count = 0;
+ private static volatile int finalize_count = 0;
private static Object sinkUser = new Object() {
protected void finalize() {
diff --git a/java/net/Inet6AddressImpl.java b/java/net/Inet6AddressImpl.java
index cfc2d132..1edfe344 100644
--- a/java/net/Inet6AddressImpl.java
+++ b/java/net/Inet6AddressImpl.java
@@ -290,9 +290,11 @@ class Inet6AddressImpl implements InetAddressImpl {
} catch (IOException e) {
// Silently ignore and fall back.
} finally {
- try {
- Libcore.os.close(fd);
- } catch (ErrnoException e) { }
+ if (fd != null) {
+ try {
+ Libcore.os.close(fd);
+ } catch (ErrnoException e) { }
+ }
}
return false;
diff --git a/java/security/KeyStore.java b/java/security/KeyStore.java
index d0917817..8fe46b86 100644
--- a/java/security/KeyStore.java
+++ b/java/security/KeyStore.java
@@ -311,7 +311,7 @@ public class KeyStore {
* @param protectionAlgorithm the encryption algorithm name, for
* example, {@code PBEWithHmacSHA256AndAES_256}.
* See the Cipher section in the <a href=
- * "{@docRoot}/../technotes/guides/security/StandardNames.html#Cipher">
+ * "{@docRoot}/openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html#Cipher">
* Java Cryptography Architecture Standard Algorithm Name
* Documentation</a>
* for information about standard encryption algorithm names.
diff --git a/java/security/cert/package-info.java b/java/security/cert/package-info.java
index 0ef896b8..0c1cb1f0 100644
--- a/java/security/cert/package-info.java
+++ b/java/security/cert/package-info.java
@@ -52,7 +52,7 @@
* <li><a href="http://www.ietf.org/rfc/rfc5280.txt">
* http://www.ietf.org/rfc/rfc5280.txt</a>
* <li><a href=
- * "{@docRoot}/../technotes/guides/security/certpath/CertPathProgGuide.html">
+ * "{@docRoot}/openjdk-redirect.html?v=8&path=/technotes/guides/security/certpath/CertPathProgGuide.html">
* <b>Java&trade;
* PKI Programmer's Guide</b></a>
* <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/cert3.html">
diff --git a/java/security/package-info.java b/java/security/package-info.java
index 376aa9d7..50a15278 100644
--- a/java/security/package-info.java
+++ b/java/security/package-info.java
@@ -46,14 +46,14 @@
* <h2>Package Specification</h2>
*
* <ul>
- * <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/crypto/CryptoSpec.html">
+ * <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/crypto/CryptoSpec.html">
* <b>Java&trade;
* Cryptography Architecture (JCA) Reference Guide</b></a></li>
*
* <li>PKCS #8: Private-Key Information Syntax Standard, Version 1.2,
* November 1993</li>
*
- * <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/StandardNames.html">
+ * <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html">
* <b>Java&trade;
* Cryptography Architecture Standard Algorithm Name
* Documentation</b></a></li>
@@ -64,44 +64,44 @@
* For further documentation, please see:
* <ul>
* <li><a href=
- * "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/spec/security-spec.doc.html">
+ * "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/spec/security-spec.doc.html">
* <b>Java&trade;
* SE Platform Security Architecture</b></a></li>
*
* <li><a href=
- * "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/crypto/HowToImplAProvider.html">
+ * "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/crypto/HowToImplAProvider.html">
* <b>How to Implement a Provider in the
* Java&trade; Cryptography Architecture
* </b></a></li>
*
* <li><a href=
- * "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/PolicyFiles.html"><b>
+ * "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/PolicyFiles.html"><b>
* Default Policy Implementation and Policy File Syntax
* </b></a></li>
*
* <li><a href=
- * "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/permissions.html"><b>
+ * "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/permissions.html"><b>
* Permissions in the
* Java&trade; SE Development Kit (JDK)
* </b></a></li>
*
* <li><a href=
- * "{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/SecurityToolsSummary.html"><b>
+ * "{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/SecurityToolsSummary.html"><b>
* Summary of Tools for
* Java&trade; Platform Security
* </b></a></li>
*
* <li><b>keytool</b>
- * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/unix/keytool.html">
+ * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/unix/keytool.html">
* for Solaris/Linux</a>)
- * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/windows/keytool.html">
+ * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/windows/keytool.html">
* for Windows</a>)
* </li>
*
* <li><b>jarsigner</b>
- * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/unix/jarsigner.html">
+ * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/unix/jarsigner.html">
* for Solaris/Linux</a>)
- * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/tools/windows/jarsigner.html">
+ * (<a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/tools/windows/jarsigner.html">
* for Windows</a>)
* </li>
*
diff --git a/java/util/LinkedHashMap.java b/java/util/LinkedHashMap.java
index aec40bc8..7c8cad04 100644
--- a/java/util/LinkedHashMap.java
+++ b/java/util/LinkedHashMap.java
@@ -158,7 +158,7 @@ import java.io.IOException;
* {@code LinkedHashMap}.
*
* <p>This class is a member of the
- * <a href="{@docRoot}/../technotes/guides/collections/index.html">
+ * <a href="{@docRoot}/openjdk-redirect.html?v=8&path=/technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @implNote
diff --git a/java/util/Locale.java b/java/util/Locale.java
index 9d9f3e41..c96bb138 100644
--- a/java/util/Locale.java
+++ b/java/util/Locale.java
@@ -520,6 +520,10 @@ import sun.util.locale.ParseStatus;
* <td><a href="http://site.icu-project.org/download/58">ICU 58.2</a></td>
* <td><a href="http://cldr.unicode.org/index/downloads/cldr-30">CLDR 30.0.3</a></td>
* <td><a href="http://www.unicode.org/versions/Unicode9.0.0/">Unicode 9.0</a></td></tr>
+ * <tr><td>Android 9.0 (TBD)</td>
+ * <td><a href="http://site.icu-project.org/download/60">ICU 60.2</a></td>
+ * <td><a href="http://cldr.unicode.org/index/downloads/cldr-32">CLDR 32.0.1</a></td>
+ * <td><a href="http://www.unicode.org/versions/Unicode10.0.0/">Unicode 10.0</a></td></tr>
* </table>
*
* <a name="default_locale"></a><h4>Be wary of the default locale</h3>
diff --git a/javax/security/auth/login/package-info.java b/javax/security/auth/login/package-info.java
index a0f7f68b..301ac21c 100644
--- a/javax/security/auth/login/package-info.java
+++ b/javax/security/auth/login/package-info.java
@@ -28,7 +28,7 @@
* <h2>Package Specification</h2>
*
* <ul>
- * <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/../technotes/guides/security/StandardNames.html">
+ * <li><a href="{@docRoot}openjdk-redirect.html?v=8&path=/technotes/guides/security/StandardNames.html">
* <b>Java&trade;
* Cryptography Architecture Standard Algorithm Name
* Documentation</b></a></li>