summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt8
-rw-r--r--core/api/module-lib-current.txt4
-rw-r--r--core/api/system-current.txt10
-rw-r--r--core/java/android/app/BroadcastOptions.java74
-rw-r--r--core/java/android/app/SystemServiceRegistry.java9
-rw-r--r--core/java/android/content/ClipDescription.java13
-rw-r--r--core/java/android/content/ComponentName.java17
-rw-r--r--core/java/android/content/Context.java8
-rw-r--r--core/java/android/content/Intent.java15
-rw-r--r--core/java/android/os/Binder.java2
-rw-r--r--core/java/android/os/BundleMerger.java379
-rw-r--r--core/java/android/os/PermissionEnforcer.java101
-rw-r--r--core/java/android/provider/Settings.java4
-rwxr-xr-xcore/java/android/util/DisplayMetrics.java8
-rw-r--r--core/java/android/view/ViewRootImpl.java2
-rw-r--r--core/proto/android/server/activitymanagerservice.proto1
-rw-r--r--core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java433
-rw-r--r--core/tests/coretests/src/android/app/backup/OWNERS1
-rw-r--r--core/tests/coretests/src/android/os/BundleMergerTest.java408
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java157
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java142
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java27
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java4
-rw-r--r--location/java/android/location/GnssCapabilities.java6
-rw-r--r--location/java/android/location/GnssStatus.java6
-rw-r--r--packages/CompanionDeviceManager/res/values/styles.xml1
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt53
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java32
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java6
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java126
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java4
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java76
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java18
-rw-r--r--packages/SystemUI/AndroidManifest.xml6
-rw-r--r--packages/SystemUI/docs/device-entry/quickaffordance.md2
-rw-r--r--packages/SystemUI/res/drawable/accessibility_floating_message_background.xml22
-rw-r--r--packages/SystemUI/res/layout-land/auth_credential_password_view.xml4
-rw-r--r--packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml4
-rw-r--r--packages/SystemUI/res/layout/auth_credential_password_view.xml4
-rw-r--r--packages/SystemUI/res/layout/auth_credential_pattern_view.xml4
-rw-r--r--packages/SystemUI/res/values-night/colors.xml2
-rw-r--r--packages/SystemUI/res/values/colors.xml2
-rw-r--r--packages/SystemUI/res/values/config.xml12
-rw-r--r--packages/SystemUI/res/values/dimens.xml8
-rw-r--r--packages/SystemUI/res/values/ids.xml1
-rw-r--r--packages/SystemUI/res/values/strings.xml11
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java9
-rw-r--r--packages/SystemUI/src/com/android/keyguard/ClockEventController.kt15
-rw-r--r--packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt96
-rw-r--r--packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt76
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java52
-rw-r--r--packages/SystemUI/src/com/android/keyguard/LockIconViewController.java74
-rw-r--r--packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java179
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java25
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java162
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java142
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java97
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java32
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java238
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java113
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java565
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt102
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt282
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt189
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt69
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt130
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt104
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt75
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt140
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt178
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java92
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt (renamed from packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt)24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt (renamed from packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt)32
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt (renamed from packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt)24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt (renamed from packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt)24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt (renamed from packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt)2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt72
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt81
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java24
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java (renamed from packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java)28
-rw-r--r--packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java10
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt76
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java90
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java225
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java269
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt123
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java67
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java31
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java33
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java25
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java46
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt93
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt81
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt216
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt270
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt92
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt181
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java478
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt)8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt)22
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt)24
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt)24
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt)22
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt25
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt29
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt32
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt166
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt88
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt222
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java)84
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java27
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt48
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt31
-rw-r--r--packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt2
-rw-r--r--packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt14
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java12
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java44
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java17
-rw-r--r--services/core/java/com/android/server/SystemService.java12
-rw-r--r--services/core/java/com/android/server/SystemServiceManager.java63
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java53
-rw-r--r--services/core/java/com/android/server/am/BroadcastConstants.java2
-rw-r--r--services/core/java/com/android/server/am/BroadcastProcessQueue.java70
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueue.md98
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueueModernImpl.java80
-rw-r--r--services/core/java/com/android/server/am/EventLogTags.logtags7
-rw-r--r--services/core/java/com/android/server/am/ProcessRecord.java19
-rw-r--r--services/core/java/com/android/server/am/SameProcessApplicationThread.java73
-rw-r--r--services/core/java/com/android/server/am/UserController.java132
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/FaceService.java9
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java3
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java39
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java7
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java36
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java18
-rw-r--r--services/core/java/com/android/server/connectivity/Vpn.java103
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java10
-rw-r--r--services/core/java/com/android/server/pm/OWNERS12
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java3
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java105
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java41
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java32
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java11
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java7
-rw-r--r--telephony/java/android/telephony/SubscriptionManager.java24
-rwxr-xr-xtelephony/java/com/android/internal/telephony/ISub.aidl8
198 files changed, 8530 insertions, 2463 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 997a3c1089cd..eef2324c7a6c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9268,6 +9268,7 @@ package android.content {
field public static final int CLASSIFICATION_NOT_COMPLETE = 1; // 0x1
field public static final int CLASSIFICATION_NOT_PERFORMED = 2; // 0x2
field @NonNull public static final android.os.Parcelable.Creator<android.content.ClipDescription> CREATOR;
+ field public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE";
field public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE";
field public static final String MIMETYPE_TEXT_HTML = "text/html";
field public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent";
@@ -19496,7 +19497,7 @@ package android.location {
method public boolean hasSatelliteBlocklist();
method public boolean hasSatellitePvt();
method public boolean hasScheduling();
- method public boolean hasSingleShot();
+ method public boolean hasSingleShotFix();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssCapabilities> CREATOR;
}
@@ -19528,7 +19529,7 @@ package android.location {
method @NonNull public android.location.GnssCapabilities.Builder setHasSatelliteBlocklist(boolean);
method @NonNull public android.location.GnssCapabilities.Builder setHasSatellitePvt(boolean);
method @NonNull public android.location.GnssCapabilities.Builder setHasScheduling(boolean);
- method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShot(boolean);
+ method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShotFix(boolean);
}
public final class GnssClock implements android.os.Parcelable {
@@ -19726,7 +19727,7 @@ package android.location {
method public int getConstellationType(@IntRange(from=0) int);
method @FloatRange(from=0xffffffa6, to=90) public float getElevationDegrees(@IntRange(from=0) int);
method @IntRange(from=0) public int getSatelliteCount();
- method @IntRange(from=1, to=200) public int getSvid(@IntRange(from=0) int);
+ method @IntRange(from=1, to=206) public int getSvid(@IntRange(from=0) int);
method public boolean hasAlmanacData(@IntRange(from=0) int);
method public boolean hasBasebandCn0DbHz(@IntRange(from=0) int);
method public boolean hasCarrierFrequencyHz(@IntRange(from=0) int);
@@ -47430,6 +47431,7 @@ package android.util {
field public static final int DENSITY_420 = 420; // 0x1a4
field public static final int DENSITY_440 = 440; // 0x1b8
field public static final int DENSITY_450 = 450; // 0x1c2
+ field public static final int DENSITY_520 = 520; // 0x208
field public static final int DENSITY_560 = 560; // 0x230
field public static final int DENSITY_600 = 600; // 0x258
field public static final int DENSITY_DEFAULT = 160; // 0xa0
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index e890005c0479..88efcced78fb 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -301,10 +301,6 @@ package android.os {
method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void reportNetworkInterfaceForTransports(@NonNull String, @NonNull int[]) throws java.lang.RuntimeException;
}
- public class Binder implements android.os.IBinder {
- method public final void markVintfStability();
- }
-
public class BluetoothServiceManager {
method @NonNull public android.os.BluetoothServiceManager.ServiceRegisterer getBluetoothManagerServiceRegisterer();
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7a22e373045d..a382ecfc99d3 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -775,11 +775,14 @@ package android.app {
}
public class BroadcastOptions {
+ method public void clearDeliveryGroupPolicy();
method public void clearRequireCompatChange();
+ method public int getDeliveryGroupPolicy();
method public boolean isPendingIntentBackgroundActivityLaunchAllowed();
method public static android.app.BroadcastOptions makeBasic();
method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long);
method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean);
+ method public void setDeliveryGroupPolicy(int);
method public void setDontSendToRestrictedApps(boolean);
method public void setPendingIntentBackgroundActivityLaunchAllowed(boolean);
method public void setRequireAllOfPermissions(@Nullable String[]);
@@ -788,6 +791,8 @@ package android.app {
method @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppAllowlist(long, int, int, @Nullable String);
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppWhitelistDuration(long);
method public android.os.Bundle toBundle();
+ field public static final int DELIVERY_GROUP_POLICY_ALL = 0; // 0x0
+ field public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; // 0x1
}
public class DownloadManager {
@@ -9346,6 +9351,7 @@ package android.os {
public class Binder implements android.os.IBinder {
method public int handleShellCommand(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]);
+ method public final void markVintfStability();
method public static void setProxyTransactListener(@Nullable android.os.Binder.ProxyTransactListener);
}
@@ -13392,7 +13398,7 @@ package android.telephony {
method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList();
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int);
method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int);
- method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int);
+ method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getSubscriptionUserHandle(int);
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int);
method public void requestEmbeddedSubscriptionInfoListRefresh();
method public void requestEmbeddedSubscriptionInfoListRefresh(int);
@@ -13402,8 +13408,8 @@ package android.telephony {
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDefaultVoiceSubscriptionId(int);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean);
+ method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setSubscriptionUserHandle(int, @Nullable android.os.UserHandle);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean);
- method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle);
field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI;
field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI;
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index cc4650a7df71..48638d1fdff4 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -31,6 +31,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
+import android.os.BundleMerger;
import android.os.PowerExemptionManager;
import android.os.PowerExemptionManager.ReasonCode;
import android.os.PowerExemptionManager.TempAllowListType;
@@ -67,6 +68,7 @@ public class BroadcastOptions extends ComponentOptions {
private @Nullable IntentFilter mRemoveMatchingFilter;
private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
private @Nullable String mDeliveryGroupKey;
+ private @Nullable BundleMerger mDeliveryGroupExtrasMerger;
/**
* Change ID which is invalid.
@@ -218,6 +220,12 @@ public class BroadcastOptions extends ComponentOptions {
"android:broadcast.deliveryGroupKey";
/**
+ * Corresponds to {@link #setDeliveryGroupExtrasMerger(BundleMerger)}.
+ */
+ private static final String KEY_DELIVERY_GROUP_EXTRAS_MERGER =
+ "android:broadcast.deliveryGroupExtrasMerger";
+
+ /**
* The list of delivery group policies which specify how multiple broadcasts belonging to
* the same delivery group has to be handled.
* @hide
@@ -225,6 +233,7 @@ public class BroadcastOptions extends ComponentOptions {
@IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = {
DELIVERY_GROUP_POLICY_ALL,
DELIVERY_GROUP_POLICY_MOST_RECENT,
+ DELIVERY_GROUP_POLICY_MERGED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DeliveryGroupPolicy {}
@@ -235,6 +244,7 @@ public class BroadcastOptions extends ComponentOptions {
*
* @hide
*/
+ @SystemApi
public static final int DELIVERY_GROUP_POLICY_ALL = 0;
/**
@@ -243,8 +253,17 @@ public class BroadcastOptions extends ComponentOptions {
*
* @hide
*/
+ @SystemApi
public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1;
+ /**
+ * Delivery group policy that indicates that the extras data from the broadcasts in the
+ * delivery group need to be merged into a single broadcast and the rest can be dropped.
+ *
+ * @hide
+ */
+ public static final int DELIVERY_GROUP_POLICY_MERGED = 2;
+
public static BroadcastOptions makeBasic() {
BroadcastOptions opts = new BroadcastOptions();
return opts;
@@ -295,6 +314,8 @@ public class BroadcastOptions extends ComponentOptions {
mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
DELIVERY_GROUP_POLICY_ALL);
mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY);
+ mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER,
+ BundleMerger.class);
}
/**
@@ -724,16 +745,35 @@ public class BroadcastOptions extends ComponentOptions {
*
* @hide
*/
+ @SystemApi
public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) {
mDeliveryGroupPolicy = policy;
}
- /** @hide */
+ /**
+ * Get the delivery group policy for this broadcast that specifies how multiple broadcasts
+ * belonging to the same delivery group has to be handled.
+ *
+ * @hide
+ */
+ @SystemApi
public @DeliveryGroupPolicy int getDeliveryGroupPolicy() {
return mDeliveryGroupPolicy;
}
/**
+ * Clears any previously set delivery group policies using
+ * {@link #setDeliveryGroupKey(String, String)} and resets the delivery group policy to
+ * the default value ({@link #DELIVERY_GROUP_POLICY_ALL}).
+ *
+ * @hide
+ */
+ @SystemApi
+ public void clearDeliveryGroupPolicy() {
+ mDeliveryGroupPolicy = DELIVERY_GROUP_POLICY_ALL;
+ }
+
+ /**
* Set namespace and key to identify the delivery group that this broadcast belongs to.
* If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be
* used to identify the delivery group.
@@ -754,12 +794,35 @@ public class BroadcastOptions extends ComponentOptions {
}
/**
+ * Set the {@link BundleMerger} that specifies how to merge the extras data from
+ * broadcasts in a delivery group.
+ *
+ * <p>Note that this value will be ignored if the delivery group policy is not set as
+ * {@link #DELIVERY_GROUP_POLICY_MERGED}.
+ *
+ * @hide
+ */
+ public void setDeliveryGroupExtrasMerger(@NonNull BundleMerger extrasMerger) {
+ Preconditions.checkNotNull(extrasMerger);
+ mDeliveryGroupExtrasMerger = extrasMerger;
+ }
+
+ /** @hide */
+ public @Nullable BundleMerger getDeliveryGroupExtrasMerger() {
+ return mDeliveryGroupExtrasMerger;
+ }
+
+ /**
* 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.
* Note that the returned Bundle is still owned by the BroadcastOptions
* object; you must not modify it, but can supply it to the sendBroadcast
* methods that take an options Bundle.
+ *
+ * @throws IllegalStateException if the broadcast option values are inconsistent. For example,
+ * if the delivery group policy is specified as "MERGED" but no
+ * extras merger is supplied.
*/
@Override
public Bundle toBundle() {
@@ -810,6 +873,15 @@ public class BroadcastOptions extends ComponentOptions {
if (mDeliveryGroupKey != null) {
b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey);
}
+ if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) {
+ if (mDeliveryGroupExtrasMerger != null) {
+ b.putParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER,
+ mDeliveryGroupExtrasMerger);
+ } else {
+ throw new IllegalStateException("Extras merger cannot be empty "
+ + "when delivery group policy is 'MERGED'");
+ }
+ }
return b.isEmpty() ? null : b;
}
}
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 08a6b8c4e135..aaa3d21a0b25 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -173,6 +173,7 @@ import android.os.IThermalService;
import android.os.IUserManager;
import android.os.IncidentManager;
import android.os.PerformanceHintManager;
+import android.os.PermissionEnforcer;
import android.os.PowerManager;
import android.os.RecoverySystem;
import android.os.ServiceManager;
@@ -1366,6 +1367,14 @@ public final class SystemServiceRegistry {
return new PermissionCheckerManager(ctx.getOuterContext());
}});
+ registerService(Context.PERMISSION_ENFORCER_SERVICE, PermissionEnforcer.class,
+ new CachedServiceFetcher<PermissionEnforcer>() {
+ @Override
+ public PermissionEnforcer createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ return new PermissionEnforcer(ctx.getOuterContext());
+ }});
+
registerService(Context.DYNAMIC_SYSTEM_SERVICE, DynamicSystemManager.class,
new CachedServiceFetcher<DynamicSystemManager>() {
@Override
diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java
index bf466116009b..de2ba44ca393 100644
--- a/core/java/android/content/ClipDescription.java
+++ b/core/java/android/content/ClipDescription.java
@@ -139,21 +139,28 @@ public class ClipDescription implements Parcelable {
* password or credit card number.
* <p>
* Type: boolean
- * </p>
* <p>
* This extra can be used to indicate that a ClipData contains sensitive information that
* should be redacted or hidden from view until a user takes explicit action to reveal it
* (e.g., by pasting).
- * </p>
* <p>
* Adding this extra does not change clipboard behavior or add additional security to
* the ClipData. Its purpose is essentially a rendering hint from the source application,
* asking that the data within be obfuscated or redacted, unless the user has taken action
* to make it visible.
- * </p>
*/
public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE";
+ /** Indicates that a ClipData's source is a remote device.
+ * <p>
+ * Type: boolean
+ * <p>
+ * This extra can be used to indicate that a ClipData comes from a separate device rather
+ * than being local. It is a rendering hint that can be used to take different behavior
+ * based on the source device of copied data.
+ */
+ public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE";
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(value =
diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java
index 5f859846a5c1..f12e971afb1f 100644
--- a/core/java/android/content/ComponentName.java
+++ b/core/java/android/content/ComponentName.java
@@ -314,17 +314,14 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co
*/
@Override
public boolean equals(@Nullable Object obj) {
- try {
- if (obj != null) {
- ComponentName other = (ComponentName)obj;
- // Note: no null checks, because mPackage and mClass can
- // never be null.
- return mPackage.equals(other.mPackage)
- && mClass.equals(other.mClass);
- }
- } catch (ClassCastException e) {
+ if (obj instanceof ComponentName) {
+ ComponentName other = (ComponentName) obj;
+ // mPackage and mClass can never be null.
+ return mPackage.equals(other.mPackage)
+ && mClass.equals(other.mClass);
+ } else {
+ return false;
}
- return false;
}
@Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index d65210b8a0bc..cbc1789b56fc 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5142,6 +5142,14 @@ public abstract class Context {
public static final String PERMISSION_CHECKER_SERVICE = "permission_checker";
/**
+ * Official published name of the (internal) permission enforcer service.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ public static final String PERMISSION_ENFORCER_SERVICE = "permission_enforcer";
+
+ /**
* Use with {@link #getSystemService(String) to retrieve an
* {@link android.apphibernation.AppHibernationManager}} for
* communicating with the hibernation service.
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 43fa61782bf6..f2ebec6306f8 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -49,6 +49,7 @@ import android.graphics.Rect;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.BundleMerger;
import android.os.IBinder;
import android.os.IncidentManager;
import android.os.Parcel;
@@ -11072,6 +11073,20 @@ public class Intent implements Parcelable, Cloneable {
}
/**
+ * Merge the extras data in this intent with that of other supplied intent using the
+ * strategy specified using {@code extrasMerger}.
+ *
+ * <p> Note the extras data in this intent is treated as the {@code first} param
+ * and the extras data in {@code other} intent is treated as the {@code last} param
+ * when using the passed in {@link BundleMerger} object.
+ *
+ * @hide
+ */
+ public void mergeExtras(@NonNull Intent other, @NonNull BundleMerger extrasMerger) {
+ mExtras = extrasMerger.merge(mExtras, other.mExtras);
+ }
+
+ /**
* Wrapper class holding an Intent and implementing comparisons on it for
* the purpose of filtering. The class implements its
* {@link #equals equals()} and {@link #hashCode hashCode()} methods as
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index d3a6323230a5..26435586f2d7 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -562,7 +562,7 @@ public class Binder implements IBinder {
*
* @hide
*/
- @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
public final native void markVintfStability();
/**
diff --git a/core/java/android/os/BundleMerger.java b/core/java/android/os/BundleMerger.java
new file mode 100644
index 000000000000..51bd4ea75005
--- /dev/null
+++ b/core/java/android/os/BundleMerger.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.BinaryOperator;
+
+/**
+ * Configured rules for merging two {@link Bundle} instances.
+ * <p>
+ * By default, values from both {@link Bundle} instances are blended together on
+ * a key-wise basis, and conflicting value definitions for a key are dropped.
+ * <p>
+ * Nuanced strategies for handling conflicting value definitions can be applied
+ * using {@link #setMergeStrategy(String, int)} and
+ * {@link #setDefaultMergeStrategy(int)}.
+ * <p>
+ * When conflicting values have <em>inconsistent</em> data types (such as trying
+ * to merge a {@link String} and a {@link Integer}), both conflicting values are
+ * rejected and the key becomes undefined, regardless of the requested strategy.
+ *
+ * @hide
+ */
+public class BundleMerger implements Parcelable {
+ private static final String TAG = "BundleMerger";
+
+ private @Strategy int mDefaultStrategy = STRATEGY_REJECT;
+
+ private final ArrayMap<String, Integer> mStrategies = new ArrayMap<>();
+
+ /**
+ * Merge strategy that rejects both conflicting values.
+ */
+ public static final int STRATEGY_REJECT = 0;
+
+ /**
+ * Merge strategy that selects the first of conflicting values.
+ */
+ public static final int STRATEGY_FIRST = 1;
+
+ /**
+ * Merge strategy that selects the last of conflicting values.
+ */
+ public static final int STRATEGY_LAST = 2;
+
+ /**
+ * Merge strategy that selects the "minimum" of conflicting values which are
+ * {@link Comparable} with each other.
+ */
+ public static final int STRATEGY_COMPARABLE_MIN = 3;
+
+ /**
+ * Merge strategy that selects the "maximum" of conflicting values which are
+ * {@link Comparable} with each other.
+ */
+ public static final int STRATEGY_COMPARABLE_MAX = 4;
+
+ /**
+ * Merge strategy that numerically adds both conflicting values.
+ */
+ public static final int STRATEGY_NUMBER_ADD = 5;
+
+ /**
+ * Merge strategy that numerically increments the first conflicting value by
+ * {@code 1} and ignores the last conflicting value.
+ */
+ public static final int STRATEGY_NUMBER_INCREMENT_FIRST = 6;
+
+ /**
+ * Merge strategy that combines conflicting values using a boolean "and"
+ * operation.
+ */
+ public static final int STRATEGY_BOOLEAN_AND = 7;
+
+ /**
+ * Merge strategy that combines conflicting values using a boolean "or"
+ * operation.
+ */
+ public static final int STRATEGY_BOOLEAN_OR = 8;
+
+ /**
+ * Merge strategy that combines two conflicting array values by appending
+ * the last array after the first array.
+ */
+ public static final int STRATEGY_ARRAY_APPEND = 9;
+
+ /**
+ * Merge strategy that combines two conflicting {@link ArrayList} values by
+ * appending the last {@link ArrayList} after the first {@link ArrayList}.
+ */
+ public static final int STRATEGY_ARRAY_LIST_APPEND = 10;
+
+ @IntDef(flag = false, prefix = { "STRATEGY_" }, value = {
+ STRATEGY_REJECT,
+ STRATEGY_FIRST,
+ STRATEGY_LAST,
+ STRATEGY_COMPARABLE_MIN,
+ STRATEGY_COMPARABLE_MAX,
+ STRATEGY_NUMBER_ADD,
+ STRATEGY_NUMBER_INCREMENT_FIRST,
+ STRATEGY_BOOLEAN_AND,
+ STRATEGY_BOOLEAN_OR,
+ STRATEGY_ARRAY_APPEND,
+ STRATEGY_ARRAY_LIST_APPEND,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Strategy {}
+
+ /**
+ * Create a empty set of rules for merging two {@link Bundle} instances.
+ */
+ public BundleMerger() {
+ }
+
+ private BundleMerger(@NonNull Parcel in) {
+ mDefaultStrategy = in.readInt();
+ final int N = in.readInt();
+ for (int i = 0; i < N; i++) {
+ mStrategies.put(in.readString(), in.readInt());
+ }
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mDefaultStrategy);
+ final int N = mStrategies.size();
+ out.writeInt(N);
+ for (int i = 0; i < N; i++) {
+ out.writeString(mStrategies.keyAt(i));
+ out.writeInt(mStrategies.valueAt(i));
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Configure the default merge strategy to be used when there isn't a
+ * more-specific strategy defined for a particular key via
+ * {@link #setMergeStrategy(String, int)}.
+ */
+ public void setDefaultMergeStrategy(@Strategy int strategy) {
+ mDefaultStrategy = strategy;
+ }
+
+ /**
+ * Configure the merge strategy to be used for the given key.
+ * <p>
+ * Subsequent calls for the same key will overwrite any previously
+ * configured strategy.
+ */
+ public void setMergeStrategy(@NonNull String key, @Strategy int strategy) {
+ mStrategies.put(key, strategy);
+ }
+
+ /**
+ * Return the merge strategy to be used for the given key, as defined by
+ * {@link #setMergeStrategy(String, int)}.
+ * <p>
+ * If no specific strategy has been configured for the given key, this
+ * returns {@link #setDefaultMergeStrategy(int)}.
+ */
+ public @Strategy int getMergeStrategy(@NonNull String key) {
+ return (int) mStrategies.getOrDefault(key, mDefaultStrategy);
+ }
+
+ /**
+ * Return a {@link BinaryOperator} which applies the strategies configured
+ * in this object to merge the two given {@link Bundle} arguments.
+ */
+ public BinaryOperator<Bundle> asBinaryOperator() {
+ return this::merge;
+ }
+
+ /**
+ * Apply the strategies configured in this object to merge the two given
+ * {@link Bundle} arguments.
+ *
+ * @return the merged {@link Bundle} result. If one argument is {@code null}
+ * it will return the other argument. If both arguments are null it
+ * will return {@code null}.
+ */
+ @SuppressWarnings("deprecation")
+ public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) {
+ if (first == null && last == null) {
+ return null;
+ }
+ if (first == null) {
+ first = Bundle.EMPTY;
+ }
+ if (last == null) {
+ last = Bundle.EMPTY;
+ }
+
+ // Start by bulk-copying all values without attempting to unpack any
+ // custom parcelables; we'll circle back to handle conflicts below
+ final Bundle res = new Bundle();
+ res.putAll(first);
+ res.putAll(last);
+
+ final ArraySet<String> conflictingKeys = new ArraySet<>();
+ conflictingKeys.addAll(first.keySet());
+ conflictingKeys.retainAll(last.keySet());
+ for (int i = 0; i < conflictingKeys.size(); i++) {
+ final String key = conflictingKeys.valueAt(i);
+ final int strategy = getMergeStrategy(key);
+ final Object firstValue = first.get(key);
+ final Object lastValue = last.get(key);
+ try {
+ res.putObject(key, merge(strategy, firstValue, lastValue));
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and "
+ + lastValue + " using strategy " + strategy, e);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Merge the two given values. If only one of the values is defined, it
+ * always wins, otherwise the given strategy is applied.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static @Nullable Object merge(@Strategy int strategy,
+ @Nullable Object first, @Nullable Object last) {
+ if (first == null) return last;
+ if (last == null) return first;
+
+ if (first.getClass() != last.getClass()) {
+ throw new IllegalArgumentException("Merging requires consistent classes; first "
+ + first.getClass() + " last " + last.getClass());
+ }
+
+ switch (strategy) {
+ case STRATEGY_REJECT:
+ // Only actually reject when the values are different
+ if (Objects.deepEquals(first, last)) {
+ return first;
+ } else {
+ return null;
+ }
+ case STRATEGY_FIRST:
+ return first;
+ case STRATEGY_LAST:
+ return last;
+ case STRATEGY_COMPARABLE_MIN:
+ return comparableMin(first, last);
+ case STRATEGY_COMPARABLE_MAX:
+ return comparableMax(first, last);
+ case STRATEGY_NUMBER_ADD:
+ return numberAdd(first, last);
+ case STRATEGY_NUMBER_INCREMENT_FIRST:
+ return numberIncrementFirst(first, last);
+ case STRATEGY_BOOLEAN_AND:
+ return booleanAnd(first, last);
+ case STRATEGY_BOOLEAN_OR:
+ return booleanOr(first, last);
+ case STRATEGY_ARRAY_APPEND:
+ return arrayAppend(first, last);
+ case STRATEGY_ARRAY_LIST_APPEND:
+ return arrayListAppend(first, last);
+ default:
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) {
+ return ((Comparable<Object>) first).compareTo(last) < 0 ? first : last;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) {
+ return ((Comparable<Object>) first).compareTo(last) >= 0 ? first : last;
+ }
+
+ private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) {
+ if (first instanceof Integer) {
+ return ((Integer) first) + ((Integer) last);
+ } else if (first instanceof Long) {
+ return ((Long) first) + ((Long) last);
+ } else if (first instanceof Float) {
+ return ((Float) first) + ((Float) last);
+ } else if (first instanceof Double) {
+ return ((Double) first) + ((Double) last);
+ } else {
+ throw new IllegalArgumentException("Unable to add " + first.getClass());
+ }
+ }
+
+ private static @NonNull Number numberIncrementFirst(@NonNull Object first,
+ @NonNull Object last) {
+ if (first instanceof Integer) {
+ return ((Integer) first) + 1;
+ } else if (first instanceof Long) {
+ return ((Long) first) + 1L;
+ } else {
+ throw new IllegalArgumentException("Unable to add " + first.getClass());
+ }
+ }
+
+ private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) {
+ return ((Boolean) first) && ((Boolean) last);
+ }
+
+ private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) {
+ return ((Boolean) first) || ((Boolean) last);
+ }
+
+ private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) {
+ if (!first.getClass().isArray()) {
+ throw new IllegalArgumentException("Unable to append " + first.getClass());
+ }
+ final Class<?> clazz = first.getClass().getComponentType();
+ final int firstLength = Array.getLength(first);
+ final int lastLength = Array.getLength(last);
+ final Object res = Array.newInstance(clazz, firstLength + lastLength);
+ System.arraycopy(first, 0, res, 0, firstLength);
+ System.arraycopy(last, 0, res, firstLength, lastLength);
+ return res;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) {
+ if (!(first instanceof ArrayList)) {
+ throw new IllegalArgumentException("Unable to append " + first.getClass());
+ }
+ final ArrayList<Object> firstList = (ArrayList<Object>) first;
+ final ArrayList<Object> lastList = (ArrayList<Object>) last;
+ final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size());
+ res.addAll(firstList);
+ res.addAll(lastList);
+ return res;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR =
+ new Parcelable.Creator<BundleMerger>() {
+ @Override
+ public BundleMerger createFromParcel(Parcel in) {
+ return new BundleMerger(in);
+ }
+
+ @Override
+ public BundleMerger[] newArray(int size) {
+ return new BundleMerger[size];
+ }
+ };
+}
diff --git a/core/java/android/os/PermissionEnforcer.java b/core/java/android/os/PermissionEnforcer.java
new file mode 100644
index 000000000000..221e89a6a76f
--- /dev/null
+++ b/core/java/android/os/PermissionEnforcer.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.content.AttributionSource;
+import android.content.Context;
+import android.content.PermissionChecker;
+import android.permission.PermissionCheckerManager;
+
+/**
+ * PermissionEnforcer check permissions for AIDL-generated services which use
+ * the @EnforcePermission annotation.
+ *
+ * <p>AIDL services may be annotated with @EnforcePermission which will trigger
+ * the generation of permission check code. This generated code relies on
+ * PermissionEnforcer to validate the permissions. The methods available are
+ * purposely similar to the AIDL annotation syntax.
+ *
+ * @see android.permission.PermissionManager
+ *
+ * @hide
+ */
+@SystemService(Context.PERMISSION_ENFORCER_SERVICE)
+public class PermissionEnforcer {
+
+ private final Context mContext;
+
+ /** Protected constructor. Allows subclasses to instantiate an object
+ * without using a Context.
+ */
+ protected PermissionEnforcer() {
+ mContext = null;
+ }
+
+ /** Constructor, prefer using the fromContext static method when possible */
+ public PermissionEnforcer(@NonNull Context context) {
+ mContext = context;
+ }
+
+ @PermissionCheckerManager.PermissionResult
+ protected int checkPermission(@NonNull String permission, @NonNull AttributionSource source) {
+ return PermissionChecker.checkPermissionForDataDelivery(
+ mContext, permission, PermissionChecker.PID_UNKNOWN, source, "" /* message */);
+ }
+
+ public void enforcePermission(@NonNull String permission, @NonNull
+ AttributionSource source) throws SecurityException {
+ int result = checkPermission(permission, source);
+ if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Access denied, requires: " + permission);
+ }
+ }
+
+ public void enforcePermissionAllOf(@NonNull String[] permissions,
+ @NonNull AttributionSource source) throws SecurityException {
+ for (String permission : permissions) {
+ int result = checkPermission(permission, source);
+ if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Access denied, requires: allOf={"
+ + String.join(", ", permissions) + "}");
+ }
+ }
+ }
+
+ public void enforcePermissionAnyOf(@NonNull String[] permissions,
+ @NonNull AttributionSource source) throws SecurityException {
+ for (String permission : permissions) {
+ int result = checkPermission(permission, source);
+ if (result == PermissionCheckerManager.PERMISSION_GRANTED) {
+ return;
+ }
+ }
+ throw new SecurityException("Access denied, requires: anyOf={"
+ + String.join(", ", permissions) + "}");
+ }
+
+ /**
+ * Returns a new PermissionEnforcer based on a Context.
+ *
+ * @hide
+ */
+ public static PermissionEnforcer fromContext(@NonNull Context context) {
+ return context.getSystemService(PermissionEnforcer.class);
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cd2bbebf3d4d..4502eec9fe4f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7135,7 +7135,7 @@ public final class Settings {
* Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"
* where imeId is ComponentName and subtype is int32.
*/
- @Readable
+ @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
public static final String ENABLED_INPUT_METHODS = "enabled_input_methods";
/**
@@ -7144,7 +7144,7 @@ public final class Settings {
* by ':'.
* @hide
*/
- @Readable
+ @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU)
public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods";
/**
diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java
index 0a3e6b1cff38..517d98222093 100755
--- a/core/java/android/util/DisplayMetrics.java
+++ b/core/java/android/util/DisplayMetrics.java
@@ -174,6 +174,14 @@ public class DisplayMetrics {
* This is not a density that applications should target, instead relying
* on the system to scale their {@link #DENSITY_XXXHIGH} assets for them.
*/
+ public static final int DENSITY_520 = 520;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them.
+ */
public static final int DENSITY_560 = 560;
/**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 5e836ef186d4..ff4588a7bc9b 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -4970,7 +4970,7 @@ public final class ViewRootImpl implements ViewParent,
}
void reportKeepClearAreasChanged() {
- if (!mHasPendingKeepClearAreaChange) {
+ if (!mHasPendingKeepClearAreaChange || mView == null) {
return;
}
mHasPendingKeepClearAreaChange = false;
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index 5099dd20a6d5..9e4f63cab86c 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -977,6 +977,7 @@ message UserControllerProto {
optional int32 profile = 2;
}
repeated UserProfile user_profile_group_ids = 4;
+ repeated int32 visible_users_array = 5;
}
// sync with com.android.server.am.AppTimeTracker.java
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
new file mode 100644
index 000000000000..fe3ab625bacc
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class TunerAdapterTest {
+
+ private static final int CALLBACK_TIMEOUT_MS = 30_000;
+ private static final int AM_LOWER_LIMIT_KHZ = 150;
+
+ private static final RadioManager.BandConfig TEST_BAND_CONFIG = createBandConfig();
+
+ private static final ProgramSelector.Identifier FM_IDENTIFIER =
+ new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+ /* value= */ 94300);
+ private static final ProgramSelector FM_SELECTOR =
+ new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER,
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+ private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo();
+
+ private RadioTuner mRadioTuner;
+ private ITunerCallback mTunerCallback;
+
+ @Mock
+ private IRadioService mRadioServiceMock;
+ @Mock
+ private Context mContextMock;
+ @Mock
+ private ITuner mTunerMock;
+ @Mock
+ private RadioTuner.Callback mCallbackMock;
+
+ @Before
+ public void setUp() throws Exception {
+ RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
+
+ doAnswer(invocation -> {
+ mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
+ return mTunerMock;
+ }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+
+ doAnswer(invocation -> {
+ ProgramSelector program = (ProgramSelector) invocation.getArguments()[0];
+ if (program.getPrimaryId().getType()
+ != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) {
+ throw new IllegalArgumentException();
+ }
+ if (program.getPrimaryId().getValue() < AM_LOWER_LIMIT_KHZ) {
+ mTunerCallback.onTuneFailed(RadioManager.STATUS_BAD_VALUE, program);
+ } else {
+ mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+ }
+ return RadioManager.STATUS_OK;
+ }).when(mTunerMock).tune(any());
+
+ mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, TEST_BAND_CONFIG,
+ /* withAudio= */ true, mCallbackMock, /* handler= */ null);
+ }
+
+ @After
+ public void cleanUp() throws Exception {
+ mRadioTuner.close();
+ }
+
+ @Test
+ public void close_forTunerAdapter() throws Exception {
+ mRadioTuner.close();
+
+ verify(mTunerMock).close();
+ }
+
+ @Test
+ public void setConfiguration_forTunerAdapter() throws Exception {
+ int status = mRadioTuner.setConfiguration(TEST_BAND_CONFIG);
+
+ verify(mTunerMock).setConfiguration(TEST_BAND_CONFIG);
+ assertWithMessage("Status for setting configuration")
+ .that(status).isEqualTo(RadioManager.STATUS_OK);
+ }
+
+ @Test
+ public void getConfiguration_forTunerAdapter() throws Exception {
+ when(mTunerMock.getConfiguration()).thenReturn(TEST_BAND_CONFIG);
+ RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1];
+
+ int status = mRadioTuner.getConfiguration(bandConfigs);
+
+ assertWithMessage("Status for getting configuration")
+ .that(status).isEqualTo(RadioManager.STATUS_OK);
+ assertWithMessage("Configuration obtained from radio tuner")
+ .that(bandConfigs[0]).isEqualTo(TEST_BAND_CONFIG);
+ }
+
+ @Test
+ public void setMute_forTunerAdapter() {
+ int status = mRadioTuner.setMute(/* mute= */ true);
+
+ assertWithMessage("Status for setting mute")
+ .that(status).isEqualTo(RadioManager.STATUS_OK);
+ }
+
+ @Test
+ public void getMute_forTunerAdapter() throws Exception {
+ when(mTunerMock.isMuted()).thenReturn(true);
+
+ boolean muteStatus = mRadioTuner.getMute();
+
+ assertWithMessage("Mute status").that(muteStatus).isTrue();
+ }
+
+ @Test
+ public void step_forTunerAdapter_succeeds() throws Exception {
+ doAnswer(invocation -> {
+ mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+ return RadioManager.STATUS_OK;
+ }).when(mTunerMock).step(anyBoolean(), anyBoolean());
+
+ int scanStatus = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
+
+ verify(mTunerMock).step(/* skipSubChannel= */ true, /* skipSubChannel= */ false);
+ assertWithMessage("Status for stepping")
+ .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+ }
+
+ @Test
+ public void seek_forTunerAdapter_succeeds() throws Exception {
+ doAnswer(invocation -> {
+ mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+ return RadioManager.STATUS_OK;
+ }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+
+ int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
+
+ verify(mTunerMock).scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+ assertWithMessage("Status for seeking")
+ .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+ }
+
+ @Test
+ public void seek_forTunerAdapter_invokesOnErrorWhenTimeout() throws Exception {
+ doAnswer(invocation -> {
+ mTunerCallback.onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+ return RadioManager.STATUS_OK;
+ }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+
+ mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+ }
+
+ @Test
+ public void tune_withChannelsForTunerAdapter_succeeds() {
+ int status = mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0);
+
+ assertWithMessage("Status for tuning with channel and sub-channel")
+ .that(status).isEqualTo(RadioManager.STATUS_OK);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+ }
+
+ @Test
+ public void tune_withValidSelectorForTunerAdapter_succeeds() throws Exception {
+ mRadioTuner.tune(FM_SELECTOR);
+
+ verify(mTunerMock).tune(FM_SELECTOR);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+ }
+
+
+ @Test
+ public void tune_withInvalidSelectorForTunerAdapter_invokesOnTuneFailed() {
+ ProgramSelector invalidSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
+ new ProgramSelector.Identifier(
+ ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 100),
+ /* secondaryIds= */ null, /* vendorIds= */ null);
+
+ mRadioTuner.tune(invalidSelector);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+ .onTuneFailed(RadioManager.STATUS_BAD_VALUE, invalidSelector);
+ }
+
+ @Test
+ public void cancel_forTunerAdapter() throws Exception {
+ mRadioTuner.tune(FM_SELECTOR);
+
+ mRadioTuner.cancel();
+
+ verify(mTunerMock).cancel();
+ }
+
+ @Test
+ public void cancelAnnouncement_forTunerAdapter() throws Exception {
+ mRadioTuner.cancelAnnouncement();
+
+ verify(mTunerMock).cancelAnnouncement();
+ }
+
+ @Test
+ public void getProgramInfo_beforeProgramInfoSetForTunerAdapter() {
+ RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1];
+
+ int status = mRadioTuner.getProgramInformation(programInfoArray);
+
+ assertWithMessage("Status for getting null program info")
+ .that(status).isEqualTo(RadioManager.STATUS_INVALID_OPERATION);
+ }
+
+ @Test
+ public void getProgramInfo_afterTuneForTunerAdapter() {
+ mRadioTuner.tune(FM_SELECTOR);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+ RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1];
+
+ int status = mRadioTuner.getProgramInformation(programInfoArray);
+
+ assertWithMessage("Status for getting program info")
+ .that(status).isEqualTo(RadioManager.STATUS_OK);
+ assertWithMessage("Program info obtained from radio tuner")
+ .that(programInfoArray[0]).isEqualTo(FM_PROGRAM_INFO);
+ }
+
+ @Test
+ public void getMetadataImage_forTunerAdapter() throws Exception {
+ Bitmap bitmapExpected = Mockito.mock(Bitmap.class);
+ when(mTunerMock.getImage(anyInt())).thenReturn(bitmapExpected);
+ int imageId = 1;
+
+ Bitmap image = mRadioTuner.getMetadataImage(/* id= */ imageId);
+
+ assertWithMessage("Image obtained from id %s", imageId)
+ .that(image).isEqualTo(bitmapExpected);
+ }
+
+ @Test
+ public void isAnalogForced_forTunerAdapter() throws Exception {
+ when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true);
+
+ boolean isAnalogForced = mRadioTuner.isAnalogForced();
+
+ assertWithMessage("Forced analog playback switch")
+ .that(isAnalogForced).isTrue();
+ }
+
+ @Test
+ public void setAnalogForced_forTunerAdapter() throws Exception {
+ boolean analogForced = true;
+
+ mRadioTuner.setAnalogForced(analogForced);
+
+ verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, analogForced);
+ }
+
+ @Test
+ public void isConfigFlagSupported_forTunerAdapter() throws Exception {
+ when(mTunerMock.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING))
+ .thenReturn(true);
+
+ boolean dabFmSoftLinking =
+ mRadioTuner.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING);
+
+ assertWithMessage("Support for DAB-DAB linking config flag")
+ .that(dabFmSoftLinking).isTrue();
+ }
+
+ @Test
+ public void isConfigFlagSet_forTunerAdapter() throws Exception {
+ when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING))
+ .thenReturn(true);
+
+ boolean dabFmSoftLinking =
+ mRadioTuner.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING);
+
+ assertWithMessage("DAB-FM soft linking config flag")
+ .that(dabFmSoftLinking).isTrue();
+ }
+
+ @Test
+ public void setConfigFlag_forTunerAdapter() throws Exception {
+ boolean dabFmLinking = true;
+
+ mRadioTuner.setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking);
+
+ verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking);
+ }
+
+ @Test
+ public void getParameters_forTunerAdapter() throws Exception {
+ List<String> parameterKeys = Arrays.asList("ParameterKeyMock");
+ Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
+ when(mTunerMock.getParameters(parameterKeys)).thenReturn(parameters);
+
+ assertWithMessage("Parameters obtained from radio tuner")
+ .that(mRadioTuner.getParameters(parameterKeys)).isEqualTo(parameters);
+ }
+
+ @Test
+ public void setParameters_forTunerAdapter() throws Exception {
+ Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock");
+ when(mTunerMock.setParameters(parameters)).thenReturn(parameters);
+
+ assertWithMessage("Parameters set for radio tuner")
+ .that(mRadioTuner.setParameters(parameters)).isEqualTo(parameters);
+ }
+
+ @Test
+ public void isAntennaConnected_forTunerAdapter() throws Exception {
+ mTunerCallback.onAntennaState(/* connected= */ false);
+
+ assertWithMessage("Antenna connection status")
+ .that(mRadioTuner.isAntennaConnected()).isFalse();
+ }
+
+ @Test
+ public void hasControl_forTunerAdapter() throws Exception {
+ when(mTunerMock.isClosed()).thenReturn(true);
+
+ assertWithMessage("Control on tuner").that(mRadioTuner.hasControl()).isFalse();
+ }
+
+ @Test
+ public void onConfigurationChanged_forTunerCallbackAdapter() throws Exception {
+ mTunerCallback.onConfigurationChanged(TEST_BAND_CONFIG);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+ .onConfigurationChanged(TEST_BAND_CONFIG);
+ }
+
+ @Test
+ public void onTrafficAnnouncement_forTunerCallbackAdapter() throws Exception {
+ mTunerCallback.onTrafficAnnouncement(/* active= */ true);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+ .onTrafficAnnouncement(/* active= */ true);
+ }
+
+ @Test
+ public void onEmergencyAnnouncement_forTunerCallbackAdapter() throws Exception {
+ mTunerCallback.onEmergencyAnnouncement(/* active= */ true);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+ .onEmergencyAnnouncement(/* active= */ true);
+ }
+
+ @Test
+ public void onBackgroundScanAvailabilityChange_forTunerCallbackAdapter() throws Exception {
+ mTunerCallback.onBackgroundScanAvailabilityChange(/* isAvailable= */ false);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+ .onBackgroundScanAvailabilityChange(/* isAvailable= */ false);
+ }
+
+ @Test
+ public void onProgramListChanged_forTunerCallbackAdapter() throws Exception {
+ mTunerCallback.onProgramListChanged();
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramListChanged();
+ }
+
+ @Test
+ public void onParametersUpdated_forTunerCallbackAdapter() throws Exception {
+ Map<String, String> parametersExpected = Map.of("ParameterKeyMock", "ParameterValueMock");
+
+ mTunerCallback.onParametersUpdated(parametersExpected);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onParametersUpdated(parametersExpected);
+ }
+
+ private static RadioManager.ProgramInfo createFmProgramInfo() {
+ return new RadioManager.ProgramInfo(FM_SELECTOR, FM_IDENTIFIER, FM_IDENTIFIER,
+ /* relatedContent= */ null, /* infoFlags= */ 0b110001,
+ /* signalQuality= */ 1, createRadioMetadata(), /* vendorInfo= */ null);
+ }
+
+ private static RadioManager.FmBandConfig createBandConfig() {
+ return new RadioManager.FmBandConfig(new RadioManager.FmBandDescriptor(
+ RadioManager.REGION_ITU_1, RadioManager.BAND_FM, /* lowerLimit= */ 87500,
+ /* upperLimit= */ 108000, /* spacing= */ 200, /* stereo= */ true,
+ /* rds= */ false, /* ta= */ false, /* af= */ false, /* es= */ false));
+ }
+
+ private static RadioMetadata createRadioMetadata() {
+ RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder();
+ return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistMock").build();
+ }
+}
diff --git a/core/tests/coretests/src/android/app/backup/OWNERS b/core/tests/coretests/src/android/app/backup/OWNERS
new file mode 100644
index 000000000000..53b6c78b3895
--- /dev/null
+++ b/core/tests/coretests/src/android/app/backup/OWNERS
@@ -0,0 +1 @@
+include /services/backup/OWNERS \ No newline at end of file
diff --git a/core/tests/coretests/src/android/os/BundleMergerTest.java b/core/tests/coretests/src/android/os/BundleMergerTest.java
new file mode 100644
index 000000000000..b7012ba66124
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BundleMergerTest.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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 static android.os.BundleMerger.STRATEGY_ARRAY_APPEND;
+import static android.os.BundleMerger.STRATEGY_ARRAY_LIST_APPEND;
+import static android.os.BundleMerger.STRATEGY_BOOLEAN_AND;
+import static android.os.BundleMerger.STRATEGY_BOOLEAN_OR;
+import static android.os.BundleMerger.STRATEGY_COMPARABLE_MAX;
+import static android.os.BundleMerger.STRATEGY_COMPARABLE_MIN;
+import static android.os.BundleMerger.STRATEGY_FIRST;
+import static android.os.BundleMerger.STRATEGY_LAST;
+import static android.os.BundleMerger.STRATEGY_NUMBER_ADD;
+import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST;
+import static android.os.BundleMerger.STRATEGY_REJECT;
+import static android.os.BundleMerger.merge;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class BundleMergerTest {
+ /**
+ * Strategies are only applied when there is an actual conflict; in the
+ * absence of conflict we pick whichever value is defined.
+ */
+ @Test
+ public void testNoConflict() throws Exception {
+ for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) {
+ assertEquals(null, merge(strategy, null, null));
+ assertEquals(10, merge(strategy, 10, null));
+ assertEquals(20, merge(strategy, null, 20));
+ }
+ }
+
+ /**
+ * Strategies are only applied to identical data types; if there are mixed
+ * types we always reject the two conflicting values.
+ */
+ @Test
+ public void testMixedTypes() throws Exception {
+ for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) {
+ final int finalStrategy = strategy;
+ assertThrows(Exception.class, () -> {
+ merge(finalStrategy, 10, "foo");
+ });
+ assertThrows(Exception.class, () -> {
+ merge(finalStrategy, List.of("foo"), "bar");
+ });
+ assertThrows(Exception.class, () -> {
+ merge(finalStrategy, new String[] { "foo" }, "bar");
+ });
+ assertThrows(Exception.class, () -> {
+ merge(finalStrategy, Integer.valueOf(10), Long.valueOf(10));
+ });
+ }
+ }
+
+ @Test
+ public void testStrategyReject() throws Exception {
+ assertEquals(null, merge(STRATEGY_REJECT, 10, 20));
+
+ // Identical values aren't technically a conflict, so they're passed
+ // through without being rejected
+ assertEquals(10, merge(STRATEGY_REJECT, 10, 10));
+ assertArrayEquals(new int[] {10},
+ (int[]) merge(STRATEGY_REJECT, new int[] {10}, new int[] {10}));
+ }
+
+ @Test
+ public void testStrategyFirst() throws Exception {
+ assertEquals(10, merge(STRATEGY_FIRST, 10, 20));
+ }
+
+ @Test
+ public void testStrategyLast() throws Exception {
+ assertEquals(20, merge(STRATEGY_LAST, 10, 20));
+ }
+
+ @Test
+ public void testStrategyComparableMin() throws Exception {
+ assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 10, 20));
+ assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 20, 10));
+ assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "a", "z"));
+ assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "z", "a"));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_COMPARABLE_MIN, new Binder(), new Binder());
+ });
+ }
+
+ @Test
+ public void testStrategyComparableMax() throws Exception {
+ assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 10, 20));
+ assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 20, 10));
+ assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "a", "z"));
+ assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "z", "a"));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_COMPARABLE_MAX, new Binder(), new Binder());
+ });
+ }
+
+ @Test
+ public void testStrategyNumberAdd() throws Exception {
+ assertEquals(30, merge(STRATEGY_NUMBER_ADD, 10, 20));
+ assertEquals(30, merge(STRATEGY_NUMBER_ADD, 20, 10));
+ assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 10L, 20L));
+ assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 20L, 10L));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_NUMBER_ADD, new Binder(), new Binder());
+ });
+ }
+
+ @Test
+ public void testStrategyNumberIncrementFirst() throws Exception {
+ assertEquals(11, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10, 20));
+ assertEquals(21, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20, 10));
+ assertEquals(11L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10L, 20L));
+ assertEquals(21L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20L, 10L));
+ }
+
+ @Test
+ public void testStrategyBooleanAnd() throws Exception {
+ assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, false));
+ assertEquals(false, merge(STRATEGY_BOOLEAN_AND, true, false));
+ assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, true));
+ assertEquals(true, merge(STRATEGY_BOOLEAN_AND, true, true));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_BOOLEAN_AND, "True!", "False?");
+ });
+ }
+
+ @Test
+ public void testStrategyBooleanOr() throws Exception {
+ assertEquals(false, merge(STRATEGY_BOOLEAN_OR, false, false));
+ assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, false));
+ assertEquals(true, merge(STRATEGY_BOOLEAN_OR, false, true));
+ assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, true));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_BOOLEAN_OR, "True!", "False?");
+ });
+ }
+
+ @Test
+ public void testStrategyArrayAppend() throws Exception {
+ assertArrayEquals(new int[] {},
+ (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {}));
+ assertArrayEquals(new int[] {10},
+ (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {}));
+ assertArrayEquals(new int[] {20},
+ (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {20}));
+ assertArrayEquals(new int[] {10, 20},
+ (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {20}));
+ assertArrayEquals(new int[] {10, 30, 20, 40},
+ (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10, 30}, new int[] {20, 40}));
+ assertArrayEquals(new String[] {"a", "b"},
+ (String[]) merge(STRATEGY_ARRAY_APPEND, new String[] {"a"}, new String[] {"b"}));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_ARRAY_APPEND, 10, 20);
+ });
+ }
+
+ @Test
+ public void testStrategyArrayListAppend() throws Exception {
+ assertEquals(arrayListOf(),
+ merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf()));
+ assertEquals(arrayListOf(10),
+ merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf()));
+ assertEquals(arrayListOf(20),
+ merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf(20)));
+ assertEquals(arrayListOf(10, 20),
+ merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf(20)));
+ assertEquals(arrayListOf(10, 30, 20, 40),
+ merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10, 30), arrayListOf(20, 40)));
+ assertEquals(arrayListOf("a", "b"),
+ merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf("a"), arrayListOf("b")));
+
+ assertThrows(Exception.class, () -> {
+ merge(STRATEGY_ARRAY_LIST_APPEND, 10, 20);
+ });
+ }
+
+ @Test
+ public void testMerge_Simple() throws Exception {
+ final BundleMerger merger = new BundleMerger();
+ final Bundle probe = new Bundle();
+ probe.putInt(Intent.EXTRA_INDEX, 42);
+
+ assertEquals(null, merger.merge(null, null));
+ assertEquals(probe.keySet(), merger.merge(probe, null).keySet());
+ assertEquals(probe.keySet(), merger.merge(null, probe).keySet());
+ assertEquals(probe.keySet(), merger.merge(probe, probe).keySet());
+ }
+
+ /**
+ * Verify that we can merge parcelables present in the base classpath, since
+ * everyone on the device will be able to unpack them.
+ */
+ @Test
+ public void testMerge_Parcelable_BCP() throws Exception {
+ final BundleMerger merger = new BundleMerger();
+ merger.setMergeStrategy(Intent.EXTRA_STREAM, STRATEGY_COMPARABLE_MIN);
+
+ Bundle a = new Bundle();
+ a.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.com"));
+ a = parcelAndUnparcel(a);
+
+ Bundle b = new Bundle();
+ b.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.net"));
+ b = parcelAndUnparcel(b);
+
+ assertEquals(Uri.parse("http://example.com"),
+ merger.merge(a, b).getParcelable(Intent.EXTRA_STREAM, Uri.class));
+ assertEquals(Uri.parse("http://example.com"),
+ merger.merge(b, a).getParcelable(Intent.EXTRA_STREAM, Uri.class));
+ }
+
+ /**
+ * Verify that we tiptoe around custom parcelables while still merging other
+ * known data types. Custom parcelables aren't in the base classpath, so not
+ * everyone on the device will be able to unpack them.
+ */
+ @Test
+ public void testMerge_Parcelable_Custom() throws Exception {
+ final BundleMerger merger = new BundleMerger();
+ merger.setMergeStrategy(Intent.EXTRA_INDEX, STRATEGY_NUMBER_ADD);
+
+ Bundle a = new Bundle();
+ a.putInt(Intent.EXTRA_INDEX, 10);
+ a.putString(Intent.EXTRA_CC, "foo@bar.com");
+ a.putParcelable(Intent.EXTRA_SUBJECT, new ExplodingParcelable());
+ a = parcelAndUnparcel(a);
+
+ Bundle b = new Bundle();
+ b.putInt(Intent.EXTRA_INDEX, 20);
+ a.putString(Intent.EXTRA_BCC, "foo@baz.com");
+ b.putParcelable(Intent.EXTRA_STREAM, new ExplodingParcelable());
+ b = parcelAndUnparcel(b);
+
+ Bundle ab = merger.merge(a, b);
+ assertEquals(Set.of(Intent.EXTRA_INDEX, Intent.EXTRA_CC, Intent.EXTRA_BCC,
+ Intent.EXTRA_SUBJECT, Intent.EXTRA_STREAM), ab.keySet());
+ assertEquals(30, ab.getInt(Intent.EXTRA_INDEX));
+ assertEquals("foo@bar.com", ab.getString(Intent.EXTRA_CC));
+ assertEquals("foo@baz.com", ab.getString(Intent.EXTRA_BCC));
+
+ // And finally, make sure that if we try unpacking one of our custom
+ // values that we actually explode
+ assertThrows(BadParcelableException.class, () -> {
+ ab.getParcelable(Intent.EXTRA_SUBJECT, ExplodingParcelable.class);
+ });
+ assertThrows(BadParcelableException.class, () -> {
+ ab.getParcelable(Intent.EXTRA_STREAM, ExplodingParcelable.class);
+ });
+ }
+
+ @Test
+ public void testMerge_PackageChanged() throws Exception {
+ final BundleMerger merger = new BundleMerger();
+ merger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, STRATEGY_ARRAY_APPEND);
+
+ final Bundle first = new Bundle();
+ first.putInt(Intent.EXTRA_UID, 10001);
+ first.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] {
+ "com.example.Foo",
+ });
+
+ final Bundle second = new Bundle();
+ second.putInt(Intent.EXTRA_UID, 10001);
+ second.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] {
+ "com.example.Bar",
+ "com.example.Baz",
+ });
+
+ final Bundle res = merger.merge(first, second);
+ assertEquals(10001, res.getInt(Intent.EXTRA_UID));
+ assertArrayEquals(new String[] {
+ "com.example.Foo", "com.example.Bar", "com.example.Baz",
+ }, res.getStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST));
+ }
+
+ /**
+ * Each event in isolation reports "zero events dropped", but if we need to
+ * merge them together, then we start incrementing.
+ */
+ @Test
+ public void testMerge_DropBox() throws Exception {
+ final BundleMerger merger = new BundleMerger();
+ merger.setMergeStrategy(DropBoxManager.EXTRA_TIME,
+ STRATEGY_COMPARABLE_MAX);
+ merger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT,
+ STRATEGY_NUMBER_INCREMENT_FIRST);
+
+ final long now = System.currentTimeMillis();
+ final Bundle a = new Bundle();
+ a.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode");
+ a.putLong(DropBoxManager.EXTRA_TIME, now);
+ a.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
+
+ final Bundle b = new Bundle();
+ b.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode");
+ b.putLong(DropBoxManager.EXTRA_TIME, now + 1000);
+ b.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
+
+ final Bundle c = new Bundle();
+ c.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode");
+ c.putLong(DropBoxManager.EXTRA_TIME, now + 2000);
+ c.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0);
+
+ final Bundle ab = merger.merge(a, b);
+ assertEquals("system_server_strictmode", ab.getString(DropBoxManager.EXTRA_TAG));
+ assertEquals(now + 1000, ab.getLong(DropBoxManager.EXTRA_TIME));
+ assertEquals(1, ab.getInt(DropBoxManager.EXTRA_DROPPED_COUNT));
+
+ final Bundle abc = merger.merge(ab, c);
+ assertEquals("system_server_strictmode", abc.getString(DropBoxManager.EXTRA_TAG));
+ assertEquals(now + 2000, abc.getLong(DropBoxManager.EXTRA_TIME));
+ assertEquals(2, abc.getInt(DropBoxManager.EXTRA_DROPPED_COUNT));
+ }
+
+ private static ArrayList<Object> arrayListOf(Object... values) {
+ final ArrayList<Object> res = new ArrayList<>(values.length);
+ for (Object value : values) {
+ res.add(value);
+ }
+ return res;
+ }
+
+ private static Bundle parcelAndUnparcel(Bundle input) {
+ final Parcel parcel = Parcel.obtain();
+ try {
+ input.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ return Bundle.CREATOR.createFromParcel(parcel);
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ /**
+ * Object that only offers to parcel itself; if something tries unparceling
+ * it, it will "explode" by throwing an exception.
+ * <p>
+ * Useful for verifying interactions that must leave unknown data in a
+ * parceled state.
+ */
+ public static class ExplodingParcelable implements Parcelable {
+ public ExplodingParcelable() {
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(42);
+ }
+
+ public static final Creator<ExplodingParcelable> CREATOR =
+ new Creator<ExplodingParcelable>() {
+ @Override
+ public ExplodingParcelable createFromParcel(Parcel in) {
+ throw new BadParcelableException("exploding!");
+ }
+
+ @Override
+ public ExplodingParcelable[] newArray(int size) {
+ throw new BadParcelableException("exploding!");
+ }
+ };
+ }
+}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 30c3d50ed8ad..df5f921f3a62 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -23,6 +23,10 @@
TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
<bool name="config_registerShellTaskOrganizerOnInit">true</bool>
+ <!-- Determines whether to register the shell transitions on init.
+ TODO(b/238217847): This config is temporary until we refactor the base WMComponent. -->
+ <bool name="config_registerShellTransitionsOnInit">true</bool>
+
<!-- Animation duration for PIP when entering. -->
<integer name="config_pipEnterAnimationDuration">425</integer>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
index 48c5f64e0dd4..bd2ea9c1f822 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java
@@ -41,7 +41,6 @@ import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.concurrent.Executor;
@@ -122,7 +121,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
/** Until all users are converted, we may have mixed-use (eg. Car). */
private boolean isUsingShellTransitions() {
- return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS;
+ return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled();
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
index 83335ac24799..07d501201105 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java
@@ -87,6 +87,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
// Note: Don't unregister handler since this is a singleton with lifetime bound to Shell
}
+ boolean isEnabled() {
+ return mTransitions.isRegistered();
+ }
+
/**
* Looks through the pending transitions for one matching `taskView`.
* @param taskView the pending transition should be for this.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 725b20525bf7..3972b592c448 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -671,10 +671,18 @@ public class BubbleController implements ConfigurationChangeListener {
return;
}
+ mAddedToWindowManager = false;
+ // Put on background for this binder call, was causing jank
+ mBackgroundExecutor.execute(() -> {
+ try {
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ } catch (IllegalArgumentException e) {
+ // Not sure if this happens in production, but was happening in tests
+ // (b/253647225)
+ e.printStackTrace();
+ }
+ });
try {
- mAddedToWindowManager = false;
- // Put on background for this binder call, was causing jank
- mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver));
if (mStackView != null) {
mWindowManager.removeView(mStackView);
mBubbleData.getOverflow().cleanUpExpandedState();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 64dbfbbb738d..8b8e192cf964 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -507,6 +507,10 @@ public abstract class WMShellBaseModule {
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler,
@ShellAnimationThread ShellExecutor animExecutor) {
+ if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
+ // TODO(b/238217847): Force override shell init if registration is disabled
+ shellInit = new ShellInit(mainExecutor);
+ }
return new Transitions(context, shellInit, shellController, organizer, pool,
displayController, mainExecutor, mainHandler, animExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index b9746e338ced..cbed4b5a501f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -115,8 +115,8 @@ public class PipSurfaceTransactionHelper {
// coordinates so offset the bounds to 0,0
mTmpDestinationRect.offsetTo(0, 0);
mTmpDestinationRect.inset(insets);
- // Scale by the shortest edge and offset such that the top/left of the scaled inset source
- // rect aligns with the top/left of the destination bounds
+ // Scale to the bounds no smaller than the destination and offset such that the top/left
+ // of the scaled inset source rect aligns with the top/left of the destination bounds
final float scale;
if (isInPipDirection
&& sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
@@ -129,9 +129,8 @@ public class PipSurfaceTransactionHelper {
: (float) destinationBounds.height() / sourceBounds.height();
scale = (1 - fraction) * startScale + fraction * endScale;
} else {
- scale = sourceBounds.width() <= sourceBounds.height()
- ? (float) destinationBounds.width() / sourceBounds.width()
- : (float) destinationBounds.height() / sourceBounds.height();
+ scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+ (float) destinationBounds.height() / sourceBounds.height());
}
final float left = destinationBounds.left - insets.left * scale;
final float top = destinationBounds.top - insets.top * scale;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index d7ca791e3863..21a13103616c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -108,6 +108,14 @@ class SplitScreenTransitions {
private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
@NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) {
+ final TransitSession pendingTransition = getPendingTransition(transition);
+ if (pendingTransition != null && pendingTransition.mCanceled) {
+ // The pending transition was canceled, so skip playing animation.
+ t.apply();
+ onFinish(null /* wct */, null /* wctCB */);
+ return;
+ }
+
// Play some place-holder fade animations
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
@@ -170,9 +178,7 @@ class SplitScreenTransitions {
}
boolean isPendingTransition(IBinder transition) {
- return isPendingEnter(transition)
- || isPendingDismiss(transition)
- || isPendingRecent(transition);
+ return getPendingTransition(transition) != null;
}
boolean isPendingEnter(IBinder transition) {
@@ -187,22 +193,38 @@ class SplitScreenTransitions {
return mPendingDismiss != null && mPendingDismiss.mTransition == transition;
}
+ @Nullable
+ private TransitSession getPendingTransition(IBinder transition) {
+ if (isPendingEnter(transition)) {
+ return mPendingEnter;
+ } else if (isPendingRecent(transition)) {
+ return mPendingRecent;
+ } else if (isPendingDismiss(transition)) {
+ return mPendingDismiss;
+ }
+
+ return null;
+ }
+
/** Starts a transition to enter split with a remote transition animator. */
IBinder startEnterTransition(
@WindowManager.TransitionType int transitType,
WindowContainerTransaction wct,
@Nullable RemoteTransition remoteTransition,
Transitions.TransitionHandler handler,
- @Nullable TransitionCallback callback) {
+ @Nullable TransitionConsumedCallback consumedCallback,
+ @Nullable TransitionFinishedCallback finishedCallback) {
final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
- setEnterTransition(transition, remoteTransition, callback);
+ setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback);
return transition;
}
/** Sets a transition to enter split. */
void setEnterTransition(@NonNull IBinder transition,
- @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
- mPendingEnter = new TransitSession(transition, callback);
+ @Nullable RemoteTransition remoteTransition,
+ @Nullable TransitionConsumedCallback consumedCallback,
+ @Nullable TransitionFinishedCallback finishedCallback) {
+ mPendingEnter = new TransitSession(transition, consumedCallback, finishedCallback);
if (remoteTransition != null) {
// Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -237,8 +259,9 @@ class SplitScreenTransitions {
}
void setRecentTransition(@NonNull IBinder transition,
- @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) {
- mPendingRecent = new TransitSession(transition, callback);
+ @Nullable RemoteTransition remoteTransition,
+ @Nullable TransitionFinishedCallback finishCallback) {
+ mPendingRecent = new TransitSession(transition, null /* consumedCb */, finishCallback);
if (remoteTransition != null) {
// Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
@@ -248,7 +271,7 @@ class SplitScreenTransitions {
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
- + " deduced Enter recent panel");
+ + " deduced Enter recent panel");
}
void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
@@ -256,14 +279,9 @@ class SplitScreenTransitions {
if (mergeTarget != mAnimatingTransition) return;
if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) {
- mPendingRecent.mCallback = new TransitionCallback() {
- @Override
- public void onTransitionFinished(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {
- // Since there's an entering transition merged, recent transition no longer
- // need to handle entering split screen after the transition finished.
- }
- };
+ // Since there's an entering transition merged, recent transition no longer
+ // need to handle entering split screen after the transition finished.
+ mPendingRecent.setFinishedCallback(null);
}
if (mActiveRemoteHandler != null) {
@@ -277,7 +295,7 @@ class SplitScreenTransitions {
}
boolean end() {
- // If its remote, there's nothing we can do right now.
+ // If It's remote, there's nothing we can do right now.
if (mActiveRemoteHandler != null) return false;
for (int i = mAnimations.size() - 1; i >= 0; --i) {
final Animator anim = mAnimations.get(i);
@@ -290,20 +308,20 @@ class SplitScreenTransitions {
@Nullable SurfaceControl.Transaction finishT) {
if (isPendingEnter(transition)) {
if (!aborted) {
- // An enter transition got merged, appends the rest operations to finish entering
+ // An entering transition got merged, appends the rest operations to finish entering
// split screen.
mStageCoordinator.finishEnterSplitScreen(finishT);
mPendingRemoteHandler = null;
}
- mPendingEnter.mCallback.onTransitionConsumed(aborted);
+ mPendingEnter.onConsumed(aborted);
mPendingEnter = null;
mPendingRemoteHandler = null;
} else if (isPendingDismiss(transition)) {
- mPendingDismiss.mCallback.onTransitionConsumed(aborted);
+ mPendingDismiss.onConsumed(aborted);
mPendingDismiss = null;
} else if (isPendingRecent(transition)) {
- mPendingRecent.mCallback.onTransitionConsumed(aborted);
+ mPendingRecent.onConsumed(aborted);
mPendingRecent = null;
mPendingRemoteHandler = null;
}
@@ -312,23 +330,16 @@ class SplitScreenTransitions {
void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) {
if (!mAnimations.isEmpty()) return;
- TransitionCallback callback = null;
+ if (wct == null) wct = new WindowContainerTransaction();
if (isPendingEnter(mAnimatingTransition)) {
- callback = mPendingEnter.mCallback;
+ mPendingEnter.onFinished(wct, mFinishTransaction);
mPendingEnter = null;
- }
- if (isPendingDismiss(mAnimatingTransition)) {
- callback = mPendingDismiss.mCallback;
- mPendingDismiss = null;
- }
- if (isPendingRecent(mAnimatingTransition)) {
- callback = mPendingRecent.mCallback;
+ } else if (isPendingRecent(mAnimatingTransition)) {
+ mPendingRecent.onFinished(wct, mFinishTransaction);
mPendingRecent = null;
- }
-
- if (callback != null) {
- if (wct == null) wct = new WindowContainerTransaction();
- callback.onTransitionFinished(wct, mFinishTransaction);
+ } else if (isPendingDismiss(mAnimatingTransition)) {
+ mPendingDismiss.onFinished(wct, mFinishTransaction);
+ mPendingDismiss = null;
}
mPendingRemoteHandler = null;
@@ -363,10 +374,7 @@ class SplitScreenTransitions {
onFinish(null /* wct */, null /* wctCB */);
});
};
- va.addListener(new Animator.AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) { }
-
+ va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finisher.run();
@@ -376,9 +384,6 @@ class SplitScreenTransitions {
public void onAnimationCancel(Animator animation) {
finisher.run();
}
-
- @Override
- public void onAnimationRepeat(Animator animation) { }
});
mAnimations.add(va);
mTransitions.getAnimExecutor().execute(va::start);
@@ -432,24 +437,66 @@ class SplitScreenTransitions {
|| info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
}
- /** Clean-up callbacks for transition. */
- interface TransitionCallback {
- /** Calls when the transition got consumed. */
- default void onTransitionConsumed(boolean aborted) {}
+ /** Calls when the transition got consumed. */
+ interface TransitionConsumedCallback {
+ void onConsumed(boolean aborted);
+ }
- /** Calls when the transition finished. */
- default void onTransitionFinished(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {}
+ /** Calls when the transition finished. */
+ interface TransitionFinishedCallback {
+ void onFinished(WindowContainerTransaction wct, SurfaceControl.Transaction t);
}
/** Session for a transition and its clean-up callback. */
static class TransitSession {
final IBinder mTransition;
- TransitionCallback mCallback;
+ TransitionConsumedCallback mConsumedCallback;
+ TransitionFinishedCallback mFinishedCallback;
- TransitSession(IBinder transition, @Nullable TransitionCallback callback) {
+ /** Whether the transition was canceled. */
+ boolean mCanceled;
+
+ TransitSession(IBinder transition,
+ @Nullable TransitionConsumedCallback consumedCallback,
+ @Nullable TransitionFinishedCallback finishedCallback) {
mTransition = transition;
- mCallback = callback != null ? callback : new TransitionCallback() {};
+ mConsumedCallback = consumedCallback;
+ mFinishedCallback = finishedCallback;
+
+ }
+
+ /** Sets transition consumed callback. */
+ void setConsumedCallback(@Nullable TransitionConsumedCallback callback) {
+ mConsumedCallback = callback;
+ }
+
+ /** Sets transition finished callback. */
+ void setFinishedCallback(@Nullable TransitionFinishedCallback callback) {
+ mFinishedCallback = callback;
+ }
+
+ /**
+ * Cancels the transition. This should be called before playing animation. A canceled
+ * transition will skip playing animation.
+ *
+ * @param finishedCb new finish callback to override.
+ */
+ void cancel(@Nullable TransitionFinishedCallback finishedCb) {
+ mCanceled = true;
+ setFinishedCallback(finishedCb);
+ }
+
+ void onConsumed(boolean aborted) {
+ if (mConsumedCallback != null) {
+ mConsumedCallback.onConsumed(aborted);
+ }
+ }
+
+ void onFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ if (mFinishedCallback != null) {
+ mFinishedCallback.onFinished(finishWct, finishT);
+ }
}
}
@@ -459,7 +506,7 @@ class SplitScreenTransitions {
final @SplitScreen.StageType int mDismissTop;
DismissTransition(IBinder transition, int reason, int dismissTop) {
- super(transition, null /* callback */);
+ super(transition, null /* consumedCallback */, null /* finishedCallback */);
this.mReason = reason;
this.mDismissTop = dismissTop;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e2ac01f7b003..943419bb8ea2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -226,33 +226,36 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
};
- private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback =
- new SplitScreenTransitions.TransitionCallback() {
- @Override
- public void onTransitionFinished(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {
- // Check if the recent transition is finished by returning to the current split, so we
- // can restore the divider bar.
- for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
- final WindowContainerTransaction.HierarchyOp op =
- finishWct.getHierarchyOps().get(i);
- final IBinder container = op.getContainer();
- if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
- && (mMainStage.containsContainer(container)
- || mSideStage.containsContainer(container))) {
- updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
- setDividerVisibility(true, finishT);
- return;
- }
- }
+ private final SplitScreenTransitions.TransitionFinishedCallback
+ mRecentTransitionFinishedCallback =
+ new SplitScreenTransitions.TransitionFinishedCallback() {
+ @Override
+ public void onFinished(WindowContainerTransaction finishWct,
+ SurfaceControl.Transaction finishT) {
+ // Check if the recent transition is finished by returning to the current
+ // split, so we
+ // can restore the divider bar.
+ for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
+ final WindowContainerTransaction.HierarchyOp op =
+ finishWct.getHierarchyOps().get(i);
+ final IBinder container = op.getContainer();
+ if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
+ && (mMainStage.containsContainer(container)
+ || mSideStage.containsContainer(container))) {
+ updateSurfaceBounds(mSplitLayout, finishT,
+ false /* applyResizingOffset */);
+ setDividerVisibility(true, finishT);
+ return;
+ }
+ }
- // Dismiss the split screen if it's not returning to split.
- prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
- setSplitsVisible(false);
- setDividerVisibility(false, finishT);
- logExit(EXIT_REASON_UNKNOWN);
- }
- };
+ // Dismiss the split screen if it's not returning to split.
+ prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct);
+ setSplitsVisible(false);
+ setDividerVisibility(false, finishT);
+ logExit(EXIT_REASON_UNKNOWN);
+ }
+ };
protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
ShellTaskOrganizer taskOrganizer, DisplayController displayController,
@@ -389,15 +392,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (ENABLE_SHELL_TRANSITIONS) {
prepareEnterSplitScreen(wct);
mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct,
- null, this, new SplitScreenTransitions.TransitionCallback() {
- @Override
- public void onTransitionFinished(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {
- if (!evictWct.isEmpty()) {
- finishWct.merge(evictWct, true);
- }
+ null, this, null /* consumedCallback */, (finishWct, finishT) -> {
+ if (!evictWct.isEmpty()) {
+ finishWct.merge(evictWct, true);
}
- });
+ } /* finishedCallback */);
} else {
if (!evictWct.isEmpty()) {
wct.merge(evictWct, true /* transfer */);
@@ -434,28 +433,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
wct.sendPendingIntent(intent, fillInIntent, options);
+
+ // If split screen is not activated, we're expecting to open a pair of apps to split.
+ final int transitType = mMainStage.isActive()
+ ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
prepareEnterSplitScreen(wct, null /* taskInfo */, position);
- mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this,
- new SplitScreenTransitions.TransitionCallback() {
- @Override
- public void onTransitionConsumed(boolean aborted) {
- // Switch the split position if launching as MULTIPLE_TASK failed.
- if (aborted
- && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
- setSideStagePositionAnimated(
- SplitLayout.reversePosition(mSideStagePosition));
- }
+ mSplitTransitions.startEnterTransition(transitType, wct, null, this,
+ aborted -> {
+ // Switch the split position if launching as MULTIPLE_TASK failed.
+ if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+ setSideStagePositionAnimated(
+ SplitLayout.reversePosition(mSideStagePosition));
}
-
- @Override
- public void onTransitionFinished(WindowContainerTransaction finishWct,
- SurfaceControl.Transaction finishT) {
- if (!evictWct.isEmpty()) {
- finishWct.merge(evictWct, true);
- }
+ } /* consumedCallback */,
+ (finishWct, finishT) -> {
+ if (!evictWct.isEmpty()) {
+ finishWct.merge(evictWct, true);
}
- });
+ } /* finishedCallback */);
}
/** Launches an activity into split by legacy transition. */
@@ -564,9 +560,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
/**
* Starts with the second task to a split pair in one transition.
*
- * @param wct transaction to start the first task
+ * @param wct transaction to start the first task
* @param instanceId if {@code null}, will not log. Otherwise it will be used in
- * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
+ * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
*/
private void startWithTask(WindowContainerTransaction wct, int mainTaskId,
@Nullable Bundle mainOptions, float splitRatio,
@@ -592,7 +588,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
wct.startTask(mainTaskId, mainOptions);
mSplitTransitions.startEnterTransition(
- TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null);
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null, null);
setEnterInstanceId(instanceId);
}
@@ -639,7 +635,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
/**
- * @param wct transaction to start the first task
+ * @param wct transaction to start the first task
* @param instanceId if {@code null}, will not log. Otherwise it will be used in
* {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
*/
@@ -1082,15 +1078,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
switch (exitReason) {
// One of the apps doesn't support MW
case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
- // User has explicitly dragged the divider to dismiss split
+ // User has explicitly dragged the divider to dismiss split
case EXIT_REASON_DRAG_DIVIDER:
- // Either of the split apps have finished
+ // Either of the split apps have finished
case EXIT_REASON_APP_FINISHED:
- // One of the children enters PiP
+ // One of the children enters PiP
case EXIT_REASON_CHILD_TASK_ENTER_PIP:
- // One of the apps occludes lock screen.
+ // One of the apps occludes lock screen.
case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
- // User has unlocked the device after folded
+ // User has unlocked the device after folded
case EXIT_REASON_DEVICE_FOLDED:
return true;
default:
@@ -1839,7 +1835,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
|| activityType == ACTIVITY_TYPE_RECENTS) {
// Enter overview panel, so start recent transition.
mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(),
- mRecentTransitionCallback);
+ mRecentTransitionFinishedCallback);
} else if (mSplitTransitions.mPendingRecent == null) {
// If split-task is not controlled by recents animation
// and occluded by the other fullscreen task, dismiss both.
@@ -1853,8 +1849,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// One task is appearing into split, prepare to enter split screen.
out = new WindowContainerTransaction();
prepareEnterSplitScreen(out);
- mSplitTransitions.setEnterTransition(
- transition, request.getRemoteTransition(), null /* callback */);
+ mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
+ null /* consumedCallback */, null /* finishedCallback */);
}
}
return out;
@@ -1873,7 +1869,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
final @WindowManager.TransitionType int type = request.getType();
if (isSplitActive() && !isOpeningType(type)
- && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
+ && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " One of the splits became "
+ "empty during a mixed transition (one not handled by split),"
+ " so make sure split-screen state is cleaned-up. "
@@ -2031,17 +2027,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
- // TODO(b/250853925): fallback logic. Probably start a new transition to exit split before
- // applying anything here. Ideally consolidate with transition-merging.
if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
if (mainChild == null && sideChild == null) {
- throw new IllegalStateException("Launched a task in split, but didn't receive any"
- + " task in transition.");
+ Log.w(TAG, "Launched a task in split, but didn't receive any task in transition.");
+ mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */);
+ return true;
}
} else {
if (mainChild == null || sideChild == null) {
- throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
+ Log.w(TAG, "Launched 2 tasks in split, but didn't receive"
+ " 2 tasks in transition. Possibly one of them failed to launch");
+ final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN :
+ (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED);
+ mSplitTransitions.mPendingEnter.cancel(
+ (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct));
+ return true;
}
}
@@ -2305,7 +2305,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final int stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
final WindowContainerTransaction wct = new WindowContainerTransaction();
prepareExitSplitScreen(stageType, wct);
- mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType,
+ mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
mSplitUnsupportedToast.show();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 394d6f6bf731..63d31cde4715 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -122,6 +122,8 @@ public class Transitions implements RemoteCallable<Transitions> {
private final ShellController mShellController;
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
+ private boolean mIsRegistered = false;
+
/** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>();
@@ -163,19 +165,18 @@ public class Transitions implements RemoteCallable<Transitions> {
displayController, pool, mainExecutor, mainHandler, animExecutor);
mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
mShellController = shellController;
- shellInit.addInitCallback(this::onInit, this);
- }
-
- private void onInit() {
- mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
- this::createExternalInterface, this);
-
// The very last handler (0 in the list) should be the default one.
mHandlers.add(mDefaultTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
// Next lowest priority is remote transitions.
mHandlers.add(mRemoteTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
+ this::createExternalInterface, this);
ContentResolver resolver = mContext.getContentResolver();
mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
@@ -186,13 +187,23 @@ public class Transitions implements RemoteCallable<Transitions> {
new SettingsObserver());
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mIsRegistered = true;
// Register this transition handler with Core
- mOrganizer.registerTransitionPlayer(mPlayerImpl);
+ try {
+ mOrganizer.registerTransitionPlayer(mPlayerImpl);
+ } catch (RuntimeException e) {
+ mIsRegistered = false;
+ throw e;
+ }
// Pre-load the instance.
TransitionMetrics.getInstance();
}
}
+ public boolean isRegistered() {
+ return mIsRegistered;
+ }
+
private float getTransitionAnimationScaleSetting() {
return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index ea0033ba4bbb..652f9b38c88f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -181,7 +181,7 @@ public class SplitTransitionTests extends ShellTestCase {
IBinder transition = mSplitScreenTransitions.startEnterTransition(
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
- new RemoteTransition(testRemote), mStageCoordinator, null);
+ new RemoteTransition(testRemote), mStageCoordinator, null, null);
mMainStage.onTaskAppeared(mMainChild, createMockSurface());
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
@@ -421,7 +421,7 @@ public class SplitTransitionTests extends ShellTestCase {
TransitionInfo enterInfo = createEnterPairInfo();
IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
- new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null);
+ new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null, null);
mMainStage.onTaskAppeared(mMainChild, createMockSurface());
mSideStage.onTaskAppeared(mSideChild, createMockSurface());
mStageCoordinator.startAnimation(enterTransit, enterInfo,
diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java
index 7a412a0ed2de..b38f9ea39136 100644
--- a/location/java/android/location/GnssCapabilities.java
+++ b/location/java/android/location/GnssCapabilities.java
@@ -207,7 +207,7 @@ public final class GnssCapabilities implements Parcelable {
/**
* Returns {@code true} if GNSS chipset supports single shot locating, {@code false} otherwise.
*/
- public boolean hasSingleShot() {
+ public boolean hasSingleShotFix() {
return (mTopFlags & TOP_HAL_CAPABILITY_SINGLE_SHOT) != 0;
}
@@ -482,7 +482,7 @@ public final class GnssCapabilities implements Parcelable {
if (hasMsa()) {
builder.append("MSA ");
}
- if (hasSingleShot()) {
+ if (hasSingleShotFix()) {
builder.append("SINGLE_SHOT ");
}
if (hasOnDemandTime()) {
@@ -602,7 +602,7 @@ public final class GnssCapabilities implements Parcelable {
/**
* Sets single shot locating capability.
*/
- public @NonNull Builder setHasSingleShot(boolean capable) {
+ public @NonNull Builder setHasSingleShotFix(boolean capable) {
mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SINGLE_SHOT, capable);
return this;
}
diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java
index 23390fce1a5f..09f40e80f885 100644
--- a/location/java/android/location/GnssStatus.java
+++ b/location/java/android/location/GnssStatus.java
@@ -199,15 +199,15 @@ public final class GnssStatus implements Parcelable {
* <li>93-106 as the frequency channel number (FCN) (-7 to +6) plus 100.
* i.e. encode FCN of -7 as 93, 0 as 100, and +6 as 106</li>
* </ul></li>
- * <li>QZSS: 193-200</li>
+ * <li>QZSS: 183-206</li>
* <li>Galileo: 1-36</li>
- * <li>Beidou: 1-37</li>
+ * <li>Beidou: 1-63</li>
* <li>IRNSS: 1-14</li>
* </ul>
*
* @param satelliteIndex An index from zero to {@link #getSatelliteCount()} - 1
*/
- @IntRange(from = 1, to = 200)
+ @IntRange(from = 1, to = 206)
public int getSvid(@IntRange(from = 0) int satelliteIndex) {
return mSvidWithFlags[satelliteIndex] >> SVID_SHIFT_WIDTH;
}
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
index 428f2dc2eb35..2000d9675ca4 100644
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ b/packages/CompanionDeviceManager/res/values/styles.xml
@@ -49,7 +49,6 @@
<style name="DescriptionSummary">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
- <item name="android:gravity">center</item>
<item name="android:layout_marginTop">18dp</item>
<item name="android:layout_marginLeft">18dp</item>
<item name="android:layout_marginRight">18dp</item>
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt
new file mode 100644
index 000000000000..d4341b498fe0
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class ActionUi(
+ val icon: Icon,
+ val text: CharSequence,
+ val subtext: CharSequence?,
+) {
+ companion object {
+ fun fromSlice(slice: Slice): ActionUi {
+ var icon: Icon? = null
+ var text: CharSequence? = null
+ var subtext: CharSequence? = null
+
+ val items = slice.items
+ items.forEach {
+ if (it.hasHint(Entry.HINT_ACTION_ICON)) {
+ icon = it.icon
+ } else if (it.hasHint(Entry.HINT_ACTION_TITLE)) {
+ text = it.text
+ } else if (it.hasHint(Entry.HINT_ACTION_SUBTEXT)) {
+ subtext = it.text
+ }
+ }
+ // TODO: fail NPE more elegantly.
+ return ActionUi(icon!!, text!!, subtext)
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index eb53ea1d44f7..950ee21ae7b5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -758,23 +758,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
}
public boolean isBusy() {
- for (CachedBluetoothDevice memberDevice : getMemberDevice()) {
- if (isBusyState(memberDevice)) {
- return true;
- }
- }
- return isBusyState(this);
- }
-
- private boolean isBusyState(CachedBluetoothDevice device){
- for (LocalBluetoothProfile profile : device.getProfiles()) {
- int status = device.getProfileConnectionState(profile);
- if (status == BluetoothProfile.STATE_CONNECTING
- || status == BluetoothProfile.STATE_DISCONNECTING) {
- return true;
+ synchronized (mProfileLock) {
+ for (LocalBluetoothProfile profile : mProfiles) {
+ int status = getProfileConnectionState(profile);
+ if (status == BluetoothProfile.STATE_CONNECTING
+ || status == BluetoothProfile.STATE_DISCONNECTING) {
+ return true;
+ }
}
+ return getBondState() == BluetoothDevice.BOND_BONDING;
}
- return device.getBondState() == BluetoothDevice.BOND_BONDING;
}
private boolean updateProfiles() {
@@ -920,7 +913,14 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
@Override
public String toString() {
- return mDevice.toString();
+ return "CachedBluetoothDevice ("
+ + "anonymizedAddress="
+ + mDevice.getAnonymizedAddress()
+ + ", name="
+ + getName()
+ + ", groupId="
+ + mGroupId
+ + ")";
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 8a9f9dd9c3fb..fb861da0a7f0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -231,7 +231,7 @@ public class LocalBluetoothProfileManager {
if (DEBUG) {
Log.d(TAG, "Adding local Volume Control profile");
}
- mVolumeControlProfile = new VolumeControlProfile();
+ mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this);
// Note: no event handler for VCP, only for being connectable.
mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile);
}
@@ -553,6 +553,10 @@ public class LocalBluetoothProfileManager {
return mCsipSetCoordinatorProfile;
}
+ public VolumeControlProfile getVolumeControlProfile() {
+ return mVolumeControlProfile;
+ }
+
/**
* Fill in a list of LocalBluetoothProfile objects that are supported by
* the local device and the remote device.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
index 511df282ea4b..57867be53bb9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
@@ -16,18 +16,91 @@
package com.android.settingslib.bluetooth;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothVolumeControl;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.RequiresApi;
/**
* VolumeControlProfile handles Bluetooth Volume Control Controller role
*/
public class VolumeControlProfile implements LocalBluetoothProfile {
private static final String TAG = "VolumeControlProfile";
+ private static boolean DEBUG = true;
static final String NAME = "VCP";
// Order of this profile in device profiles list
- private static final int ORDINAL = 23;
+ private static final int ORDINAL = 1;
+
+ private Context mContext;
+ private final CachedBluetoothDeviceManager mDeviceManager;
+ private final LocalBluetoothProfileManager mProfileManager;
+
+ private BluetoothVolumeControl mService;
+ private boolean mIsProfileReady;
+
+ // These callbacks run on the main thread.
+ private final class VolumeControlProfileServiceListener
+ implements BluetoothProfile.ServiceListener {
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (DEBUG) {
+ Log.d(TAG, "Bluetooth service connected");
+ }
+ mService = (BluetoothVolumeControl) proxy;
+ // We just bound to the service, so refresh the UI for any connected
+ // VolumeControlProfile devices.
+ List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+ while (!deviceList.isEmpty()) {
+ BluetoothDevice nextDevice = deviceList.remove(0);
+ CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+ // we may add a new device here, but generally this should not happen
+ if (device == null) {
+ if (DEBUG) {
+ Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice);
+ }
+ device = mDeviceManager.addDevice(nextDevice);
+ }
+ device.onProfileStateChanged(VolumeControlProfile.this,
+ BluetoothProfile.STATE_CONNECTED);
+ device.refresh();
+ }
+
+ mProfileManager.callServiceConnectedListeners();
+ mIsProfileReady = true;
+ }
+
+ public void onServiceDisconnected(int profile) {
+ if (DEBUG) {
+ Log.d(TAG, "Bluetooth service disconnected");
+ }
+ mProfileManager.callServiceDisconnectedListeners();
+ mIsProfileReady = false;
+ }
+ }
+
+ VolumeControlProfile(Context context, CachedBluetoothDeviceManager deviceManager,
+ LocalBluetoothProfileManager profileManager) {
+ mContext = context;
+ mDeviceManager = deviceManager;
+ mProfileManager = profileManager;
+
+ BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
+ new VolumeControlProfile.VolumeControlProfileServiceListener(),
+ BluetoothProfile.VOLUME_CONTROL);
+ }
@Override
public boolean accessProfileEnabled() {
@@ -39,29 +112,70 @@ public class VolumeControlProfile implements LocalBluetoothProfile {
return true;
}
+ /**
+ * Get VolumeControlProfile devices matching connection states{
+ *
+ * @return Matching device list
+ * @code BluetoothProfile.STATE_CONNECTED,
+ * @code BluetoothProfile.STATE_CONNECTING,
+ * @code BluetoothProfile.STATE_DISCONNECTING}
+ */
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (mService == null) {
+ return new ArrayList<BluetoothDevice>(0);
+ }
+ return mService.getDevicesMatchingConnectionStates(
+ new int[]{BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_DISCONNECTING});
+ }
+
@Override
public int getConnectionStatus(BluetoothDevice device) {
- return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle VCP
+ if (mService == null) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
- return false;
+ if (mService == null || device == null) {
+ return false;
+ }
+ return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
- return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle VCP
+ if (mService == null || device == null) {
+ return CONNECTION_POLICY_FORBIDDEN;
+ }
+ return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
- return false;
+ boolean isSuccessful = false;
+ if (mService == null || device == null) {
+ return false;
+ }
+ if (DEBUG) {
+ Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled);
+ }
+ if (enabled) {
+ if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
+ isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
+ }
+ } else {
+ isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
+ }
+
+ return isSuccessful;
}
@Override
public boolean isProfileReady() {
- return true;
+ return mIsProfileReady;
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 1606540da3fd..2614644feb20 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -92,7 +92,8 @@ public class DreamBackend {
COMPLICATION_TYPE_AIR_QUALITY,
COMPLICATION_TYPE_CAST_INFO,
COMPLICATION_TYPE_HOME_CONTROLS,
- COMPLICATION_TYPE_SMARTSPACE
+ COMPLICATION_TYPE_SMARTSPACE,
+ COMPLICATION_TYPE_MEDIA_ENTRY
})
@Retention(RetentionPolicy.SOURCE)
public @interface ComplicationType {
@@ -105,6 +106,7 @@ public class DreamBackend {
public static final int COMPLICATION_TYPE_CAST_INFO = 5;
public static final int COMPLICATION_TYPE_HOME_CONTROLS = 6;
public static final int COMPLICATION_TYPE_SMARTSPACE = 7;
+ public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8;
private final Context mContext;
private final IDreamManager mDreamManager;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 315ab0aac878..79e99387b2fa 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -1069,80 +1069,4 @@ public class CachedBluetoothDeviceTest {
assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
}
-
- @Test
- public void isBusy_mainDeviceIsConnecting_returnsBusy() {
- mCachedDevice.addMemberDevice(mSubCachedDevice);
- updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
- when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
- updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING);
-
- assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
- assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mCachedDevice.isBusy()).isTrue();
- }
-
- @Test
- public void isBusy_mainDeviceIsBonding_returnsBusy() {
- mCachedDevice.addMemberDevice(mSubCachedDevice);
- updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
- when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
- assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
- assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mCachedDevice.isBusy()).isTrue();
- }
-
- @Test
- public void isBusy_memberDeviceIsConnecting_returnsBusy() {
- mCachedDevice.addMemberDevice(mSubCachedDevice);
- updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
- when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
- updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING);
-
- assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
- assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mCachedDevice.isBusy()).isTrue();
- }
-
- @Test
- public void isBusy_memberDeviceIsBonding_returnsBusy() {
- mCachedDevice.addMemberDevice(mSubCachedDevice);
- updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
- when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
- assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
- assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mCachedDevice.isBusy()).isTrue();
- }
-
- @Test
- public void isBusy_allDevicesAreNotBusy_returnsNotBusy() {
- mCachedDevice.addMemberDevice(mSubCachedDevice);
- updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
- when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
- when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
- assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
- assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
- assertThat(mCachedDevice.isBusy()).isFalse();
- }
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index ccbfac226c46..fa96a2f0ee7f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -5533,13 +5533,17 @@ public class SettingsProvider extends ContentProvider {
}
if (currentVersion == 210) {
final SettingsState secureSettings = getSecureSettingsLocked(userId);
- final int defaultValueVibrateIconEnabled = getContext().getResources()
- .getInteger(R.integer.def_statusBarVibrateIconEnabled);
- secureSettings.insertSettingOverrideableByRestoreLocked(
- Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
- String.valueOf(defaultValueVibrateIconEnabled),
- null /* tag */, true /* makeDefault */,
- SettingsState.SYSTEM_PACKAGE_NAME);
+ final Setting currentSetting = secureSettings.getSettingLocked(
+ Secure.STATUS_BAR_SHOW_VIBRATE_ICON);
+ if (currentSetting.isNull()) {
+ final int defaultValueVibrateIconEnabled = getContext().getResources()
+ .getInteger(R.integer.def_statusBarVibrateIconEnabled);
+ secureSettings.insertSettingOverrideableByRestoreLocked(
+ Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
+ String.valueOf(defaultValueVibrateIconEnabled),
+ null /* tag */, true /* makeDefault */,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ }
currentVersion = 211;
}
// vXXX: Add new settings above this point.
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b5145f926abd..4267ba2ff0b7 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -289,6 +289,12 @@
<!-- Query all packages on device on R+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.NOTES" />
+ </intent>
+ </queries>
+
<!-- Permission to register process observer -->
<uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/>
diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md
index 38d636d7ff82..95b986faebb4 100644
--- a/packages/SystemUI/docs/device-entry/quickaffordance.md
+++ b/packages/SystemUI/docs/device-entry/quickaffordance.md
@@ -8,7 +8,7 @@ credit card, etc.
### Step 1: create a new quick affordance config
* Create a new class under the [systemui/keyguard/domain/quickaffordance](../../src/com/android/systemui/keyguard/domain/quickaffordance) directory
* Please make sure that the class is injected through the Dagger dependency injection system by using the `@Inject` annotation on its main constructor and the `@SysUISingleton` annotation at class level, to make sure only one instance of the class is ever instantiated
-* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes:
+* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes:
* The `state` Flow property must emit `State.Hidden` when the feature is not enabled!
* It is safe to assume that `onQuickAffordanceClicked` will not be invoked if-and-only-if the previous rule is followed
* When implementing `onQuickAffordanceClicked`, the implementation can do something or it can ask the framework to start an activity using an `Intent` provided by the implementation
diff --git a/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml
new file mode 100644
index 000000000000..de83df4e625c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/accessibility_floating_menu_message_background"/>
+ <corners android:radius="28dp"/>
+</shape>
diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
index 3bcc37a478c9..e2ce34f5008e 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<com.android.systemui.biometrics.AuthCredentialPasswordView
+<com.android.systemui.biometrics.ui.CredentialPasswordView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -86,4 +86,4 @@
</LinearLayout>
-</com.android.systemui.biometrics.AuthCredentialPasswordView> \ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPasswordView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index a3dd334bd667..6e0e38b95ee5 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<com.android.systemui.biometrics.AuthCredentialPatternView
+<com.android.systemui.biometrics.ui.CredentialPatternView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -83,4 +83,4 @@
</FrameLayout>
-</com.android.systemui.biometrics.AuthCredentialPatternView> \ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPatternView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml
index 774b335f913e..021ebe6e7bff 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<com.android.systemui.biometrics.AuthCredentialPasswordView
+<com.android.systemui.biometrics.ui.CredentialPasswordView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -83,4 +83,4 @@
</LinearLayout>
-</com.android.systemui.biometrics.AuthCredentialPasswordView> \ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPasswordView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index 4af997017bba..891c6af4b667 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<com.android.systemui.biometrics.AuthCredentialPatternView
+<com.android.systemui.biometrics.ui.CredentialPatternView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -78,4 +78,4 @@
android:layout_gravity="center_horizontal|bottom"/>
</FrameLayout>
-</com.android.systemui.biometrics.AuthCredentialPatternView> \ No newline at end of file
+</com.android.systemui.biometrics.ui.CredentialPatternView>
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index dc2bee56373c..16152f80308a 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -99,6 +99,8 @@
<!-- Accessibility floating menu -->
<color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% -->
+ <color name="accessibility_floating_menu_message_background">@*android:color/background_material_dark</color>
+ <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_dark</color>
<color name="people_tile_background">@color/material_dynamic_secondary20</color>
</resources>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 9e8bef06270b..55b59b63c2f9 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -219,6 +219,8 @@
<!-- Accessibility floating menu -->
<color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% -->
<color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% -->
+ <color name="accessibility_floating_menu_message_background">@*android:color/background_material_light</color>
+ <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_light</color>
<!-- Wallet screen -->
<color name="wallet_card_border">#33FFFFFF</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9188ce091a3b..93982cb2c5b9 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -643,6 +643,18 @@
<item>26</item> <!-- MOUTH_COVERING_DETECTED -->
</integer-array>
+ <!-- Which device wake-ups will trigger face auth. These values correspond with
+ PowerManager#WakeReason. -->
+ <integer-array name="config_face_auth_wake_up_triggers">
+ <item>1</item> <!-- WAKE_REASON_POWER_BUTTON -->
+ <item>4</item> <!-- WAKE_REASON_GESTURE -->
+ <item>6</item> <!-- WAKE_REASON_WAKE_KEY -->
+ <item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
+ <item>9</item> <!-- WAKE_REASON_LID -->
+ <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
+ <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
+ </integer-array>
+
<!-- Whether the communal service should be enabled -->
<bool name="config_communalServiceEnabled">false</bool>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 66f0e7543469..f02f29a4f741 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1336,6 +1336,14 @@
<dimen name="accessibility_floating_menu_large_single_radius">35dp</dimen>
<dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen>
+ <dimen name="accessibility_floating_menu_message_container_horizontal_padding">15dp</dimen>
+ <dimen name="accessibility_floating_menu_message_text_vertical_padding">8dp</dimen>
+ <dimen name="accessibility_floating_menu_message_margin">8dp</dimen>
+ <dimen name="accessibility_floating_menu_message_elevation">5dp</dimen>
+ <dimen name="accessibility_floating_menu_message_text_size">14sp</dimen>
+ <dimen name="accessibility_floating_menu_message_min_width">312dp</dimen>
+ <dimen name="accessibility_floating_menu_message_min_height">48dp</dimen>
+
<dimen name="accessibility_floating_tooltip_arrow_width">8dp</dimen>
<dimen name="accessibility_floating_tooltip_arrow_height">16dp</dimen>
<dimen name="accessibility_floating_tooltip_arrow_margin">-2dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 7ca42f7d7015..4fd25a98a71c 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -177,6 +177,7 @@
<item type="id" name="action_move_bottom_right"/>
<item type="id" name="action_move_to_edge_and_hide"/>
<item type="id" name="action_move_out_edge_and_show"/>
+ <item type="id" name="action_remove_menu"/>
<!-- rounded corner view id -->
<item type="id" name="rounded_corner_top_left"/>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 44031bb99751..b325c56adefc 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2197,6 +2197,15 @@
<string name="accessibility_floating_button_migration_tooltip">Tap to open accessibility features. Customize or replace this button in Settings.\n\n<annotation id="link">View settings</annotation></string>
<!-- Message for the accessibility floating button docking tooltip. It shows when the user first time drag the button. It will tell the user about docking behavior. [CHAR LIMIT=70] -->
<string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string>
+ <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]-->
+ <string name="accessibility_floating_button_undo">Undo</string>
+
+ <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]-->
+ <string name="accessibility_floating_button_undo_message_text">{count, plural,
+ =1 {{label} shortcut removed}
+ other {# shortcuts removed}
+ }</string>
+
<!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] -->
<string name="accessibility_floating_button_action_move_top_left">Move top left</string>
<!-- Action in accessibility menu to move the accessibility floating button to the top right of the screen. [CHAR LIMIT=30] -->
@@ -2209,6 +2218,8 @@
<string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half">Move to edge and hide</string>
<!-- Action in accessibility menu to move the accessibility floating button out the edge and show. [CHAR LIMIT=36]-->
<string name="accessibility_floating_button_action_move_out_edge_and_show">Move out edge and show</string>
+ <!-- Action in accessibility menu to remove the accessibility floating menu view on the screen. [CHAR LIMIT=36]-->
+ <string name="accessibility_floating_button_action_remove_menu">Remove</string>
<!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]-->
<string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
index 7e42e1b89b1f..8ac1de898be8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
@@ -85,13 +85,12 @@ public class PipSurfaceTransactionHelper {
mTmpSourceRectF.set(sourceBounds);
mTmpDestinationRect.set(sourceBounds);
mTmpDestinationRect.inset(insets);
- // Scale by the shortest edge and offset such that the top/left of the scaled inset
- // source rect aligns with the top/left of the destination bounds
+ // Scale to the bounds no smaller than the destination and offset such that the top/left
+ // of the scaled inset source rect aligns with the top/left of the destination bounds
final float scale;
if (sourceRectHint.isEmpty() || sourceRectHint.width() == sourceBounds.width()) {
- scale = sourceBounds.width() <= sourceBounds.height()
- ? (float) destinationBounds.width() / sourceBounds.width()
- : (float) destinationBounds.height() / sourceBounds.height();
+ scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
+ (float) destinationBounds.height() / sourceBounds.height());
} else {
// scale by sourceRectHint if it's not edge-to-edge
final float endScale = sourceRectHint.width() <= sourceRectHint.height()
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 386c09575a1a..40a96b060bc0 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -30,6 +30,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
import com.android.systemui.flags.Flags.REGION_SAMPLING
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -221,8 +222,11 @@ open class ClockEventController @Inject constructor(
disposableHandle = parent.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
listenForDozing(this)
- listenForDozeAmount(this)
- listenForDozeAmountTransition(this)
+ if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ listenForDozeAmountTransition(this)
+ } else {
+ listenForDozeAmount(this)
+ }
}
}
}
@@ -265,10 +269,9 @@ open class ClockEventController @Inject constructor(
@VisibleForTesting
internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
return scope.launch {
- keyguardTransitionInteractor.aodToLockscreenTransition.collect {
- // Would eventually run this:
- // dozeAmount = it.value
- // clock?.animations?.doze(dozeAmount)
+ keyguardTransitionInteractor.dozeAmountTransition.collect {
+ dozeAmount = it.value
+ clock?.animations?.doze(dozeAmount)
}
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index 6fcb6f55dc20..4a41b3fe2589 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -17,6 +17,7 @@
package com.android.keyguard
import android.annotation.StringDef
+import android.os.PowerManager
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED
@@ -122,122 +123,93 @@ private object InternalFaceAuthReasons {
"Face auth started/stopped because biometric is enabled on keyguard"
}
-/** UiEvents that are logged to identify why face auth is being triggered. */
-enum class FaceAuthUiEvent constructor(private val id: Int, val reason: String) :
+/**
+ * UiEvents that are logged to identify why face auth is being triggered.
+ * @param extraInfo is logged as the position. See [UiEventLogger#logWithInstanceIdAndPosition]
+ */
+enum class FaceAuthUiEvent
+constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) :
UiEventLogger.UiEventEnum {
@UiEvent(doc = OCCLUDING_APP_REQUESTED)
FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED(1146, OCCLUDING_APP_REQUESTED),
-
@UiEvent(doc = UDFPS_POINTER_DOWN)
FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN(1147, UDFPS_POINTER_DOWN),
-
@UiEvent(doc = SWIPE_UP_ON_BOUNCER)
FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER(1148, SWIPE_UP_ON_BOUNCER),
-
@UiEvent(doc = DEVICE_WOKEN_UP_ON_REACH_GESTURE)
FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD(1149, DEVICE_WOKEN_UP_ON_REACH_GESTURE),
-
@UiEvent(doc = FACE_LOCKOUT_RESET)
FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET(1150, FACE_LOCKOUT_RESET),
-
- @UiEvent(doc = QS_EXPANDED)
- FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED),
-
+ @UiEvent(doc = QS_EXPANDED) FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED),
@UiEvent(doc = NOTIFICATION_PANEL_CLICKED)
FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED(1152, NOTIFICATION_PANEL_CLICKED),
-
@UiEvent(doc = PICK_UP_GESTURE_TRIGGERED)
FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED(1153, PICK_UP_GESTURE_TRIGGERED),
-
@UiEvent(doc = ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
- FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154,
- ALTERNATE_BIOMETRIC_BOUNCER_SHOWN),
-
+ FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, ALTERNATE_BIOMETRIC_BOUNCER_SHOWN),
@UiEvent(doc = PRIMARY_BOUNCER_SHOWN)
FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN(1155, PRIMARY_BOUNCER_SHOWN),
-
@UiEvent(doc = PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN)
FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN(
1197,
PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN
),
-
@UiEvent(doc = RETRY_AFTER_HW_UNAVAILABLE)
FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE(1156, RETRY_AFTER_HW_UNAVAILABLE),
-
- @UiEvent(doc = TRUST_DISABLED)
- FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED),
-
- @UiEvent(doc = TRUST_ENABLED)
- FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED),
-
+ @UiEvent(doc = TRUST_DISABLED) FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED),
+ @UiEvent(doc = TRUST_ENABLED) FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED),
@UiEvent(doc = KEYGUARD_OCCLUSION_CHANGED)
FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED(1159, KEYGUARD_OCCLUSION_CHANGED),
-
@UiEvent(doc = ASSISTANT_VISIBILITY_CHANGED)
FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED(1160, ASSISTANT_VISIBILITY_CHANGED),
-
@UiEvent(doc = STARTED_WAKING_UP)
- FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP),
-
+ FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP) {
+ override fun extraInfoToString(): String {
+ return PowerManager.wakeReasonToString(extraInfo)
+ }
+ },
+ @Deprecated(
+ "Not a face auth trigger.",
+ ReplaceWith(
+ "FACE_AUTH_UPDATED_STARTED_WAKING_UP, " +
+ "extraInfo=PowerManager.WAKE_REASON_DREAM_FINISHED"
+ )
+ )
@UiEvent(doc = DREAM_STOPPED)
FACE_AUTH_TRIGGERED_DREAM_STOPPED(1162, DREAM_STOPPED),
-
@UiEvent(doc = ALL_AUTHENTICATORS_REGISTERED)
FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED(1163, ALL_AUTHENTICATORS_REGISTERED),
-
@UiEvent(doc = ENROLLMENTS_CHANGED)
FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED(1164, ENROLLMENTS_CHANGED),
-
@UiEvent(doc = KEYGUARD_VISIBILITY_CHANGED)
FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED(1165, KEYGUARD_VISIBILITY_CHANGED),
-
@UiEvent(doc = FACE_CANCEL_NOT_RECEIVED)
FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED(1174, FACE_CANCEL_NOT_RECEIVED),
-
@UiEvent(doc = AUTH_REQUEST_DURING_CANCELLATION)
FACE_AUTH_TRIGGERED_DURING_CANCELLATION(1175, AUTH_REQUEST_DURING_CANCELLATION),
-
- @UiEvent(doc = DREAM_STARTED)
- FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED),
-
- @UiEvent(doc = FP_LOCKED_OUT)
- FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT),
-
+ @UiEvent(doc = DREAM_STARTED) FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED),
+ @UiEvent(doc = FP_LOCKED_OUT) FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT),
@UiEvent(doc = FACE_AUTH_STOPPED_ON_USER_INPUT)
FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER(1178, FACE_AUTH_STOPPED_ON_USER_INPUT),
-
@UiEvent(doc = KEYGUARD_GOING_AWAY)
FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY(1179, KEYGUARD_GOING_AWAY),
-
- @UiEvent(doc = CAMERA_LAUNCHED)
- FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED),
-
- @UiEvent(doc = FP_AUTHENTICATED)
- FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED),
-
- @UiEvent(doc = GOING_TO_SLEEP)
- FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP),
-
+ @UiEvent(doc = CAMERA_LAUNCHED) FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED),
+ @UiEvent(doc = FP_AUTHENTICATED) FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED),
+ @UiEvent(doc = GOING_TO_SLEEP) FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP),
@UiEvent(doc = FINISHED_GOING_TO_SLEEP)
FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP(1183, FINISHED_GOING_TO_SLEEP),
-
- @UiEvent(doc = KEYGUARD_INIT)
- FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT),
-
- @UiEvent(doc = KEYGUARD_RESET)
- FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET),
-
- @UiEvent(doc = USER_SWITCHING)
- FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING),
-
+ @UiEvent(doc = KEYGUARD_INIT) FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT),
+ @UiEvent(doc = KEYGUARD_RESET) FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET),
+ @UiEvent(doc = USER_SWITCHING) FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING),
@UiEvent(doc = FACE_AUTHENTICATED)
FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED(1187, FACE_AUTHENTICATED),
-
@UiEvent(doc = BIOMETRIC_ENABLED)
FACE_AUTH_UPDATED_BIOMETRIC_ENABLED_ON_KEYGUARD(1188, BIOMETRIC_ENABLED);
override fun getId(): Int = this.id
+
+ /** Convert [extraInfo] to a human-readable string. By default, this is empty. */
+ open fun extraInfoToString(): String = ""
}
private val apiRequestReasonToUiEvent =
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
new file mode 100644
index 000000000000..a0c43fba4bc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.keyguard
+
+import android.content.res.Resources
+import android.os.Build
+import android.os.PowerManager
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.GlobalSettings
+import java.io.PrintWriter
+import java.util.stream.Collectors
+import javax.inject.Inject
+
+/** Determines which device wake-ups should trigger face authentication. */
+@SysUISingleton
+class FaceWakeUpTriggersConfig
+@Inject
+constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpManager: DumpManager) :
+ Dumpable {
+ private val defaultTriggerFaceAuthOnWakeUpFrom: Set<Int> =
+ resources.getIntArray(R.array.config_face_auth_wake_up_triggers).toSet()
+ private val triggerFaceAuthOnWakeUpFrom: Set<Int>
+
+ init {
+ triggerFaceAuthOnWakeUpFrom =
+ if (Build.IS_DEBUGGABLE) {
+ // Update face wake triggers via adb on debuggable builds:
+ // ie: adb shell settings put global face_wake_triggers "1\|4" &&
+ // adb shell am crash com.android.systemui
+ processStringArray(
+ globalSettings.getString("face_wake_triggers"),
+ defaultTriggerFaceAuthOnWakeUpFrom
+ )
+ } else {
+ defaultTriggerFaceAuthOnWakeUpFrom
+ }
+ dumpManager.registerDumpable(this)
+ }
+
+ fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean {
+ return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.println("FaceWakeUpTriggers:")
+ for (pmWakeReason in triggerFaceAuthOnWakeUpFrom) {
+ pw.println(" ${PowerManager.wakeReasonToString(pmWakeReason)}")
+ }
+ }
+
+ /** Convert a pipe-separated set of integers into a set of ints. */
+ private fun processStringArray(stringSetting: String?, default: Set<Int>): Set<Int> {
+ return stringSetting?.let {
+ stringSetting.split("|").stream().map(Integer::parseInt).collect(Collectors.toSet())
+ }
+ ?: default
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index aff9dcbc26e3..cc1f2fe5b027 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -44,7 +44,6 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_TRUST_ENABL
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN;
-import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DREAM_STOPPED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DURING_CANCELLATION;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED;
import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET;
@@ -287,6 +286,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
}
}
};
+ private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
HashMap<Integer, SimData> mSimDatas = new HashMap<>();
HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
@@ -1807,11 +1807,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
}
}
- protected void handleStartedWakingUp() {
+ protected void handleStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp");
Assert.isMainThread();
- updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_STARTED_WAKING_UP);
- requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp");
+
+ updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+ if (mFaceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(pmWakeReason)) {
+ FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason);
+ updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+ FACE_AUTH_UPDATED_STARTED_WAKING_UP);
+ requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - "
+ + PowerManager.wakeReasonToString(pmWakeReason));
+ } else {
+ mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason);
+ }
+
for (int i = 0; i < mCallbacks.size(); i++) {
KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
if (cb != null) {
@@ -1863,12 +1873,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
cb.onDreamingStateChanged(mIsDreaming);
}
}
+ updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
if (mIsDreaming) {
- updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_DREAM_STARTED);
- } else {
- updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
- FACE_AUTH_TRIGGERED_DREAM_STOPPED);
}
}
@@ -1948,7 +1955,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
PackageManager packageManager,
@Nullable FaceManager faceManager,
@Nullable FingerprintManager fingerprintManager,
- @Nullable BiometricManager biometricManager) {
+ @Nullable BiometricManager biometricManager,
+ FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) {
mContext = context;
mSubscriptionManager = subscriptionManager;
mTelephonyListenerManager = telephonyListenerManager;
@@ -1987,6 +1995,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
R.array.config_face_acquire_device_entry_ignorelist))
.boxed()
.collect(Collectors.toSet());
+ mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig;
mHandler = new Handler(mainLooper) {
@Override
@@ -2036,7 +2045,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
break;
case MSG_STARTED_WAKING_UP:
Trace.beginSection("KeyguardUpdateMonitor#handler MSG_STARTED_WAKING_UP");
- handleStartedWakingUp();
+ handleStartedWakingUp(msg.arg1);
Trace.endSection();
break;
case MSG_SIM_SUBSCRIPTION_INFO_CHANGED:
@@ -2227,8 +2236,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
private void updateFaceEnrolled(int userId) {
mIsFaceEnrolled = whitelistIpcs(
() -> mFaceManager != null && mFaceManager.isHardwareDetected()
- && mFaceManager.hasEnrolledTemplates(userId)
- && mBiometricEnabledForUser.get(userId));
+ && mBiometricEnabledForUser.get(userId))
+ && mAuthController.isFaceAuthEnrolled(userId);
}
public boolean isFaceSupported() {
@@ -2784,8 +2793,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
// Waiting for ERROR_CANCELED before requesting auth again
return;
}
- mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason());
- mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId());
+ mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent);
+ mUiEventLogger.logWithInstanceIdAndPosition(
+ faceAuthUiEvent,
+ 0,
+ null,
+ getKeyguardSessionId(),
+ faceAuthUiEvent.getExtraInfo()
+ );
if (unlockPossible) {
mFaceCancelSignal = new CancellationSignal();
@@ -3564,11 +3579,16 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
// TODO: use these callbacks elsewhere in place of the existing notifyScreen*()
// (KeyguardViewMediator, KeyguardHostView)
- public void dispatchStartedWakingUp() {
+ /**
+ * Dispatch wakeup events to:
+ * - update biometric listening states
+ * - send to registered KeyguardUpdateMonitorCallbacks
+ */
+ public void dispatchStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
synchronized (this) {
mDeviceInteractive = true;
}
- mHandler.sendEmptyMessage(MSG_STARTED_WAKING_UP);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_WAKING_UP, pmWakeReason, 0));
}
public void dispatchStartedGoingToSleep(int why) {
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 70758dfec932..8fbbd3840964 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -24,6 +24,8 @@ import static com.android.keyguard.LockIconView.ICON_LOCK;
import static com.android.keyguard.LockIconView.ICON_UNLOCK;
import static com.android.systemui.classifier.Classifier.LOCK_ICON;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -46,6 +48,7 @@ import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.android.systemui.Dumpable;
@@ -55,6 +58,10 @@ import com.android.systemui.biometrics.AuthRippleController;
import com.android.systemui.biometrics.UdfpsController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
@@ -67,6 +74,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.PrintWriter;
import java.util.Objects;
+import java.util.function.Consumer;
import javax.inject.Inject;
@@ -103,6 +111,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
@NonNull private CharSequence mLockedLabel;
@NonNull private final VibratorHelper mVibrator;
@Nullable private final AuthRippleController mAuthRippleController;
+ @NonNull private final FeatureFlags mFeatureFlags;
+ @NonNull private final KeyguardTransitionInteractor mTransitionInteractor;
+ @NonNull private final KeyguardInteractor mKeyguardInteractor;
// Tracks the velocity of a touch to help filter out the touches that move too fast.
private VelocityTracker mVelocityTracker;
@@ -139,6 +150,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
private boolean mDownDetected;
private final Rect mSensorTouchLocation = new Rect();
+ @VisibleForTesting
+ final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> {
+ mInterpolatedDarkAmount = step.getValue();
+ mView.setDozeAmount(step.getValue());
+ updateBurnInOffsets();
+ };
+
+ @VisibleForTesting
+ final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> {
+ mIsDozing = isDozing;
+ updateBurnInOffsets();
+ updateVisibility();
+ };
+
@Inject
public LockIconViewController(
@Nullable LockIconView view,
@@ -154,7 +179,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
@NonNull @Main DelayableExecutor executor,
@NonNull VibratorHelper vibrator,
@Nullable AuthRippleController authRippleController,
- @NonNull @Main Resources resources
+ @NonNull @Main Resources resources,
+ @NonNull KeyguardTransitionInteractor transitionInteractor,
+ @NonNull KeyguardInteractor keyguardInteractor,
+ @NonNull FeatureFlags featureFlags
) {
super(view);
mStatusBarStateController = statusBarStateController;
@@ -168,6 +196,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
mExecutor = executor;
mVibrator = vibrator;
mAuthRippleController = authRippleController;
+ mTransitionInteractor = transitionInteractor;
+ mKeyguardInteractor = keyguardInteractor;
+ mFeatureFlags = featureFlags;
mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
@@ -184,6 +215,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
@Override
protected void onInit() {
mView.setAccessibilityDelegate(mAccessibilityDelegate);
+
+ if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(),
+ mDozeTransitionCallback);
+ collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback);
+ }
}
@Override
@@ -379,14 +416,17 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
pw.println(" mShowLockIcon: " + mShowLockIcon);
pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon);
- pw.println(" mIsDozing: " + mIsDozing);
- pw.println(" mIsBouncerShowing: " + mIsBouncerShowing);
- pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
- pw.println(" mRunningFPS: " + mRunningFPS);
- pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
- pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
- pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
- pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
+ pw.println();
+ pw.println(" mIsDozing: " + mIsDozing);
+ pw.println(" isFlagEnabled(DOZING_MIGRATION_1): "
+ + mFeatureFlags.isEnabled(DOZING_MIGRATION_1));
+ pw.println(" mIsBouncerShowing: " + mIsBouncerShowing);
+ pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
+ pw.println(" mRunningFPS: " + mRunningFPS);
+ pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
+ pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
+ pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
+ pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
if (mView != null) {
mView.dump(pw, args);
@@ -427,16 +467,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme
new StatusBarStateController.StateListener() {
@Override
public void onDozeAmountChanged(float linear, float eased) {
- mInterpolatedDarkAmount = eased;
- mView.setDozeAmount(eased);
- updateBurnInOffsets();
+ if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ mInterpolatedDarkAmount = eased;
+ mView.setDozeAmount(eased);
+ updateBurnInOffsets();
+ }
}
@Override
public void onDozingChanged(boolean isDozing) {
- mIsDozing = isDozing;
- updateBurnInOffsets();
- updateVisibility();
+ if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+ mIsDozing = isDozing;
+ updateBurnInOffsets();
+ updateVisibility();
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 2f79e30a0b5b..31fc3204a7f1 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -17,9 +17,12 @@
package com.android.keyguard.logging
import android.hardware.biometrics.BiometricConstants.LockoutMode
+import android.os.PowerManager
+import android.os.PowerManager.WakeReason
import android.telephony.ServiceState
import android.telephony.SubscriptionInfo
import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.FaceAuthUiEvent
import com.android.keyguard.KeyguardListenModel
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.plugins.log.LogBuffer
@@ -269,11 +272,19 @@ class KeyguardUpdateMonitorLogger @Inject constructor(
logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
}
- fun logStartedListeningForFace(faceRunningState: Int, faceAuthReason: String) {
+ fun logStartedListeningForFace(faceRunningState: Int, faceAuthUiEvent: FaceAuthUiEvent) {
logBuffer.log(TAG, VERBOSE, {
int1 = faceRunningState
- str1 = faceAuthReason
- }, { "startListeningForFace(): $int1, reason: $str1" })
+ str1 = faceAuthUiEvent.reason
+ str2 = faceAuthUiEvent.extraInfoToString()
+ }, { "startListeningForFace(): $int1, reason: $str1 $str2" })
+ }
+
+ fun logStartedListeningForFaceFromWakeUp(faceRunningState: Int, @WakeReason pmWakeReason: Int) {
+ logBuffer.log(TAG, VERBOSE, {
+ int1 = faceRunningState
+ str1 = PowerManager.wakeReasonToString(pmWakeReason)
+ }, { "startListeningForFace(): $int1, reason: wakeUp-$str1" })
}
fun logStoppedListeningForFace(faceRunningState: Int, faceAuthReason: String) {
@@ -383,4 +394,10 @@ class KeyguardUpdateMonitorLogger @Inject constructor(
}, { "#update secure=$bool1 canDismissKeyguard=$bool2" +
" trusted=$bool3 trustManaged=$bool4" })
}
+
+ fun logSkipUpdateFaceListeningOnWakeup(@WakeReason pmWakeReason: Int) {
+ logBuffer.log(TAG, VERBOSE, {
+ str1 = PowerManager.wakeReasonToString(pmWakeReason)
+ }, { "Skip updating face listening state on wakeup from $str1"})
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index ea334b27fa09..777d10c7acfd 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -28,6 +28,7 @@ import android.os.UserHandle;
import android.text.TextUtils;
import android.view.Display;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
import androidx.annotation.MainThread;
@@ -56,6 +57,7 @@ public class AccessibilityFloatingMenuController implements
private Context mContext;
private final WindowManager mWindowManager;
private final DisplayManager mDisplayManager;
+ private final AccessibilityManager mAccessibilityManager;
private final FeatureFlags mFeatureFlags;
@VisibleForTesting
IAccessibilityFloatingMenu mFloatingMenu;
@@ -96,6 +98,7 @@ public class AccessibilityFloatingMenuController implements
public AccessibilityFloatingMenuController(Context context,
WindowManager windowManager,
DisplayManager displayManager,
+ AccessibilityManager accessibilityManager,
AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver,
AccessibilityButtonModeObserver accessibilityButtonModeObserver,
KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -103,6 +106,7 @@ public class AccessibilityFloatingMenuController implements
mContext = context;
mWindowManager = windowManager;
mDisplayManager = displayManager;
+ mAccessibilityManager = accessibilityManager;
mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver;
mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -180,7 +184,8 @@ public class AccessibilityFloatingMenuController implements
final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
mFloatingMenu = new MenuViewLayerController(
mContext.createWindowContext(defaultDisplay,
- TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager);
+ TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager,
+ mAccessibilityManager);
} else {
mFloatingMenu = new AccessibilityFloatingMenu(mContext);
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
new file mode 100644
index 000000000000..ee048e1a02d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.accessibility.floatingmenu;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.ComponentCallbacks;
+import android.content.res.Configuration;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+
+import com.android.systemui.R;
+import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
+/**
+ * Controls the interaction between {@link MagnetizedObject} and
+ * {@link MagnetizedObject.MagneticTarget}.
+ */
+class DismissAnimationController implements ComponentCallbacks {
+ private static final float COMPLETELY_OPAQUE = 1.0f;
+ private static final float COMPLETELY_TRANSPARENT = 0.0f;
+ private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f;
+ private static final float ANIMATING_MAX_ALPHA = 0.7f;
+
+ private final DismissView mDismissView;
+ private final MenuView mMenuView;
+ private final ValueAnimator mDismissAnimator;
+ private final MagnetizedObject<?> mMagnetizedObject;
+ private float mMinDismissSize;
+ private float mSizePercent;
+
+ DismissAnimationController(DismissView dismissView, MenuView menuView) {
+ mDismissView = dismissView;
+ mDismissView.setPivotX(dismissView.getWidth() / 2.0f);
+ mDismissView.setPivotY(dismissView.getHeight() / 2.0f);
+ mMenuView = menuView;
+
+ updateResources();
+
+ mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT);
+ mDismissAnimator.addUpdateListener(dismissAnimation -> {
+ final float animatedValue = (float) dismissAnimation.getAnimatedValue();
+ final float scaleValue = Math.max(animatedValue, mSizePercent);
+ dismissView.getCircle().setScaleX(scaleValue);
+ dismissView.getCircle().setScaleY(scaleValue);
+
+ menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA));
+ });
+
+ mDismissAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) {
+ super.onAnimationEnd(animation, isReverse);
+
+ if (isReverse) {
+ mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE);
+ mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE);
+ mMenuView.setAlpha(COMPLETELY_OPAQUE);
+ }
+ }
+ });
+
+ mMagnetizedObject =
+ new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView,
+ new MenuAnimationController.MenuPositionProperty(
+ DynamicAnimation.TRANSLATION_X),
+ new MenuAnimationController.MenuPositionProperty(
+ DynamicAnimation.TRANSLATION_Y)) {
+ @Override
+ public void getLocationOnScreen(MenuView underlyingObject, int[] loc) {
+ underlyingObject.getLocationOnScreen(loc);
+ }
+
+ @Override
+ public float getHeight(MenuView underlyingObject) {
+ return underlyingObject.getHeight();
+ }
+
+ @Override
+ public float getWidth(MenuView underlyingObject) {
+ return underlyingObject.getWidth();
+ }
+ };
+
+ final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget(
+ dismissView.getCircle(), (int) mMinDismissSize);
+ mMagnetizedObject.addTarget(magneticTarget);
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ updateResources();
+ }
+
+ @Override
+ public void onLowMemory() {
+ // Do nothing
+ }
+
+ void showDismissView(boolean show) {
+ if (show) {
+ mDismissView.show();
+ } else {
+ mDismissView.hide();
+ }
+ }
+
+ void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) {
+ mMagnetizedObject.setMagnetListener(magnetListener);
+ }
+
+ void maybeConsumeDownMotionEvent(MotionEvent event) {
+ mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
+ /**
+ * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was
+ * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
+ *
+ * @param event that move the magnetized object which is also the menu list view.
+ * @return true if the location of the motion events moves within the magnetic field of a
+ * target, but false if didn't set
+ * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
+ */
+ boolean maybeConsumeMoveMotionEvent(MotionEvent event) {
+ return mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
+ /**
+ * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was
+ * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}.
+ *
+ * @param event that move the magnetized object which is also the menu list view.
+ * @return true if the location of the motion events moves within the magnetic field of a
+ * target, but false if didn't set
+ * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
+ */
+ boolean maybeConsumeUpMotionEvent(MotionEvent event) {
+ return mMagnetizedObject.maybeConsumeMotionEvent(event);
+ }
+
+ void animateDismissMenu(boolean scaleUp) {
+ if (scaleUp) {
+ mDismissAnimator.start();
+ } else {
+ mDismissAnimator.reverse();
+ }
+ }
+
+ private void updateResources() {
+ final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize(
+ R.dimen.dismiss_circle_size);
+ mMinDismissSize = mDismissView.getResources().getDimensionPixelSize(
+ R.dimen.dismiss_circle_small);
+ mSizePercent = mMinDismissSize / maxDismissSize;
+ }
+
+ interface DismissCallback {
+ void onDismiss();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index d6d039903505..396f584d76a6 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -35,6 +35,8 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.internal.util.Preconditions;
+
import java.util.HashMap;
/**
@@ -47,6 +49,9 @@ class MenuAnimationController {
private static final float MIN_PERCENT = 0.0f;
private static final float MAX_PERCENT = 1.0f;
private static final float COMPLETELY_OPAQUE = 1.0f;
+ private static final float COMPLETELY_TRANSPARENT = 0.0f;
+ private static final float SCALE_SHRINK = 0.0f;
+ private static final float SCALE_GROW = 1.0f;
private static final float FLING_FRICTION_SCALAR = 1.9f;
private static final float DEFAULT_FRICTION = 4.2f;
private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
@@ -61,6 +66,7 @@ class MenuAnimationController {
private final Handler mHandler;
private boolean mIsMovedToEdge;
private boolean mIsFadeEffectEnabled;
+ private DismissAnimationController.DismissCallback mDismissCallback;
// Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
// DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
@@ -99,6 +105,11 @@ class MenuAnimationController {
}
}
+ void setDismissCallback(
+ DismissAnimationController.DismissCallback dismissCallback) {
+ mDismissCallback = dismissCallback;
+ }
+
void moveToTopLeftPosition() {
mIsMovedToEdge = false;
final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
@@ -129,6 +140,13 @@ class MenuAnimationController {
constrainPositionAndUpdate(position);
}
+ void removeMenu() {
+ Preconditions.checkArgument(mDismissCallback != null,
+ "The dismiss callback should be initialized first.");
+
+ mDismissCallback.onDismiss();
+ }
+
void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) {
final boolean shouldMenuFlingLeft = isOnLeftSide()
? velocityX < ESCAPE_VELOCITY
@@ -297,6 +315,28 @@ class MenuAnimationController {
mMenuView.onDraggingStart();
}
+ void startShrinkAnimation(Runnable endAction) {
+ mMenuView.animate().cancel();
+
+ mMenuView.animate()
+ .scaleX(SCALE_SHRINK)
+ .scaleY(SCALE_SHRINK)
+ .alpha(COMPLETELY_TRANSPARENT)
+ .translationY(mMenuView.getTranslationY())
+ .withEndAction(endAction).start();
+ }
+
+ void startGrowAnimation() {
+ mMenuView.animate().cancel();
+
+ mMenuView.animate()
+ .scaleX(SCALE_GROW)
+ .scaleY(SCALE_GROW)
+ .alpha(COMPLETELY_OPAQUE)
+ .translationY(mMenuView.getTranslationY())
+ .start();
+ }
+
private void onSpringAnimationEnd(PointF position) {
mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
constrainPositionAndUpdate(position);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
index e69a24810fdc..ac5736b0c26d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
@@ -84,6 +84,12 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It
new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
res.getString(moveEdgeTextResId));
info.addAction(moveToOrOutEdge);
+
+ final AccessibilityNodeInfoCompat.AccessibilityActionCompat removeMenu =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ R.id.action_remove_menu,
+ res.getString(R.string.accessibility_floating_button_action_remove_menu));
+ info.addAction(removeMenu);
}
@Override
@@ -126,6 +132,11 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It
return true;
}
+ if (action == R.id.action_remove_menu) {
+ mAnimationController.removeMenu();
+ return true;
+ }
+
return super.performAccessibilityAction(host, action, args);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
index 3146c9f0d2af..bc3cf0a6bab0 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
@@ -38,9 +38,12 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
private final PointF mMenuTranslationDown = new PointF();
private boolean mIsDragging = false;
private float mTouchSlop;
+ private final DismissAnimationController mDismissAnimationController;
- MenuListViewTouchHandler(MenuAnimationController menuAnimationController) {
+ MenuListViewTouchHandler(MenuAnimationController menuAnimationController,
+ DismissAnimationController dismissAnimationController) {
mMenuAnimationController = menuAnimationController;
+ mDismissAnimationController = dismissAnimationController;
}
@Override
@@ -61,6 +64,7 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY());
mMenuAnimationController.cancelAnimations();
+ mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent);
break;
case MotionEvent.ACTION_MOVE:
if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
@@ -69,8 +73,13 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
mMenuAnimationController.onDraggingStart();
}
- mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
- mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy);
+ mDismissAnimationController.showDismissView(/* show= */ true);
+
+ if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) {
+ mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
+ mMenuAnimationController.moveToPositionYIfNeeded(
+ mMenuTranslationDown.y + dy);
+ }
}
break;
case MotionEvent.ACTION_UP:
@@ -79,10 +88,18 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
final float endX = mMenuTranslationDown.x + dx;
mIsDragging = false;
- if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
+ if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
+ mDismissAnimationController.showDismissView(/* show= */ false);
+ mMenuAnimationController.fadeOutIfEnabled();
+
+ return true;
+ }
+
+ if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) {
mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS);
mMenuAnimationController.flingMenuThenSpringToEdge(endX,
mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+ mDismissAnimationController.showDismissView(/* show= */ false);
}
// Avoid triggering the listener of the item.
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
new file mode 100644
index 000000000000..9875ad06f1ed
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.accessibility.floatingmenu;
+
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The message view with the action prompt to whether to undo operation for users when removing
+ * the {@link MenuView}.
+ */
+class MenuMessageView extends LinearLayout implements
+ ViewTreeObserver.OnComputeInternalInsetsListener {
+ private final TextView mTextView;
+ private final Button mUndoButton;
+
+ @IntDef({
+ Index.TEXT_VIEW,
+ Index.UNDO_BUTTON
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Index {
+ int TEXT_VIEW = 0;
+ int UNDO_BUTTON = 1;
+ }
+
+ MenuMessageView(Context context) {
+ super(context);
+
+ setVisibility(GONE);
+
+ mTextView = new TextView(context);
+ mUndoButton = new Button(context);
+
+ addView(mTextView, Index.TEXT_VIEW,
+ new LayoutParams(/* width= */ 0, WRAP_CONTENT, /* weight= */ 1));
+ addView(mUndoButton, Index.UNDO_BUTTON, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ updateResources();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ final FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(WRAP_CONTENT,
+ WRAP_CONTENT);
+ containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+ setLayoutParams(containerParams);
+ setGravity(Gravity.CENTER_VERTICAL);
+
+ mUndoButton.setBackground(null);
+
+ updateResources();
+
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+ }
+
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+
+ if (getVisibility() == VISIBLE) {
+ final int x = (int) getX();
+ final int y = (int) getY();
+ inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight()));
+ }
+ }
+
+ /**
+ * Registers a listener to be invoked when this undo action button is clicked. It should be
+ * called after {@link View#onAttachedToWindow()}.
+ *
+ * @param listener The listener that will run
+ */
+ void setUndoListener(OnClickListener listener) {
+ mUndoButton.setOnClickListener(listener);
+ }
+
+ private void updateResources() {
+ final Resources res = getResources();
+
+ final int containerPadding =
+ res.getDimensionPixelSize(
+ R.dimen.accessibility_floating_menu_message_container_horizontal_padding);
+ final int margin = res.getDimensionPixelSize(
+ R.dimen.accessibility_floating_menu_message_margin);
+ final FrameLayout.LayoutParams containerParams =
+ (FrameLayout.LayoutParams) getLayoutParams();
+ containerParams.setMargins(margin, margin, margin, margin);
+ setLayoutParams(containerParams);
+ setBackground(res.getDrawable(R.drawable.accessibility_floating_message_background));
+ setPadding(containerPadding, /* top= */ 0, containerPadding, /* bottom= */ 0);
+ setMinimumWidth(
+ res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_width));
+ setMinimumHeight(
+ res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_height));
+ setElevation(
+ res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_elevation));
+
+ final int textPadding =
+ res.getDimensionPixelSize(
+ R.dimen.accessibility_floating_menu_message_text_vertical_padding);
+ final int textColor = res.getColor(R.color.accessibility_floating_menu_message_text);
+ final int textSize = res.getDimensionPixelSize(
+ R.dimen.accessibility_floating_menu_message_text_size);
+ mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding);
+ mTextView.setTextSize(COMPLEX_UNIT_PX, textSize);
+ mTextView.setTextColor(textColor);
+
+ final ColorStateList colorAccent = Utils.getColorAccent(getContext());
+ mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo));
+ mUndoButton.setTextSize(COMPLEX_UNIT_PX, textSize);
+ mUndoButton.setTextColor(colorAccent);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index 15d139cf15da..6a14af52fbaf 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -42,7 +42,7 @@ import java.util.Collections;
import java.util.List;
/**
- * The menu view displays the accessibility features.
+ * The container view displays the accessibility features.
*/
@SuppressLint("ViewConstructor")
class MenuView extends FrameLayout implements
@@ -64,13 +64,14 @@ class MenuView extends FrameLayout implements
this::onTargetFeaturesChanged;
private final MenuViewAppearance mMenuViewAppearance;
+ private OnTargetFeaturesChangeListener mFeaturesChangeListener;
+
MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) {
super(context);
mMenuViewModel = menuViewModel;
mMenuViewAppearance = menuViewAppearance;
mMenuAnimationController = new MenuAnimationController(this);
-
mAdapter = new AccessibilityTargetAdapter(mTargetFeatures);
mTargetFeaturesView = new RecyclerView(context);
mTargetFeaturesView.setAdapter(mAdapter);
@@ -96,7 +97,9 @@ class MenuView extends FrameLayout implements
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
- inoutInfo.touchableRegion.set(mBoundsInParent);
+ if (getVisibility() == VISIBLE) {
+ inoutInfo.touchableRegion.union(mBoundsInParent);
+ }
}
@Override
@@ -108,10 +111,18 @@ class MenuView extends FrameLayout implements
mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
}
+ void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) {
+ mFeaturesChangeListener = listener;
+ }
+
void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
mTargetFeaturesView.addOnItemTouchListener(listener);
}
+ MenuAnimationController getMenuAnimationController() {
+ return mMenuAnimationController;
+ }
+
@SuppressLint("NotifyDataSetChanged")
private void onItemSizeChanged() {
mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding());
@@ -139,7 +150,7 @@ class MenuView extends FrameLayout implements
onEdgeChanged();
}
- private void onEdgeChanged() {
+ void onEdgeChanged() {
final int[] insets = mMenuViewAppearance.getMenuInsets();
getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
insets[3]);
@@ -193,6 +204,9 @@ class MenuView extends FrameLayout implements
onEdgeChanged();
onPositionChanged();
+ if (mFeaturesChangeListener != null) {
+ mFeaturesChangeListener.onChange(newTargetFeatures);
+ }
mMenuAnimationController.fadeOutIfEnabled();
}
@@ -299,4 +313,17 @@ class MenuView extends FrameLayout implements
final ViewGroup parentView = (ViewGroup) getParent();
parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent));
}
+
+ /**
+ * Interface definition for the {@link AccessibilityTarget} list changes.
+ */
+ interface OnTargetFeaturesChangeListener {
+ /**
+ * Called when the list of accessibility target features was updated. This will be
+ * invoked when the end of {@code onTargetFeaturesChanged}.
+ *
+ * @param newTargetFeatures the list related to the current accessibility features.
+ */
+ void onChange(List<AccessibilityTarget> newTargetFeatures);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 5252519e9faf..33e155df80e3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -16,42 +16,155 @@
package com.android.systemui.accessibility.floatingmenu;
+import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
+
import android.annotation.IntDef;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.PluralsMessageFormatter;
import android.view.MotionEvent;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
+import android.widget.TextView;
import androidx.annotation.NonNull;
+import com.android.internal.accessibility.dialog.AccessibilityTarget;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.systemui.R;
+import com.android.wm.shell.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
- * The basic interactions with the child view {@link MenuView}.
+ * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and
+ * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the
+ * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu
+ * message view would be shown and allowed users to undo it.
*/
@SuppressLint("ViewConstructor")
class MenuViewLayer extends FrameLayout {
+ private static final int SHOW_MESSAGE_DELAY_MS = 3000;
+
private final MenuView mMenuView;
+ private final MenuMessageView mMessageView;
+ private final DismissView mDismissView;
+ private final MenuAnimationController mMenuAnimationController;
+ private final AccessibilityManager mAccessibilityManager;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final IAccessibilityFloatingMenu mFloatingMenu;
+ private final DismissAnimationController mDismissAnimationController;
@IntDef({
- LayerIndex.MENU_VIEW
+ LayerIndex.MENU_VIEW,
+ LayerIndex.DISMISS_VIEW,
+ LayerIndex.MESSAGE_VIEW,
})
@Retention(RetentionPolicy.SOURCE)
@interface LayerIndex {
int MENU_VIEW = 0;
+ int DISMISS_VIEW = 1;
+ int MESSAGE_VIEW = 2;
}
- MenuViewLayer(@NonNull Context context, WindowManager windowManager) {
+ @VisibleForTesting
+ final Runnable mDismissMenuAction = new Runnable() {
+ @Override
+ public void run() {
+ Settings.Secure.putString(getContext().getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ "");
+ mFloatingMenu.hide();
+ }
+ };
+
+ MenuViewLayer(@NonNull Context context, WindowManager windowManager,
+ AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) {
super(context);
+ mAccessibilityManager = accessibilityManager;
+ mFloatingMenu = floatingMenu;
+
final MenuViewModel menuViewModel = new MenuViewModel(context);
final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context,
windowManager);
mMenuView = new MenuView(context, menuViewModel, menuViewAppearance);
+ mMenuAnimationController = mMenuView.getMenuAnimationController();
+ mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+
+ mDismissView = new DismissView(context);
+ mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
+ mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true);
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ float velocityX, float velocityY, boolean wasFlungOut) {
+ mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false);
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+ hideMenuAndShowMessage();
+ mDismissView.hide();
+ mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false);
+ }
+ });
+
+ final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler(
+ mMenuAnimationController, mDismissAnimationController);
+ mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler);
+
+ mMessageView = new MenuMessageView(context);
+
+ mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
+ if (newTargetFeatures.size() < 1) {
+ return;
+ }
+
+ // During the undo action period, the pending action will be canceled and undo back
+ // to the previous state if users did any action related to the accessibility features.
+ if (mMessageView.getVisibility() == VISIBLE) {
+ undo();
+ }
+
+ final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
+ messageText.setText(getMessageText(newTargetFeatures));
+ });
addView(mMenuView, LayerIndex.MENU_VIEW);
+ addView(mDismissView, LayerIndex.DISMISS_VIEW);
+ addView(mMessageView, LayerIndex.MESSAGE_VIEW);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDismissView.updateResources();
+ }
+
+ private String getMessageText(List<AccessibilityTarget> newTargetFeatures) {
+ Preconditions.checkArgument(newTargetFeatures.size() > 0,
+ "The list should at least have one feature.");
+
+ final Map<String, Object> arguments = new HashMap<>();
+ arguments.put("count", newTargetFeatures.size());
+ arguments.put("label", newTargetFeatures.get(0).getLabel());
+ return PluralsMessageFormatter.format(getResources(), arguments,
+ R.string.accessibility_floating_button_undo_message_text);
}
@Override
@@ -68,6 +181,8 @@ class MenuViewLayer extends FrameLayout {
super.onAttachedToWindow();
mMenuView.show();
+ mMessageView.setUndoListener(view -> undo());
+ mContext.registerComponentCallbacks(mDismissAnimationController);
}
@Override
@@ -75,5 +190,26 @@ class MenuViewLayer extends FrameLayout {
super.onDetachedFromWindow();
mMenuView.hide();
+ mHandler.removeCallbacksAndMessages(/* token= */ null);
+ mContext.unregisterComponentCallbacks(mDismissAnimationController);
+ }
+
+ private void hideMenuAndShowMessage() {
+ final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis(
+ SHOW_MESSAGE_DELAY_MS,
+ AccessibilityManager.FLAG_CONTENT_TEXT
+ | AccessibilityManager.FLAG_CONTENT_CONTROLS);
+ mHandler.postDelayed(mDismissMenuAction, delayTime);
+ mMessageView.setVisibility(VISIBLE);
+ mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
+ }
+
+ private void undo() {
+ mHandler.removeCallbacksAndMessages(/* token= */ null);
+ mMessageView.setVisibility(GONE);
+ mMenuView.onEdgeChanged();
+ mMenuView.onPositionChanged();
+ mMenuView.setVisibility(VISIBLE);
+ mMenuAnimationController.startGrowAnimation();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index d2093c200ca2..b1a64eda46ff 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.graphics.PixelFormat;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
/**
* Controls the {@link MenuViewLayer} whether to be attached to the window via the interface
@@ -32,9 +33,10 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu {
private final MenuViewLayer mMenuViewLayer;
private boolean mIsShowing;
- MenuViewLayerController(Context context, WindowManager windowManager) {
+ MenuViewLayerController(Context context, WindowManager windowManager,
+ AccessibilityManager accessibilityManager) {
mWindowManager = windowManager;
- mMenuViewLayer = new MenuViewLayer(context, windowManager);
+ mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index b50bfd7c24f9..f74c721bf114 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -26,6 +26,7 @@ import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.AlertDialog;
import android.content.Context;
import android.graphics.PixelFormat;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
@@ -63,6 +64,9 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.ui.CredentialView;
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -74,11 +78,13 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import javax.inject.Provider;
+
/**
* Top level container/controller for the BiometricPrompt UI.
*/
public class AuthContainerView extends LinearLayout
- implements AuthDialog, WakefulnessLifecycle.Observer {
+ implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host {
private static final String TAG = "AuthContainerView";
@@ -112,15 +118,18 @@ public class AuthContainerView extends LinearLayout
private final IBinder mWindowToken = new Binder();
private final WindowManager mWindowManager;
private final Interpolator mLinearOutSlowIn;
- private final CredentialCallback mCredentialCallback;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final InteractionJankMonitor mInteractionJankMonitor;
+ // TODO: these should be migrated out once ready
+ private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+ private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+
@VisibleForTesting final BiometricCallback mBiometricCallback;
@Nullable private AuthBiometricView mBiometricView;
- @Nullable private AuthCredentialView mCredentialView;
+ @Nullable private View mCredentialView;
private final AuthPanelController mPanelController;
private final FrameLayout mFrameLayout;
private final ImageView mBackgroundView;
@@ -229,11 +238,13 @@ public class AuthContainerView extends LinearLayout
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
- @NonNull InteractionJankMonitor jankMonitor) {
+ @NonNull InteractionJankMonitor jankMonitor,
+ @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
+ @NonNull Provider<CredentialViewModel> credentialViewModelProvider) {
mConfig.mSensorIds = sensorIds;
return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
- userManager, lockPatternUtils, jankMonitor, new Handler(Looper.getMainLooper()),
- bgExecutor);
+ userManager, lockPatternUtils, jankMonitor, biometricPromptInteractor,
+ credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor);
}
}
@@ -271,12 +282,49 @@ public class AuthContainerView extends LinearLayout
}
}
- final class CredentialCallback implements AuthCredentialView.Callback {
- @Override
- public void onCredentialMatched(byte[] attestation) {
- mCredentialAttestation = attestation;
- animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
- }
+ @Override
+ public void onCredentialMatched(@NonNull byte[] attestation) {
+ mCredentialAttestation = attestation;
+ animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED);
+ }
+
+ @Override
+ public void onCredentialAborted() {
+ sendEarlyUserCanceled();
+ animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
+ }
+
+ @Override
+ public void onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody) {
+ // Only show dialog if <=1 attempts are left before wiping.
+ if (remaining == 1) {
+ showLastAttemptBeforeWipeDialog(messageBody);
+ } else if (remaining <= 0) {
+ showNowWipingDialog(messageBody);
+ }
+ }
+
+ private void showLastAttemptBeforeWipeDialog(@NonNull String messageBody) {
+ final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+ .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
+ .setMessage(messageBody)
+ .setPositiveButton(android.R.string.ok, null)
+ .create();
+ alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+ alertDialog.show();
+ }
+
+ private void showNowWipingDialog(@NonNull String messageBody) {
+ final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
+ .setMessage(messageBody)
+ .setPositiveButton(
+ com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
+ null /* OnClickListener */)
+ .setOnDismissListener(
+ dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR))
+ .create();
+ alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+ alertDialog.show();
}
@VisibleForTesting
@@ -287,6 +335,8 @@ public class AuthContainerView extends LinearLayout
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
+ @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
+ @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull Handler mainHandler,
@NonNull @Background DelayableExecutor bgExecutor) {
super(config.mContext);
@@ -302,7 +352,6 @@ public class AuthContainerView extends LinearLayout
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mBiometricCallback = new BiometricCallback();
- mCredentialCallback = new CredentialCallback();
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
mFrameLayout = (FrameLayout) layoutInflater.inflate(
@@ -314,6 +363,8 @@ public class AuthContainerView extends LinearLayout
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
+ mBiometricPromptInteractor = biometricPromptInteractor;
+ mCredentialViewModelProvider = credentialViewModelProvider;
// Inflate biometric view only if necessary.
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
@@ -404,12 +455,12 @@ public class AuthContainerView extends LinearLayout
switch (credentialType) {
case Utils.CREDENTIAL_PATTERN:
- mCredentialView = (AuthCredentialView) factory.inflate(
+ mCredentialView = factory.inflate(
R.layout.auth_credential_pattern_view, null, false);
break;
case Utils.CREDENTIAL_PIN:
case Utils.CREDENTIAL_PASSWORD:
- mCredentialView = (AuthCredentialView) factory.inflate(
+ mCredentialView = factory.inflate(
R.layout.auth_credential_password_view, null, false);
break;
default:
@@ -422,16 +473,12 @@ public class AuthContainerView extends LinearLayout
mBackgroundView.setOnClickListener(null);
mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mCredentialView.setContainerView(this);
- mCredentialView.setUserId(mConfig.mUserId);
- mCredentialView.setOperationId(mConfig.mOperationId);
- mCredentialView.setEffectiveUserId(mEffectiveUserId);
- mCredentialView.setCredentialType(credentialType);
- mCredentialView.setCallback(mCredentialCallback);
- mCredentialView.setPromptInfo(mConfig.mPromptInfo);
- mCredentialView.setPanelController(mPanelController, animatePanel);
- mCredentialView.setShouldAnimateContents(animateContents);
- mCredentialView.setBackgroundExecutor(mBackgroundExecutor);
+ mBiometricPromptInteractor.get().useCredentialsForAuthentication(
+ mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId);
+ final CredentialViewModel vm = mCredentialViewModelProvider.get();
+ vm.setAnimateContents(animateContents);
+ ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel);
+
mFrameLayout.addView(mCredentialView);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 8c7e0efee7e6..313ff4157155 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -72,6 +72,8 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.os.SomeArgs;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.CoreStartable;
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -122,6 +124,10 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
private final Provider<UdfpsController> mUdfpsControllerFactory;
private final Provider<SidefpsController> mSidefpsControllerFactory;
+ // TODO: these should be migrated out once ready
+ @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+ @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+
private final Display mDisplay;
private float mScaleFactor = 1f;
// sensor locations without any resolution scaling nor rotation adjustments:
@@ -153,6 +159,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
@Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
@NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
+ @NonNull private final SparseBooleanArray mFaceEnrolledForUser;
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private boolean mAllFingerprintAuthenticatorsRegistered;
@@ -349,6 +356,15 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
}
}
}
+ if (mFaceProps == null) {
+ Log.d(TAG, "handleEnrollmentsChanged, mFaceProps is null");
+ } else {
+ for (FaceSensorPropertiesInternal prop : mFaceProps) {
+ if (prop.sensorId == sensorId) {
+ mFaceEnrolledForUser.put(userId, hasEnrollments);
+ }
+ }
+ }
for (Callback cb : mCallbacks) {
cb.onEnrollmentsChanged(modality);
}
@@ -683,6 +699,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull UdfpsLogger udfpsLogger,
@NonNull StatusBarStateController statusBarStateController,
+ @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
+ @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
@@ -704,8 +722,12 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
mWindowManager = windowManager;
mInteractionJankMonitor = jankMonitor;
mUdfpsEnrolledForUser = new SparseBooleanArray();
+ mFaceEnrolledForUser = new SparseBooleanArray();
mVibratorHelper = vibrator;
+ mBiometricPromptInteractor = biometricPromptInteractor;
+ mCredentialViewModelProvider = credentialViewModelProvider;
+
mOrientationListener = new BiometricDisplayListener(
context,
mDisplayManager,
@@ -1054,7 +1076,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
return false;
}
- return mFaceManager.hasEnrolledTemplates(userId);
+ return mFaceEnrolledForUser.get(userId);
}
/**
@@ -1068,6 +1090,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
return mUdfpsEnrolledForUser.get(userId);
}
+ /** If BiometricPrompt is currently being shown to the user. */
+ public boolean isShowing() {
+ return mCurrentDialog != null;
+ }
+
private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
mCurrentDialogArgs = args;
@@ -1199,7 +1226,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
.setMultiSensorConfig(multiSensorConfig)
.setScaleFactorProvider(() -> getScaleFactor())
.build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle,
- userManager, lockPatternUtils, mInteractionJankMonitor);
+ userManager, lockPatternUtils, mInteractionJankMonitor,
+ mBiometricPromptInteractor, mCredentialViewModelProvider);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
deleted file mode 100644
index 76cd3f4c4f1d..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.biometrics;
-
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.view.WindowInsets.Type.ime;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Insets;
-import android.os.UserHandle;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.View.OnApplyWindowInsetsListener;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.ImeAwareEditText;
-import android.widget.TextView;
-
-import com.android.internal.widget.LockPatternChecker;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.LockscreenCredential;
-import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.systemui.Dumpable;
-import com.android.systemui.R;
-
-import java.io.PrintWriter;
-
-/**
- * Pin and Password UI
- */
-public class AuthCredentialPasswordView extends AuthCredentialView
- implements TextView.OnEditorActionListener, OnApplyWindowInsetsListener, Dumpable {
-
- private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView";
-
- private final InputMethodManager mImm;
- private ImeAwareEditText mPasswordField;
- private ViewGroup mAuthCredentialHeader;
- private ViewGroup mAuthCredentialInput;
- private int mBottomInset = 0;
-
- public AuthCredentialPasswordView(Context context,
- AttributeSet attrs) {
- super(context, attrs);
- mImm = mContext.getSystemService(InputMethodManager.class);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mAuthCredentialHeader = findViewById(R.id.auth_credential_header);
- mAuthCredentialInput = findViewById(R.id.auth_credential_input);
- mPasswordField = findViewById(R.id.lockPassword);
- mPasswordField.setOnEditorActionListener(this);
- // TODO: De-dupe the logic with AuthContainerView
- mPasswordField.setOnKeyListener((v, keyCode, event) -> {
- if (keyCode != KeyEvent.KEYCODE_BACK) {
- return false;
- }
- if (event.getAction() == KeyEvent.ACTION_UP) {
- mContainerView.sendEarlyUserCanceled();
- mContainerView.animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED);
- }
- return true;
- });
-
- setOnApplyWindowInsetsListener(this);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- mPasswordField.setTextOperationUser(UserHandle.of(mUserId));
- if (mCredentialType == Utils.CREDENTIAL_PIN) {
- mPasswordField.setInputType(
- InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
- }
-
- mPasswordField.requestFocus();
- mPasswordField.scheduleShowSoftInput();
- }
-
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- // Check if this was the result of hitting the enter key
- final boolean isSoftImeEvent = event == null
- && (actionId == EditorInfo.IME_NULL
- || actionId == EditorInfo.IME_ACTION_DONE
- || actionId == EditorInfo.IME_ACTION_NEXT);
- final boolean isKeyboardEnterKey = event != null
- && KeyEvent.isConfirmKey(event.getKeyCode())
- && event.getAction() == KeyEvent.ACTION_DOWN;
- if (isSoftImeEvent || isKeyboardEnterKey) {
- checkPasswordAndUnlock();
- return true;
- }
- return false;
- }
-
- private void checkPasswordAndUnlock() {
- try (LockscreenCredential password = mCredentialType == Utils.CREDENTIAL_PIN
- ? LockscreenCredential.createPinOrNone(mPasswordField.getText())
- : LockscreenCredential.createPasswordOrNone(mPasswordField.getText())) {
- if (password.isNone()) {
- return;
- }
-
- // Request LockSettingsService to return the Gatekeeper Password in the
- // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
- // Gatekeeper Password and operationId.
- mPendingLockCheck = LockPatternChecker.verifyCredential(mLockPatternUtils,
- password, mEffectiveUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
- this::onCredentialVerified);
- }
- }
-
- @Override
- protected void onCredentialVerified(@NonNull VerifyCredentialResponse response,
- int timeoutMs) {
- super.onCredentialVerified(response, timeoutMs);
-
- if (response.isMatched()) {
- mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */);
- } else {
- mPasswordField.setText("");
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null
- || mDescriptionView == null || mPasswordField == null || mErrorView == null) {
- return;
- }
-
- int inputLeftBound;
- int inputTopBound;
- int headerRightBound = right;
- int headerTopBounds = top;
- final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom()
- : mSubtitleView.getBottom();
- final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom
- : mDescriptionView.getBottom();
- if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
- inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2;
- inputLeftBound = (right - left) / 2;
- headerRightBound = inputLeftBound;
- headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset);
- } else {
- inputTopBound =
- descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2;
- inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2;
- }
-
- if (mDescriptionView.getBottom() > mBottomInset) {
- mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom);
- }
- mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- final int newWidth = MeasureSpec.getSize(widthMeasureSpec);
- final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset;
-
- setMeasuredDimension(newWidth, newHeight);
-
- final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2,
- MeasureSpec.AT_MOST);
- final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED);
- if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
- measureChildren(halfWidthSpec, fullHeightSpec);
- } else {
- measureChildren(widthMeasureSpec, fullHeightSpec);
- }
- }
-
- @NonNull
- @Override
- public WindowInsets onApplyWindowInsets(@NonNull View v, WindowInsets insets) {
-
- final Insets bottomInset = insets.getInsets(ime());
- if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) {
- mBottomInset = bottomInset.bottom;
- if (mBottomInset > 0
- && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
- mTitleView.setSingleLine(true);
- mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
- mTitleView.setMarqueeRepeatLimit(-1);
- // select to enable marquee unless a screen reader is enabled
- mTitleView.setSelected(!mAccessibilityManager.isEnabled()
- || !mAccessibilityManager.isTouchExplorationEnabled());
- } else {
- mTitleView.setSingleLine(false);
- mTitleView.setEllipsize(null);
- // select to enable marquee unless a screen reader is enabled
- mTitleView.setSelected(false);
- }
- requestLayout();
- }
- return insets;
- }
-
- @Override
- public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
- pw.println(TAG + "State:");
- pw.println(" mBottomInset=" + mBottomInset);
- pw.println(" mAuthCredentialHeader size=(" + mAuthCredentialHeader.getWidth() + ","
- + mAuthCredentialHeader.getHeight());
- pw.println(" mAuthCredentialInput size=(" + mAuthCredentialInput.getWidth() + ","
- + mAuthCredentialInput.getHeight());
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
deleted file mode 100644
index f9e44a0c1724..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.biometrics;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.util.AttributeSet;
-
-import com.android.internal.widget.LockPatternChecker;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.LockPatternView;
-import com.android.internal.widget.LockscreenCredential;
-import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.systemui.R;
-
-import java.util.List;
-
-/**
- * Pattern UI
- */
-public class AuthCredentialPatternView extends AuthCredentialView {
-
- private LockPatternView mLockPatternView;
-
- private class UnlockPatternListener implements LockPatternView.OnPatternListener {
-
- @Override
- public void onPatternStart() {
-
- }
-
- @Override
- public void onPatternCleared() {
-
- }
-
- @Override
- public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
-
- }
-
- @Override
- public void onPatternDetected(List<LockPatternView.Cell> pattern) {
- if (mPendingLockCheck != null) {
- mPendingLockCheck.cancel(false);
- }
-
- mLockPatternView.setEnabled(false);
-
- if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
- // Pattern size is less than the minimum, do not count it as a failed attempt.
- onPatternVerified(VerifyCredentialResponse.ERROR, 0 /* timeoutMs */);
- return;
- }
-
- try (LockscreenCredential credential = LockscreenCredential.createPattern(pattern)) {
- // Request LockSettingsService to return the Gatekeeper Password in the
- // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
- // Gatekeeper Password and operationId.
- mPendingLockCheck = LockPatternChecker.verifyCredential(
- mLockPatternUtils,
- credential,
- mEffectiveUserId,
- LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE,
- this::onPatternVerified);
- }
- }
-
- private void onPatternVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) {
- AuthCredentialPatternView.this.onCredentialVerified(response, timeoutMs);
- if (timeoutMs > 0) {
- mLockPatternView.setEnabled(false);
- } else {
- mLockPatternView.setEnabled(true);
- }
- }
- }
-
- @Override
- protected void onErrorTimeoutFinish() {
- super.onErrorTimeoutFinish();
- // select to enable marquee unless a screen reader is enabled
- mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled()
- || !mAccessibilityManager.isTouchExplorationEnabled());
- }
-
- public AuthCredentialPatternView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mLockPatternView = findViewById(R.id.lockPattern);
- mLockPatternView.setOnPatternListener(new UnlockPatternListener());
- mLockPatternView.setInStealthMode(
- !mLockPatternUtils.isVisiblePatternEnabled(mUserId));
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
deleted file mode 100644
index fa623d146756..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ /dev/null
@@ -1,565 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.biometrics;
-
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT;
-import static android.app.admin.DevicePolicyResources.UNDEFINED;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.AlertDialog;
-import android.app.admin.DevicePolicyManager;
-import android.content.Context;
-import android.content.pm.UserInfo;
-import android.graphics.drawable.Drawable;
-import android.hardware.biometrics.BiometricPrompt;
-import android.hardware.biometrics.PromptInfo;
-import android.os.AsyncTask;
-import android.os.CountDownTimer;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-import android.os.UserManager;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.StringRes;
-
-import com.android.internal.widget.LockPatternUtils;
-import com.android.internal.widget.VerifyCredentialResponse;
-import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Abstract base class for Pin, Pattern, or Password authentication, for
- * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}}
- */
-public abstract class AuthCredentialView extends LinearLayout {
- private static final String TAG = "BiometricPrompt/AuthCredentialView";
- private static final int ERROR_DURATION_MS = 3000;
-
- static final int USER_TYPE_PRIMARY = 1;
- static final int USER_TYPE_MANAGED_PROFILE = 2;
- static final int USER_TYPE_SECONDARY = 3;
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY})
- private @interface UserType {}
-
- protected final Handler mHandler;
- protected final LockPatternUtils mLockPatternUtils;
-
- protected final AccessibilityManager mAccessibilityManager;
- private final UserManager mUserManager;
- private final DevicePolicyManager mDevicePolicyManager;
-
- private PromptInfo mPromptInfo;
- private AuthPanelController mPanelController;
- private boolean mShouldAnimatePanel;
- private boolean mShouldAnimateContents;
-
- protected TextView mTitleView;
- protected TextView mSubtitleView;
- protected TextView mDescriptionView;
- protected ImageView mIconView;
- protected TextView mErrorView;
-
- protected @Utils.CredentialType int mCredentialType;
- protected AuthContainerView mContainerView;
- protected Callback mCallback;
- protected AsyncTask<?, ?, ?> mPendingLockCheck;
- protected int mUserId;
- protected long mOperationId;
- protected int mEffectiveUserId;
- protected ErrorTimer mErrorTimer;
-
- protected @Background DelayableExecutor mBackgroundExecutor;
-
- interface Callback {
- void onCredentialMatched(byte[] attestation);
- }
-
- protected static class ErrorTimer extends CountDownTimer {
- private final TextView mErrorView;
- private final Context mContext;
-
- /**
- * @param millisInFuture The number of millis in the future from the call
- * to {@link #start()} until the countdown is done and {@link
- * #onFinish()}
- * is called.
- * @param countDownInterval The interval along the way to receive
- * {@link #onTick(long)} callbacks.
- */
- public ErrorTimer(Context context, long millisInFuture, long countDownInterval,
- TextView errorView) {
- super(millisInFuture, countDownInterval);
- mErrorView = errorView;
- mContext = context;
- }
-
- @Override
- public void onTick(long millisUntilFinished) {
- final int secondsCountdown = (int) (millisUntilFinished / 1000);
- mErrorView.setText(mContext.getString(
- R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown));
- }
-
- @Override
- public void onFinish() {
- if (mErrorView != null) {
- mErrorView.setText("");
- }
- }
- }
-
- protected final Runnable mClearErrorRunnable = new Runnable() {
- @Override
- public void run() {
- if (mErrorView != null) {
- mErrorView.setText("");
- }
- }
- };
-
- public AuthCredentialView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mLockPatternUtils = new LockPatternUtils(mContext);
- mHandler = new Handler(Looper.getMainLooper());
- mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
- mUserManager = mContext.getSystemService(UserManager.class);
- mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
- }
-
- protected void showError(String error) {
- if (mHandler != null) {
- mHandler.removeCallbacks(mClearErrorRunnable);
- mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS);
- }
- if (mErrorView != null) {
- mErrorView.setText(error);
- }
- }
-
- private void setTextOrHide(TextView view, CharSequence text) {
- if (TextUtils.isEmpty(text)) {
- view.setVisibility(View.GONE);
- } else {
- view.setText(text);
- }
-
- Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this);
- }
-
- private void setText(TextView view, CharSequence text) {
- view.setText(text);
- }
-
- void setUserId(int userId) {
- mUserId = userId;
- }
-
- void setOperationId(long operationId) {
- mOperationId = operationId;
- }
-
- void setEffectiveUserId(int effectiveUserId) {
- mEffectiveUserId = effectiveUserId;
- }
-
- void setCredentialType(@Utils.CredentialType int credentialType) {
- mCredentialType = credentialType;
- }
-
- void setCallback(Callback callback) {
- mCallback = callback;
- }
-
- void setPromptInfo(PromptInfo promptInfo) {
- mPromptInfo = promptInfo;
- }
-
- void setPanelController(AuthPanelController panelController, boolean animatePanel) {
- mPanelController = panelController;
- mShouldAnimatePanel = animatePanel;
- }
-
- void setShouldAnimateContents(boolean animateContents) {
- mShouldAnimateContents = animateContents;
- }
-
- void setContainerView(AuthContainerView containerView) {
- mContainerView = containerView;
- }
-
- void setBackgroundExecutor(@Background DelayableExecutor bgExecutor) {
- mBackgroundExecutor = bgExecutor;
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- final CharSequence title = getTitle(mPromptInfo);
- setText(mTitleView, title);
- setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo));
- setTextOrHide(mDescriptionView, getDescription(mPromptInfo));
- announceForAccessibility(title);
-
- if (mIconView != null) {
- final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId);
- final Drawable image;
- if (isManagedProfile) {
- image = getResources().getDrawable(R.drawable.auth_dialog_enterprise,
- mContext.getTheme());
- } else {
- image = getResources().getDrawable(R.drawable.auth_dialog_lock,
- mContext.getTheme());
- }
- mIconView.setImageDrawable(image);
- }
-
- // Only animate this if we're transitioning from a biometric view.
- if (mShouldAnimateContents) {
- setTranslationY(getResources()
- .getDimension(R.dimen.biometric_dialog_credential_translation_offset));
- setAlpha(0);
-
- postOnAnimation(() -> {
- animate().translationY(0)
- .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS)
- .alpha(1.f)
- .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
- .withLayer()
- .start();
- });
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mErrorTimer != null) {
- mErrorTimer.cancel();
- }
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mTitleView = findViewById(R.id.title);
- mSubtitleView = findViewById(R.id.subtitle);
- mDescriptionView = findViewById(R.id.description);
- mIconView = findViewById(R.id.icon);
- mErrorView = findViewById(R.id.error);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mShouldAnimatePanel) {
- // Credential view is always full screen.
- mPanelController.setUseFullScreen(true);
- mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(),
- mPanelController.getContainerHeight(), 0 /* animateDurationMs */);
- mShouldAnimatePanel = false;
- }
- }
-
- protected void onErrorTimeoutFinish() {}
-
- protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) {
- if (response.isMatched()) {
- mClearErrorRunnable.run();
- mLockPatternUtils.userPresent(mEffectiveUserId);
-
- // The response passed into this method contains the Gatekeeper Password. We still
- // have to request Gatekeeper to create a Hardware Auth Token with the
- // Gatekeeper Password and Challenge (keystore operationId in this case)
- final long pwHandle = response.getGatekeeperPasswordHandle();
- final VerifyCredentialResponse gkResponse = mLockPatternUtils
- .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId);
-
- mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT());
- mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle);
- } else {
- if (timeoutMs > 0) {
- mHandler.removeCallbacks(mClearErrorRunnable);
- long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
- mEffectiveUserId, timeoutMs);
- mErrorTimer = new ErrorTimer(mContext,
- deadline - SystemClock.elapsedRealtime(),
- LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS,
- mErrorView) {
- @Override
- public void onFinish() {
- onErrorTimeoutFinish();
- mClearErrorRunnable.run();
- }
- };
- mErrorTimer.start();
- } else {
- final boolean didUpdateErrorText = reportFailedAttempt();
- if (!didUpdateErrorText) {
- final @StringRes int errorRes;
- switch (mCredentialType) {
- case Utils.CREDENTIAL_PIN:
- errorRes = R.string.biometric_dialog_wrong_pin;
- break;
- case Utils.CREDENTIAL_PATTERN:
- errorRes = R.string.biometric_dialog_wrong_pattern;
- break;
- case Utils.CREDENTIAL_PASSWORD:
- default:
- errorRes = R.string.biometric_dialog_wrong_password;
- break;
- }
- showError(getResources().getString(errorRes));
- }
- }
- }
- }
-
- private boolean reportFailedAttempt() {
- boolean result = updateErrorMessage(
- mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1);
- mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId);
- return result;
- }
-
- private boolean updateErrorMessage(int numAttempts) {
- // Don't show any message if there's no maximum number of attempts.
- final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe(
- mEffectiveUserId);
- if (maxAttempts <= 0 || numAttempts <= 0) {
- return false;
- }
-
- // Update the on-screen error string.
- if (mErrorView != null) {
- final String message = getResources().getString(
- R.string.biometric_dialog_credential_attempts_before_wipe,
- numAttempts,
- maxAttempts);
- showError(message);
- }
-
- // Only show dialog if <=1 attempts are left before wiping.
- final int remainingAttempts = maxAttempts - numAttempts;
- if (remainingAttempts == 1) {
- showLastAttemptBeforeWipeDialog();
- } else if (remainingAttempts <= 0) {
- showNowWipingDialog();
- }
- return true;
- }
-
- private void showLastAttemptBeforeWipeDialog() {
- mBackgroundExecutor.execute(() -> {
- final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
- .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title)
- .setMessage(
- getLastAttemptBeforeWipeMessage(getUserTypeForWipe(), mCredentialType))
- .setPositiveButton(android.R.string.ok, null)
- .create();
- alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
- mHandler.post(alertDialog::show);
- });
- }
-
- private void showNowWipingDialog() {
- mBackgroundExecutor.execute(() -> {
- String nowWipingMessage = getNowWipingMessage(getUserTypeForWipe());
- final AlertDialog alertDialog = new AlertDialog.Builder(mContext)
- .setMessage(nowWipingMessage)
- .setPositiveButton(
- com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss,
- null /* OnClickListener */)
- .setOnDismissListener(
- dialog -> mContainerView.animateAway(
- AuthDialogCallback.DISMISSED_ERROR))
- .create();
- alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
- mHandler.post(alertDialog::show);
- });
- }
-
- private @UserType int getUserTypeForWipe() {
- final UserInfo userToBeWiped = mUserManager.getUserInfo(
- mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId));
- if (userToBeWiped == null || userToBeWiped.isPrimary()) {
- return USER_TYPE_PRIMARY;
- } else if (userToBeWiped.isManagedProfile()) {
- return USER_TYPE_MANAGED_PROFILE;
- } else {
- return USER_TYPE_SECONDARY;
- }
- }
-
- // This should not be called on the main thread to avoid making an IPC.
- private String getLastAttemptBeforeWipeMessage(
- @UserType int userType, @Utils.CredentialType int credentialType) {
- switch (userType) {
- case USER_TYPE_PRIMARY:
- return getLastAttemptBeforeWipeDeviceMessage(credentialType);
- case USER_TYPE_MANAGED_PROFILE:
- return getLastAttemptBeforeWipeProfileMessage(credentialType);
- case USER_TYPE_SECONDARY:
- return getLastAttemptBeforeWipeUserMessage(credentialType);
- default:
- throw new IllegalArgumentException("Unrecognized user type:" + userType);
- }
- }
-
- private String getLastAttemptBeforeWipeDeviceMessage(
- @Utils.CredentialType int credentialType) {
- switch (credentialType) {
- case Utils.CREDENTIAL_PIN:
- return mContext.getString(
- R.string.biometric_dialog_last_pin_attempt_before_wipe_device);
- case Utils.CREDENTIAL_PATTERN:
- return mContext.getString(
- R.string.biometric_dialog_last_pattern_attempt_before_wipe_device);
- case Utils.CREDENTIAL_PASSWORD:
- default:
- return mContext.getString(
- R.string.biometric_dialog_last_password_attempt_before_wipe_device);
- }
- }
-
- // This should not be called on the main thread to avoid making an IPC.
- private String getLastAttemptBeforeWipeProfileMessage(
- @Utils.CredentialType int credentialType) {
- return mDevicePolicyManager.getResources().getString(
- getLastAttemptBeforeWipeProfileUpdatableStringId(credentialType),
- () -> getLastAttemptBeforeWipeProfileDefaultMessage(credentialType));
- }
-
- private static String getLastAttemptBeforeWipeProfileUpdatableStringId(
- @Utils.CredentialType int credentialType) {
- switch (credentialType) {
- case Utils.CREDENTIAL_PIN:
- return BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT;
- case Utils.CREDENTIAL_PATTERN:
- return BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT;
- case Utils.CREDENTIAL_PASSWORD:
- default:
- return BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT;
- }
- }
-
- private String getLastAttemptBeforeWipeProfileDefaultMessage(
- @Utils.CredentialType int credentialType) {
- int resId;
- switch (credentialType) {
- case Utils.CREDENTIAL_PIN:
- resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_profile;
- break;
- case Utils.CREDENTIAL_PATTERN:
- resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile;
- break;
- case Utils.CREDENTIAL_PASSWORD:
- default:
- resId = R.string.biometric_dialog_last_password_attempt_before_wipe_profile;
- }
- return mContext.getString(resId);
- }
-
- private String getLastAttemptBeforeWipeUserMessage(
- @Utils.CredentialType int credentialType) {
- int resId;
- switch (credentialType) {
- case Utils.CREDENTIAL_PIN:
- resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_user;
- break;
- case Utils.CREDENTIAL_PATTERN:
- resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_user;
- break;
- case Utils.CREDENTIAL_PASSWORD:
- default:
- resId = R.string.biometric_dialog_last_password_attempt_before_wipe_user;
- }
- return mContext.getString(resId);
- }
-
- private String getNowWipingMessage(@UserType int userType) {
- return mDevicePolicyManager.getResources().getString(
- getNowWipingUpdatableStringId(userType),
- () -> getNowWipingDefaultMessage(userType));
- }
-
- private String getNowWipingUpdatableStringId(@UserType int userType) {
- switch (userType) {
- case USER_TYPE_MANAGED_PROFILE:
- return BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS;
- default:
- return UNDEFINED;
- }
- }
-
- private String getNowWipingDefaultMessage(@UserType int userType) {
- int resId;
- switch (userType) {
- case USER_TYPE_PRIMARY:
- resId = com.android.settingslib.R.string.failed_attempts_now_wiping_device;
- break;
- case USER_TYPE_MANAGED_PROFILE:
- resId = com.android.settingslib.R.string.failed_attempts_now_wiping_profile;
- break;
- case USER_TYPE_SECONDARY:
- resId = com.android.settingslib.R.string.failed_attempts_now_wiping_user;
- break;
- default:
- throw new IllegalArgumentException("Unrecognized user type:" + userType);
- }
- return mContext.getString(resId);
- }
-
- @Nullable
- private static CharSequence getTitle(@NonNull PromptInfo promptInfo) {
- final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle();
- return credentialTitle != null ? credentialTitle : promptInfo.getTitle();
- }
-
- @Nullable
- private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) {
- final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle();
- return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle();
- }
-
- @Nullable
- private static CharSequence getDescription(@NonNull PromptInfo promptInfo) {
- final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription();
- return credentialDescription != null ? credentialDescription : promptInfo.getDescription();
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index f1e42e0c5454..5c616f005d4d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -177,11 +177,11 @@ public class AuthPanelController extends ViewOutlineProvider {
}
}
- int getContainerWidth() {
+ public int getContainerWidth() {
return mContainerWidth;
}
- int getContainerHeight() {
+ public int getContainerHeight() {
return mContainerHeight;
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index b5d81f253916..7c0c3b710e66 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -16,32 +16,45 @@
package com.android.systemui.biometrics.dagger
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
+import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.concurrency.ThreadFactory
+import dagger.Binds
import dagger.Module
import dagger.Provides
import java.util.concurrent.Executor
import javax.inject.Qualifier
-/**
- * Dagger module for all things biometric.
- */
+/** Dagger module for all things biometric. */
@Module
-object BiometricsModule {
+interface BiometricsModule {
- /** Background [Executor] for HAL related operations. */
- @Provides
+ @Binds
@SysUISingleton
- @JvmStatic
- @BiometricsBackground
- fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
- threadFactory.buildExecutorOnNewThread("biometrics")
+ fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository
+
+ @Binds
+ @SysUISingleton
+ fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
+
+ companion object {
+ /** Background [Executor] for HAL related operations. */
+ @Provides
+ @SysUISingleton
+ @JvmStatic
+ @BiometricsBackground
+ fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
+ threadFactory.buildExecutorOnNewThread("biometrics")
+ }
}
/**
- * Background executor for HAL operations that are latency sensitive but too
- * slow to run on the main thread. Prefer the shared executors, such as
- * [com.android.systemui.dagger.qualifiers.Background] when a HAL is not directly involved.
+ * Background executor for HAL operations that are latency sensitive but too slow to run on the main
+ * thread. Prefer the shared executors, such as [com.android.systemui.dagger.qualifiers.Background]
+ * when a HAL is not directly involved.
*/
@Qualifier
@MustBeDocumented
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
new file mode 100644
index 000000000000..e82646f0d861
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.biometrics.data.model
+
+import com.android.systemui.biometrics.Utils
+
+// TODO(b/251476085): this should eventually replace Utils.CredentialType
+/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
+enum class PromptKind {
+ ANY_BIOMETRIC,
+ PIN,
+ PATTERN,
+ PASSWORD,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
new file mode 100644
index 000000000000..92a13cfe538b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -0,0 +1,102 @@
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.PromptInfo
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * A repository for the global state of BiometricPrompt.
+ *
+ * There is never more than one instance of the prompt at any given time.
+ */
+interface PromptRepository {
+
+ /** If the prompt is showing. */
+ val isShowing: Flow<Boolean>
+
+ /** The app-specific details to show in the prompt. */
+ val promptInfo: StateFlow<PromptInfo?>
+
+ /** The user that the prompt is for. */
+ val userId: StateFlow<Int?>
+
+ /** The gatekeeper challenge, if one is associated with this prompt. */
+ val challenge: StateFlow<Long?>
+
+ /** The kind of credential to use (biometric, pin, pattern, etc.). */
+ val kind: StateFlow<PromptKind>
+
+ /** Update the prompt configuration, which should be set before [isShowing]. */
+ fun setPrompt(
+ promptInfo: PromptInfo,
+ userId: Int,
+ gatekeeperChallenge: Long?,
+ kind: PromptKind = PromptKind.ANY_BIOMETRIC,
+ )
+
+ /** Unset the prompt info. */
+ fun unsetPrompt()
+}
+
+@SysUISingleton
+class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) :
+ PromptRepository {
+
+ override val isShowing: Flow<Boolean> = conflatedCallbackFlow {
+ val callback =
+ object : AuthController.Callback {
+ override fun onBiometricPromptShown() =
+ trySendWithFailureLogging(true, TAG, "set isShowing")
+
+ override fun onBiometricPromptDismissed() =
+ trySendWithFailureLogging(false, TAG, "unset isShowing")
+ }
+ authController.addCallback(callback)
+ trySendWithFailureLogging(authController.isShowing, TAG, "update isShowing")
+ awaitClose { authController.removeCallback(callback) }
+ }
+
+ private val _promptInfo: MutableStateFlow<PromptInfo?> = MutableStateFlow(null)
+ override val promptInfo = _promptInfo.asStateFlow()
+
+ private val _challenge: MutableStateFlow<Long?> = MutableStateFlow(null)
+ override val challenge: StateFlow<Long?> = _challenge.asStateFlow()
+
+ private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
+ override val userId = _userId.asStateFlow()
+
+ private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+ override val kind = _kind.asStateFlow()
+
+ override fun setPrompt(
+ promptInfo: PromptInfo,
+ userId: Int,
+ gatekeeperChallenge: Long?,
+ kind: PromptKind,
+ ) {
+ _kind.value = kind
+ _userId.value = userId
+ _challenge.value = gatekeeperChallenge
+ _promptInfo.value = promptInfo
+ }
+
+ override fun unsetPrompt() {
+ _promptInfo.value = null
+ _userId.value = null
+ _challenge.value = null
+ _kind.value = PromptKind.ANY_BIOMETRIC
+ }
+
+ companion object {
+ private const val TAG = "BiometricPromptRepository"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
new file mode 100644
index 000000000000..1f1a1b5c83bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -0,0 +1,282 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources
+import android.content.Context
+import android.os.UserManager
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockscreenCredential
+import com.android.internal.widget.VerifyCredentialResponse
+import com.android.systemui.R
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+/**
+ * A wrapper for [LockPatternUtils] to verify PIN, pattern, or password credentials.
+ *
+ * This class also uses the [DevicePolicyManager] to generate appropriate error messages when policy
+ * exceptions are raised (i.e. wipe device due to excessive failed attempts, etc.).
+ */
+interface CredentialInteractor {
+ /** If the user's pattern credential should be hidden */
+ fun isStealthModeActive(userId: Int): Boolean
+
+ /** Get the effective user id (profile owner, if one exists) */
+ fun getCredentialOwnerOrSelfId(userId: Int): Int
+
+ /**
+ * Verifies a credential and returns a stream of results.
+ *
+ * The final emitted value will either be a [CredentialStatus.Fail.Error] or a
+ * [CredentialStatus.Success.Verified].
+ */
+ fun verifyCredential(
+ request: BiometricPromptRequest.Credential,
+ credential: LockscreenCredential,
+ ): Flow<CredentialStatus>
+}
+
+/** Standard implementation of [CredentialInteractor]. */
+class CredentialInteractorImpl
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ private val lockPatternUtils: LockPatternUtils,
+ private val userManager: UserManager,
+ private val devicePolicyManager: DevicePolicyManager,
+ private val systemClock: SystemClock,
+) : CredentialInteractor {
+
+ override fun isStealthModeActive(userId: Int): Boolean =
+ !lockPatternUtils.isVisiblePatternEnabled(userId)
+
+ override fun getCredentialOwnerOrSelfId(userId: Int): Int =
+ userManager.getCredentialOwnerProfile(userId)
+
+ override fun verifyCredential(
+ request: BiometricPromptRequest.Credential,
+ credential: LockscreenCredential,
+ ): Flow<CredentialStatus> = flow {
+ // Request LockSettingsService to return the Gatekeeper Password in the
+ // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
+ // Gatekeeper Password and operationId.
+ val effectiveUserId = request.userInfo.deviceCredentialOwnerId
+ val response =
+ lockPatternUtils.verifyCredential(
+ credential,
+ effectiveUserId,
+ LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE
+ )
+
+ if (response.isMatched) {
+ lockPatternUtils.userPresent(effectiveUserId)
+
+ // The response passed into this method contains the Gatekeeper
+ // Password. We still have to request Gatekeeper to create a
+ // Hardware Auth Token with the Gatekeeper Password and Challenge
+ // (keystore operationId in this case)
+ val pwHandle = response.gatekeeperPasswordHandle
+ val gkResponse: VerifyCredentialResponse =
+ lockPatternUtils.verifyGatekeeperPasswordHandle(
+ pwHandle,
+ request.operationInfo.gatekeeperChallenge,
+ effectiveUserId
+ )
+ val hat = gkResponse.gatekeeperHAT
+ lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle)
+ emit(CredentialStatus.Success.Verified(hat))
+ } else if (response.timeout > 0) {
+ // if requests are being throttled, update the error message every
+ // second until the temporary lock has expired
+ val deadline: Long =
+ lockPatternUtils.setLockoutAttemptDeadline(effectiveUserId, response.timeout)
+ val interval = LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS
+ var remaining = deadline - systemClock.elapsedRealtime()
+ while (remaining > 0) {
+ emit(
+ CredentialStatus.Fail.Throttled(
+ applicationContext.getString(
+ R.string.biometric_dialog_credential_too_many_attempts,
+ remaining / 1000
+ )
+ )
+ )
+ delay(interval)
+ remaining -= interval
+ }
+ emit(CredentialStatus.Fail.Error(""))
+ } else { // bad request, but not throttled
+ val numAttempts = lockPatternUtils.getCurrentFailedPasswordAttempts(effectiveUserId) + 1
+ val maxAttempts = lockPatternUtils.getMaximumFailedPasswordsForWipe(effectiveUserId)
+ if (maxAttempts <= 0 || numAttempts <= 0) {
+ // use a generic message if there's no maximum number of attempts
+ emit(CredentialStatus.Fail.Error())
+ } else {
+ val remainingAttempts = (maxAttempts - numAttempts).coerceAtLeast(0)
+ emit(
+ CredentialStatus.Fail.Error(
+ applicationContext.getString(
+ R.string.biometric_dialog_credential_attempts_before_wipe,
+ numAttempts,
+ maxAttempts
+ ),
+ remainingAttempts,
+ fetchFinalAttemptMessageOrNull(request, remainingAttempts)
+ )
+ )
+ }
+ lockPatternUtils.reportFailedPasswordAttempt(effectiveUserId)
+ }
+ }
+
+ private fun fetchFinalAttemptMessageOrNull(
+ request: BiometricPromptRequest.Credential,
+ remainingAttempts: Int?,
+ ): String? =
+ if (remainingAttempts != null && remainingAttempts <= 1) {
+ applicationContext.getFinalAttemptMessageOrBlank(
+ request,
+ devicePolicyManager,
+ userManager.getUserTypeForWipe(
+ devicePolicyManager,
+ request.userInfo.deviceCredentialOwnerId
+ ),
+ remainingAttempts
+ )
+ } else {
+ null
+ }
+}
+
+private enum class UserType {
+ PRIMARY,
+ MANAGED_PROFILE,
+ SECONDARY,
+}
+
+private fun UserManager.getUserTypeForWipe(
+ devicePolicyManager: DevicePolicyManager,
+ effectiveUserId: Int,
+): UserType {
+ val userToBeWiped =
+ getUserInfo(
+ devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(effectiveUserId)
+ )
+ return when {
+ userToBeWiped == null || userToBeWiped.isPrimary -> UserType.PRIMARY
+ userToBeWiped.isManagedProfile -> UserType.MANAGED_PROFILE
+ else -> UserType.SECONDARY
+ }
+}
+
+private fun Context.getFinalAttemptMessageOrBlank(
+ request: BiometricPromptRequest.Credential,
+ devicePolicyManager: DevicePolicyManager,
+ userType: UserType,
+ remaining: Int,
+): String =
+ when {
+ remaining == 1 -> getLastAttemptBeforeWipeMessage(request, devicePolicyManager, userType)
+ remaining <= 0 -> getNowWipingMessage(devicePolicyManager, userType)
+ else -> ""
+ }
+
+private fun Context.getLastAttemptBeforeWipeMessage(
+ request: BiometricPromptRequest.Credential,
+ devicePolicyManager: DevicePolicyManager,
+ userType: UserType,
+): String =
+ when (userType) {
+ UserType.PRIMARY -> getLastAttemptBeforeWipeDeviceMessage(request)
+ UserType.MANAGED_PROFILE ->
+ getLastAttemptBeforeWipeProfileMessage(request, devicePolicyManager)
+ UserType.SECONDARY -> getLastAttemptBeforeWipeUserMessage(request)
+ }
+
+private fun Context.getLastAttemptBeforeWipeDeviceMessage(
+ request: BiometricPromptRequest.Credential,
+): String {
+ val id =
+ when (request) {
+ is BiometricPromptRequest.Credential.Pin ->
+ R.string.biometric_dialog_last_pin_attempt_before_wipe_device
+ is BiometricPromptRequest.Credential.Pattern ->
+ R.string.biometric_dialog_last_pattern_attempt_before_wipe_device
+ is BiometricPromptRequest.Credential.Password ->
+ R.string.biometric_dialog_last_password_attempt_before_wipe_device
+ }
+ return getString(id)
+}
+
+private fun Context.getLastAttemptBeforeWipeProfileMessage(
+ request: BiometricPromptRequest.Credential,
+ devicePolicyManager: DevicePolicyManager,
+): String {
+ val id =
+ when (request) {
+ is BiometricPromptRequest.Credential.Pin ->
+ DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT
+ is BiometricPromptRequest.Credential.Pattern ->
+ DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT
+ is BiometricPromptRequest.Credential.Password ->
+ DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT
+ }
+ return devicePolicyManager.resources.getString(id) {
+ // use fallback a string if not found
+ val defaultId =
+ when (request) {
+ is BiometricPromptRequest.Credential.Pin ->
+ R.string.biometric_dialog_last_pin_attempt_before_wipe_profile
+ is BiometricPromptRequest.Credential.Pattern ->
+ R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile
+ is BiometricPromptRequest.Credential.Password ->
+ R.string.biometric_dialog_last_password_attempt_before_wipe_profile
+ }
+ getString(defaultId)
+ }
+}
+
+private fun Context.getLastAttemptBeforeWipeUserMessage(
+ request: BiometricPromptRequest.Credential,
+): String {
+ val resId =
+ when (request) {
+ is BiometricPromptRequest.Credential.Pin ->
+ R.string.biometric_dialog_last_pin_attempt_before_wipe_user
+ is BiometricPromptRequest.Credential.Pattern ->
+ R.string.biometric_dialog_last_pattern_attempt_before_wipe_user
+ is BiometricPromptRequest.Credential.Password ->
+ R.string.biometric_dialog_last_password_attempt_before_wipe_user
+ }
+ return getString(resId)
+}
+
+private fun Context.getNowWipingMessage(
+ devicePolicyManager: DevicePolicyManager,
+ userType: UserType,
+): String {
+ val id =
+ when (userType) {
+ UserType.MANAGED_PROFILE ->
+ DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS
+ else -> DevicePolicyResources.UNDEFINED
+ }
+ return devicePolicyManager.resources.getString(id) {
+ // use fallback a string if not found
+ val defaultId =
+ when (userType) {
+ UserType.PRIMARY ->
+ com.android.settingslib.R.string.failed_attempts_now_wiping_device
+ UserType.MANAGED_PROFILE ->
+ com.android.settingslib.R.string.failed_attempts_now_wiping_profile
+ UserType.SECONDARY ->
+ com.android.settingslib.R.string.failed_attempts_now_wiping_user
+ }
+ getString(defaultId)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt
new file mode 100644
index 000000000000..40b76121f237
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.biometrics.domain.interactor
+
+/** Result of a [CredentialInteractor.verifyCredential] check. */
+sealed interface CredentialStatus {
+ /** A successful result. */
+ sealed interface Success : CredentialStatus {
+ /** The credential is valid and a [hat] has been generated. */
+ data class Verified(val hat: ByteArray) : Success
+ }
+ /** A failed result. */
+ sealed interface Fail : CredentialStatus {
+ val error: String?
+
+ /** The credential check failed with an [error]. */
+ data class Error(
+ override val error: String? = null,
+ val remainingAttempts: Int? = null,
+ val urgentMessage: String? = null,
+ ) : Fail
+ /** The credential check failed with an [error] and is temporarily locked out. */
+ data class Throttled(override val error: String) : Fail
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
new file mode 100644
index 000000000000..6362c2f627d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
@@ -0,0 +1,189 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import com.android.internal.widget.LockPatternView
+import com.android.internal.widget.LockscreenCredential
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.lastOrNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
+
+/**
+ * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users
+ * PIN, pattern, or password credential instead of a biometric.
+ */
+class BiometricPromptCredentialInteractor
+@Inject
+constructor(
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val biometricPromptRepository: PromptRepository,
+ private val credentialInteractor: CredentialInteractor,
+) {
+ /** If the prompt is currently showing. */
+ val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing
+
+ /** Metadata about the current credential prompt, including app-supplied preferences. */
+ val prompt: Flow<BiometricPromptRequest?> =
+ combine(
+ biometricPromptRepository.promptInfo,
+ biometricPromptRepository.challenge,
+ biometricPromptRepository.userId,
+ biometricPromptRepository.kind
+ ) { promptInfo, challenge, userId, kind ->
+ if (promptInfo == null || userId == null || challenge == null) {
+ return@combine null
+ }
+
+ when (kind) {
+ PromptKind.PIN ->
+ BiometricPromptRequest.Credential.Pin(
+ info = promptInfo,
+ userInfo = userInfo(userId),
+ operationInfo = operationInfo(challenge)
+ )
+ PromptKind.PATTERN ->
+ BiometricPromptRequest.Credential.Pattern(
+ info = promptInfo,
+ userInfo = userInfo(userId),
+ operationInfo = operationInfo(challenge),
+ stealthMode = credentialInteractor.isStealthModeActive(userId)
+ )
+ PromptKind.PASSWORD ->
+ BiometricPromptRequest.Credential.Password(
+ info = promptInfo,
+ userInfo = userInfo(userId),
+ operationInfo = operationInfo(challenge)
+ )
+ else -> null
+ }
+ }
+ .distinctUntilChanged()
+
+ private fun userInfo(userId: Int): BiometricUserInfo =
+ BiometricUserInfo(
+ userId = userId,
+ deviceCredentialOwnerId = credentialInteractor.getCredentialOwnerOrSelfId(userId)
+ )
+
+ private fun operationInfo(challenge: Long): BiometricOperationInfo =
+ BiometricOperationInfo(gatekeeperChallenge = challenge)
+
+ /** Most recent error due to [verifyCredential]. */
+ private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null)
+ val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow()
+
+ /** Update the current request to use credential-based authentication instead of biometrics. */
+ fun useCredentialsForAuthentication(
+ promptInfo: PromptInfo,
+ @Utils.CredentialType kind: Int,
+ userId: Int,
+ challenge: Long,
+ ) {
+ biometricPromptRepository.setPrompt(
+ promptInfo,
+ userId,
+ challenge,
+ kind.asBiometricPromptCredential()
+ )
+ }
+
+ /** Unset the current authentication request. */
+ fun resetPrompt() {
+ biometricPromptRepository.unsetPrompt()
+ }
+
+ /**
+ * Check a credential and return the attestation token (HAT) if successful.
+ *
+ * This method will not return if credential checks are being throttled until the throttling has
+ * expired and the user can try again. It will periodically update the [verificationError] until
+ * cancelled or the throttling has completed. If the request is not throttled, but unsuccessful,
+ * the [verificationError] will be set and an optional
+ * [CredentialStatus.Fail.Error.urgentMessage] message may be provided to indicate additional
+ * hints to the user (i.e. device will be wiped on next failure, etc.).
+ *
+ * The check happens on the background dispatcher given in the constructor.
+ */
+ suspend fun checkCredential(
+ request: BiometricPromptRequest.Credential,
+ text: CharSequence? = null,
+ pattern: List<LockPatternView.Cell>? = null,
+ ): CredentialStatus =
+ withContext(bgDispatcher) {
+ val credential =
+ when (request) {
+ is BiometricPromptRequest.Credential.Pin ->
+ LockscreenCredential.createPinOrNone(text ?: "")
+ is BiometricPromptRequest.Credential.Password ->
+ LockscreenCredential.createPasswordOrNone(text ?: "")
+ is BiometricPromptRequest.Credential.Pattern ->
+ LockscreenCredential.createPattern(pattern ?: listOf())
+ }
+
+ credential.use { c -> verifyCredential(request, c) }
+ }
+
+ private suspend fun verifyCredential(
+ request: BiometricPromptRequest.Credential,
+ credential: LockscreenCredential?
+ ): CredentialStatus {
+ if (credential == null || credential.isNone) {
+ return CredentialStatus.Fail.Error()
+ }
+
+ val finalStatus =
+ credentialInteractor
+ .verifyCredential(request, credential)
+ .onEach { status ->
+ when (status) {
+ is CredentialStatus.Success -> _verificationError.value = null
+ is CredentialStatus.Fail -> _verificationError.value = status
+ }
+ }
+ .lastOrNull()
+
+ return finalStatus ?: CredentialStatus.Fail.Error()
+ }
+
+ /**
+ * Report a user-visible error.
+ *
+ * Use this instead of calling [verifyCredential] when it is not necessary because the check
+ * will obviously fail (i.e. too short, empty, etc.)
+ */
+ fun setVerificationError(error: CredentialStatus.Fail.Error?) {
+ if (error != null) {
+ _verificationError.value = error
+ } else {
+ resetVerificationError()
+ }
+ }
+
+ /** Clear the current error message, if any. */
+ fun resetVerificationError() {
+ _verificationError.value = null
+ }
+}
+
+// TODO(b/251476085): remove along with Utils.CredentialType
+/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
+private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
+ when (this) {
+ Utils.CREDENTIAL_PIN -> PromptKind.PIN
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD
+ Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN
+ else -> PromptKind.ANY_BIOMETRIC
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt
new file mode 100644
index 000000000000..c619b12361c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt
@@ -0,0 +1,4 @@
+package com.android.systemui.biometrics.domain.model
+
+/** Metadata about an in-progress biometric operation. */
+data class BiometricOperationInfo(val gatekeeperChallenge: Long = -1)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
new file mode 100644
index 000000000000..5ee0381db630
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -0,0 +1,69 @@
+package com.android.systemui.biometrics.domain.model
+
+import android.hardware.biometrics.PromptInfo
+
+/**
+ * Preferences for BiometricPrompt, such as title & description, that are immutable while the prompt
+ * is showing.
+ *
+ * This roughly corresponds to a "request" by the system or an app to show BiometricPrompt and it
+ * contains a subset of the information in a [PromptInfo] that is relevant to SysUI.
+ */
+sealed class BiometricPromptRequest(
+ val title: String,
+ val subtitle: String,
+ val description: String,
+ val userInfo: BiometricUserInfo,
+ val operationInfo: BiometricOperationInfo,
+) {
+ /** Prompt using one or more biometrics. */
+ class Biometric(
+ info: PromptInfo,
+ userInfo: BiometricUserInfo,
+ operationInfo: BiometricOperationInfo,
+ ) :
+ BiometricPromptRequest(
+ title = info.title?.toString() ?: "",
+ subtitle = info.subtitle?.toString() ?: "",
+ description = info.description?.toString() ?: "",
+ userInfo = userInfo,
+ operationInfo = operationInfo
+ )
+
+ /** Prompt using a credential (pin, pattern, password). */
+ sealed class Credential(
+ info: PromptInfo,
+ userInfo: BiometricUserInfo,
+ operationInfo: BiometricOperationInfo,
+ ) :
+ BiometricPromptRequest(
+ title = (info.deviceCredentialTitle ?: info.title)?.toString() ?: "",
+ subtitle = (info.deviceCredentialSubtitle ?: info.subtitle)?.toString() ?: "",
+ description = (info.deviceCredentialDescription ?: info.description)?.toString() ?: "",
+ userInfo = userInfo,
+ operationInfo = operationInfo,
+ ) {
+
+ /** PIN prompt. */
+ class Pin(
+ info: PromptInfo,
+ userInfo: BiometricUserInfo,
+ operationInfo: BiometricOperationInfo,
+ ) : Credential(info, userInfo, operationInfo)
+
+ /** Password prompt. */
+ class Password(
+ info: PromptInfo,
+ userInfo: BiometricUserInfo,
+ operationInfo: BiometricOperationInfo,
+ ) : Credential(info, userInfo, operationInfo)
+
+ /** Pattern prompt. */
+ class Pattern(
+ info: PromptInfo,
+ userInfo: BiometricUserInfo,
+ operationInfo: BiometricOperationInfo,
+ val stealthMode: Boolean,
+ ) : Credential(info, userInfo, operationInfo)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt
new file mode 100644
index 000000000000..08da04d27606
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt
@@ -0,0 +1,7 @@
+package com.android.systemui.biometrics.domain.model
+
+/** Metadata about the current user BiometricPrompt is being shown to. */
+data class BiometricUserInfo(
+ val userId: Int,
+ val deviceCredentialOwnerId: Int = userId,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
new file mode 100644
index 000000000000..bcc0575651e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -0,0 +1,130 @@
+package com.android.systemui.biometrics.ui
+
+import android.content.Context
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowInsets.Type.ime
+import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
+import android.widget.ImeAwareEditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.isGone
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+
+/** PIN or password credential view for BiometricPrompt. */
+class CredentialPasswordView(context: Context, attrs: AttributeSet?) :
+ LinearLayout(context, attrs), CredentialView, View.OnApplyWindowInsetsListener {
+
+ private lateinit var titleView: TextView
+ private lateinit var subtitleView: TextView
+ private lateinit var descriptionView: TextView
+ private lateinit var iconView: ImageView
+ private lateinit var passwordField: ImeAwareEditText
+ private lateinit var credentialHeader: View
+ private lateinit var credentialInput: View
+
+ private var bottomInset: Int = 0
+
+ private val accessibilityManager by lazy {
+ context.getSystemService(AccessibilityManager::class.java)
+ }
+
+ /** Initializes the view. */
+ override fun init(
+ viewModel: CredentialViewModel,
+ host: CredentialView.Host,
+ panelViewController: AuthPanelController,
+ animatePanel: Boolean,
+ ) {
+ CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel)
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ titleView = requireViewById(R.id.title)
+ subtitleView = requireViewById(R.id.subtitle)
+ descriptionView = requireViewById(R.id.description)
+ iconView = requireViewById(R.id.icon)
+ subtitleView = requireViewById(R.id.subtitle)
+ passwordField = requireViewById(R.id.lockPassword)
+ credentialHeader = requireViewById(R.id.auth_credential_header)
+ credentialInput = requireViewById(R.id.auth_credential_input)
+
+ setOnApplyWindowInsetsListener(this)
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+
+ val inputLeftBound: Int
+ val inputTopBound: Int
+ var headerRightBound = right
+ var headerTopBounds = top
+ val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom
+ val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom
+ if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
+ inputTopBound = (bottom - credentialInput.height) / 2
+ inputLeftBound = (right - left) / 2
+ headerRightBound = inputLeftBound
+ headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset)
+ } else {
+ inputTopBound = descBottom + (bottom - descBottom - credentialInput.height) / 2
+ inputLeftBound = (right - left - credentialInput.width) / 2
+ }
+
+ if (descriptionView.bottom > bottomInset) {
+ credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom)
+ }
+ credentialInput.layout(inputLeftBound, inputTopBound, right, bottom)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+ val newWidth = MeasureSpec.getSize(widthMeasureSpec)
+ val newHeight = MeasureSpec.getSize(heightMeasureSpec) - bottomInset
+
+ setMeasuredDimension(newWidth, newHeight)
+
+ val halfWidthSpec = MeasureSpec.makeMeasureSpec(width / 2, MeasureSpec.AT_MOST)
+ val fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED)
+ if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
+ measureChildren(halfWidthSpec, fullHeightSpec)
+ } else {
+ measureChildren(widthMeasureSpec, fullHeightSpec)
+ }
+ }
+
+ override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
+ val bottomInsets = insets.getInsets(ime())
+ if (bottomInset != bottomInsets.bottom) {
+ bottomInset = bottomInsets.bottom
+
+ if (bottomInset > 0 && resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
+ titleView.isSingleLine = true
+ titleView.ellipsize = TextUtils.TruncateAt.MARQUEE
+ titleView.marqueeRepeatLimit = -1
+ // select to enable marquee unless a screen reader is enabled
+ titleView.isSelected = accessibilityManager.shouldMarquee()
+ } else {
+ titleView.isSingleLine = false
+ titleView.ellipsize = null
+ // select to enable marquee unless a screen reader is enabled
+ titleView.isSelected = false
+ }
+
+ requestLayout()
+ }
+ return insets
+ }
+}
+
+private fun AccessibilityManager.shouldMarquee(): Boolean = !isEnabled || !isTouchExplorationEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
new file mode 100644
index 000000000000..75331f083851
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
@@ -0,0 +1,23 @@
+package com.android.systemui.biometrics.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+
+/** Pattern credential view for BiometricPrompt. */
+class CredentialPatternView(context: Context, attrs: AttributeSet?) :
+ LinearLayout(context, attrs), CredentialView {
+
+ /** Initializes the view. */
+ override fun init(
+ viewModel: CredentialViewModel,
+ host: CredentialView.Host,
+ panelViewController: AuthPanelController,
+ animatePanel: Boolean,
+ ) {
+ CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
new file mode 100644
index 000000000000..b7c6a4566108
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.biometrics.ui
+
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+
+/** A credential variant of BiometricPrompt. */
+sealed interface CredentialView {
+ /**
+ * Callbacks for the "host" container view that contains this credential view.
+ *
+ * TODO(b/251476085): Removed when the host view is converted to use a parent view model.
+ */
+ interface Host {
+ /** When the user's credential has been verified. */
+ fun onCredentialMatched(attestation: ByteArray)
+
+ /** When the user abandons credential verification. */
+ fun onCredentialAborted()
+
+ /** Warn the user is warned about excessive attempts. */
+ fun onCredentialAttemptsRemaining(remaining: Int, messageBody: String)
+ }
+
+ // TODO(251476085): remove AuthPanelController
+ fun init(
+ viewModel: CredentialViewModel,
+ host: Host,
+ panelViewController: AuthPanelController,
+ animatePanel: Boolean,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
new file mode 100644
index 000000000000..c619648a314c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
@@ -0,0 +1,104 @@
+package com.android.systemui.biometrics.ui.binder
+
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import android.widget.ImeAwareEditText
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.biometrics.ui.CredentialPasswordView
+import com.android.systemui.biometrics.ui.CredentialView
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** Sub-binder for the [CredentialPasswordView]. */
+object CredentialPasswordViewBinder {
+
+ /** Bind the view. */
+ fun bind(
+ view: CredentialPasswordView,
+ host: CredentialView.Host,
+ viewModel: CredentialViewModel,
+ ) {
+ val imeManager = view.context.getSystemService(InputMethodManager::class.java)!!
+
+ val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword)
+
+ view.repeatWhenAttached {
+ passwordField.requestFocus()
+ passwordField.scheduleShowSoftInput()
+
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // observe credential validation attempts and submit/cancel buttons
+ launch {
+ viewModel.header.collect { header ->
+ passwordField.setTextOperationUser(header.user)
+ passwordField.setOnEditorActionListener(
+ OnImeSubmitListener { text ->
+ launch { viewModel.checkCredential(text, header) }
+ }
+ )
+ passwordField.setOnKeyListener(
+ OnBackButtonListener { host.onCredentialAborted() }
+ )
+ }
+ }
+
+ launch {
+ viewModel.inputFlags.collect { flags ->
+ flags?.let { passwordField.inputType = it }
+ }
+ }
+
+ // dismiss on a valid credential check
+ launch {
+ viewModel.validatedAttestation.collect { attestation ->
+ if (attestation != null) {
+ imeManager.hideSoftInputFromWindow(view.windowToken, 0 /* flags */)
+ host.onCredentialMatched(attestation)
+ } else {
+ passwordField.setText("")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private class OnBackButtonListener(private val onBack: () -> Unit) : View.OnKeyListener {
+ override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode != KeyEvent.KEYCODE_BACK) {
+ return false
+ }
+ if (event.action == KeyEvent.ACTION_UP) {
+ onBack()
+ }
+ return true
+ }
+}
+
+private class OnImeSubmitListener(private val onSubmit: (text: CharSequence) -> Unit) :
+ TextView.OnEditorActionListener {
+ override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {
+ val isSoftImeEvent =
+ event == null &&
+ (actionId == EditorInfo.IME_NULL ||
+ actionId == EditorInfo.IME_ACTION_DONE ||
+ actionId == EditorInfo.IME_ACTION_NEXT)
+ val isKeyboardEnterKey =
+ event != null &&
+ KeyEvent.isConfirmKey(event.keyCode) &&
+ event.action == KeyEvent.ACTION_DOWN
+ if (isSoftImeEvent || isKeyboardEnterKey) {
+ onSubmit(v.text)
+ return true
+ }
+ return false
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
new file mode 100644
index 000000000000..4765551df3f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.biometrics.ui.binder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternView
+import com.android.systemui.R
+import com.android.systemui.biometrics.ui.CredentialPatternView
+import com.android.systemui.biometrics.ui.CredentialView
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** Sub-binder for the [CredentialPatternView]. */
+object CredentialPatternViewBinder {
+
+ /** Bind the view. */
+ fun bind(
+ view: CredentialPatternView,
+ host: CredentialView.Host,
+ viewModel: CredentialViewModel,
+ ) {
+ val lockPatternView: LockPatternView = view.requireViewById(R.id.lockPattern)
+
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // observe credential validation attempts and submit/cancel buttons
+ launch {
+ viewModel.header.collect { header ->
+ lockPatternView.setOnPatternListener(
+ OnPatternDetectedListener { pattern ->
+ if (pattern.isPatternLongEnough()) {
+ // Pattern size is less than the minimum
+ // do not count it as a failed attempt
+ viewModel.showPatternTooShortError()
+ } else {
+ lockPatternView.isEnabled = false
+ launch { viewModel.checkCredential(pattern, header) }
+ }
+ }
+ )
+ }
+ }
+
+ launch { viewModel.stealthMode.collect { lockPatternView.isInStealthMode = it } }
+
+ // dismiss on a valid credential check
+ launch {
+ viewModel.validatedAttestation.collect { attestation ->
+ val matched = attestation != null
+ lockPatternView.isEnabled = !matched
+ if (matched) {
+ host.onCredentialMatched(attestation!!)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private class OnPatternDetectedListener(
+ private val onDetected: (pattern: List<LockPatternView.Cell>) -> Unit
+) : LockPatternView.OnPatternListener {
+ override fun onPatternCellAdded(pattern: List<LockPatternView.Cell>) {}
+ override fun onPatternCleared() {}
+ override fun onPatternStart() {}
+ override fun onPatternDetected(pattern: List<LockPatternView.Cell>) {
+ onDetected(pattern)
+ }
+}
+
+private fun List<LockPatternView.Cell>.isPatternLongEnough(): Boolean =
+ size < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
new file mode 100644
index 000000000000..fcc948756972
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -0,0 +1,140 @@
+package com.android.systemui.biometrics.ui.binder
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.biometrics.AuthDialog
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.ui.CredentialPasswordView
+import com.android.systemui.biometrics.ui.CredentialPatternView
+import com.android.systemui.biometrics.ui.CredentialView
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/**
+ * View binder for all credential variants of BiometricPrompt, including [CredentialPatternView] and
+ * [CredentialPasswordView].
+ *
+ * This binder delegates to sub-binders for each variant, such as the [CredentialPasswordViewBinder]
+ * and [CredentialPatternViewBinder].
+ */
+object CredentialViewBinder {
+
+ /** Binds a [CredentialPasswordView] or [CredentialPatternView] to a [CredentialViewModel]. */
+ @JvmStatic
+ fun bind(
+ view: ViewGroup,
+ host: CredentialView.Host,
+ viewModel: CredentialViewModel,
+ panelViewController: AuthPanelController,
+ animatePanel: Boolean,
+ maxErrorDuration: Long = 3_000L,
+ ) {
+ val titleView: TextView = view.requireViewById(R.id.title)
+ val subtitleView: TextView = view.requireViewById(R.id.subtitle)
+ val descriptionView: TextView = view.requireViewById(R.id.description)
+ val iconView: ImageView? = view.findViewById(R.id.icon)
+ val errorView: TextView = view.requireViewById(R.id.error)
+
+ var errorTimer: Job? = null
+
+ // bind common elements
+ view.repeatWhenAttached {
+ if (animatePanel) {
+ with(panelViewController) {
+ // Credential view is always full screen.
+ setUseFullScreen(true)
+ updateForContentDimensions(
+ containerWidth,
+ containerHeight,
+ 0 /* animateDurationMs */
+ )
+ }
+ }
+
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // show prompt metadata
+ launch {
+ viewModel.header.collect { header ->
+ titleView.text = header.title
+ view.announceForAccessibility(header.title)
+
+ subtitleView.textOrHide = header.subtitle
+ descriptionView.textOrHide = header.description
+
+ iconView?.setImageDrawable(header.icon)
+
+ // Only animate this if we're transitioning from a biometric view.
+ if (viewModel.animateContents.value) {
+ view.animateCredentialViewIn()
+ }
+ }
+ }
+
+ // show transient error messages
+ launch {
+ viewModel.errorMessage
+ .onEach { msg ->
+ errorTimer?.cancel()
+ if (msg.isNotBlank()) {
+ errorTimer = launch {
+ delay(maxErrorDuration)
+ viewModel.resetErrorMessage()
+ }
+ }
+ }
+ .collect { errorView.textOrHide = it }
+ }
+
+ // show an extra dialog if the remaining attempts becomes low
+ launch {
+ viewModel.remainingAttempts
+ .filter { it.remaining != null }
+ .collect { info ->
+ host.onCredentialAttemptsRemaining(info.remaining!!, info.message)
+ }
+ }
+ }
+ }
+
+ // bind the auth widget
+ when (view) {
+ is CredentialPasswordView -> CredentialPasswordViewBinder.bind(view, host, viewModel)
+ is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel)
+ else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}")
+ }
+ }
+}
+
+private fun View.animateCredentialViewIn() {
+ translationY = resources.getDimension(R.dimen.biometric_dialog_credential_translation_offset)
+ alpha = 0f
+ postOnAnimation {
+ animate()
+ .translationY(0f)
+ .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS.toLong())
+ .alpha(1f)
+ .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+ .withLayer()
+ .start()
+ }
+}
+
+private var TextView.textOrHide: String?
+ set(value) {
+ val gone = value.isNullOrBlank()
+ visibility = if (gone) View.GONE else View.VISIBLE
+ text = if (gone) "" else value
+ }
+ get() = text?.toString()
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
new file mode 100644
index 000000000000..84bbceb38fa7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -0,0 +1,178 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.text.InputType
+import com.android.internal.widget.LockPatternView
+import com.android.systemui.R
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlin.reflect.KClass
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
+
+/** View-model for all CredentialViews within BiometricPrompt. */
+class CredentialViewModel
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ private val credentialInteractor: BiometricPromptCredentialInteractor,
+) {
+
+ /** Top level information about the prompt. */
+ val header: Flow<HeaderViewModel> =
+ credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map {
+ request ->
+ BiometricPromptHeaderViewModelImpl(
+ request,
+ user = UserHandle.of(request.userInfo.userId),
+ title = request.title,
+ subtitle = request.subtitle,
+ description = request.description,
+ icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId),
+ )
+ }
+
+ /** Input flags for text based credential views */
+ val inputFlags: Flow<Int?> =
+ credentialInteractor.prompt.map {
+ when (it) {
+ is BiometricPromptRequest.Credential.Pin ->
+ InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
+ else -> null
+ }
+ }
+
+ /** If stealth mode is active (hide user credential input). */
+ val stealthMode: Flow<Boolean> =
+ credentialInteractor.prompt.map {
+ when (it) {
+ is BiometricPromptRequest.Credential.Pattern -> it.stealthMode
+ else -> false
+ }
+ }
+
+ private val _animateContents: MutableStateFlow<Boolean> = MutableStateFlow(true)
+ /** If this view should be animated on transitions. */
+ val animateContents = _animateContents.asStateFlow()
+
+ /** Error messages to show the user. */
+ val errorMessage: Flow<String> =
+ combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p ->
+ when (error) {
+ is CredentialStatus.Fail.Error -> error.error
+ ?: applicationContext.asBadCredentialErrorMessage(p)
+ is CredentialStatus.Fail.Throttled -> error.error
+ null -> ""
+ }
+ }
+
+ private val _validatedAttestation: MutableSharedFlow<ByteArray?> = MutableSharedFlow()
+ /** Results of [checkPatternCredential]. A non-null attestation is supplied on success. */
+ val validatedAttestation: Flow<ByteArray?> = _validatedAttestation.asSharedFlow()
+
+ private val _remainingAttempts: MutableStateFlow<RemainingAttempts> =
+ MutableStateFlow(RemainingAttempts())
+ /** If set, the number of remaining attempts before the user must stop. */
+ val remainingAttempts: Flow<RemainingAttempts> = _remainingAttempts.asStateFlow()
+
+ /** Enable transition animations. */
+ fun setAnimateContents(animate: Boolean) {
+ _animateContents.value = animate
+ }
+
+ /** Show an error message to inform the user the pattern is too short to attempt validation. */
+ fun showPatternTooShortError() {
+ credentialInteractor.setVerificationError(
+ CredentialStatus.Fail.Error(
+ applicationContext.asBadCredentialErrorMessage(
+ BiometricPromptRequest.Credential.Pattern::class
+ )
+ )
+ )
+ }
+
+ /** Reset the error message to an empty string. */
+ fun resetErrorMessage() {
+ credentialInteractor.resetVerificationError()
+ }
+
+ /** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */
+ suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) =
+ checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text))
+
+ /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
+ suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) =
+ checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
+
+ private suspend fun checkCredential(result: CredentialStatus) {
+ when (result) {
+ is CredentialStatus.Success.Verified -> {
+ _validatedAttestation.emit(result.hat)
+ _remainingAttempts.value = RemainingAttempts()
+ }
+ is CredentialStatus.Fail.Error -> {
+ _validatedAttestation.emit(null)
+ _remainingAttempts.value =
+ RemainingAttempts(result.remainingAttempts, result.urgentMessage ?: "")
+ }
+ is CredentialStatus.Fail.Throttled -> {
+ // required for completeness, but a throttled error cannot be the final result
+ _validatedAttestation.emit(null)
+ _remainingAttempts.value = RemainingAttempts()
+ }
+ }
+ }
+}
+
+private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String =
+ asBadCredentialErrorMessage(
+ if (prompt != null) prompt::class else BiometricPromptRequest.Credential.Password::class
+ )
+
+private fun <T : BiometricPromptRequest> Context.asBadCredentialErrorMessage(
+ clazz: KClass<T>
+): String =
+ getString(
+ when (clazz) {
+ BiometricPromptRequest.Credential.Pin::class -> R.string.biometric_dialog_wrong_pin
+ BiometricPromptRequest.Credential.Password::class ->
+ R.string.biometric_dialog_wrong_password
+ BiometricPromptRequest.Credential.Pattern::class ->
+ R.string.biometric_dialog_wrong_pattern
+ else -> R.string.biometric_dialog_wrong_password
+ }
+ )
+
+private fun Context.asLockIcon(userId: Int): Drawable {
+ val id =
+ if (Utils.isManagedProfile(this, userId)) {
+ R.drawable.auth_dialog_enterprise
+ } else {
+ R.drawable.auth_dialog_lock
+ }
+ return resources.getDrawable(id, theme)
+}
+
+private class BiometricPromptHeaderViewModelImpl(
+ val request: BiometricPromptRequest.Credential,
+ override val user: UserHandle,
+ override val title: String,
+ override val subtitle: String,
+ override val description: String,
+ override val icon: Drawable,
+) : HeaderViewModel
+
+private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
+ (this as BiometricPromptHeaderViewModelImpl).request
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
new file mode 100644
index 000000000000..ba23f1cfa22d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
@@ -0,0 +1,13 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+
+/** View model for the top-level header / info area of BiometricPrompt. */
+interface HeaderViewModel {
+ val user: UserHandle
+ val title: String
+ val subtitle: String
+ val description: String
+ val icon: Drawable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt
new file mode 100644
index 000000000000..0f221734cb44
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt
@@ -0,0 +1,4 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+/** Metadata about the number of credential attempts the user has left [remaining], if known. */
+data class RemainingAttempts(val remaining: Int? = null, val message: String = "")
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 7e31626983e7..e47e636fa445 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -48,6 +48,7 @@ import com.android.systemui.log.dagger.LogModule;
import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.notetask.NoteTaskModule;
import com.android.systemui.people.PeopleModule;
import com.android.systemui.plugins.BcSmartspaceDataPlugin;
import com.android.systemui.privacy.PrivacyModule;
@@ -152,6 +153,7 @@ import dagger.Provides;
TunerModule.class,
UserModule.class,
UtilModule.class,
+ NoteTaskModule.class,
WalletModule.class
},
subcomponents = {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
index 29bb2f42cca5..41f557850f88 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
@@ -164,7 +164,8 @@ public interface Complication {
COMPLICATION_TYPE_AIR_QUALITY,
COMPLICATION_TYPE_CAST_INFO,
COMPLICATION_TYPE_HOME_CONTROLS,
- COMPLICATION_TYPE_SMARTSPACE
+ COMPLICATION_TYPE_SMARTSPACE,
+ COMPLICATION_TYPE_MEDIA_ENTRY
})
@Retention(RetentionPolicy.SOURCE)
@interface ComplicationType {}
@@ -177,6 +178,7 @@ public interface Complication {
int COMPLICATION_TYPE_CAST_INFO = 1 << 4;
int COMPLICATION_TYPE_HOME_CONTROLS = 1 << 5;
int COMPLICATION_TYPE_SMARTSPACE = 1 << 6;
+ int COMPLICATION_TYPE_MEDIA_ENTRY = 1 << 7;
/**
* The {@link Host} interface specifies a way a {@link Complication} to communicate with its
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
index 75a97de10e7e..18aacd21bd12 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
@@ -20,6 +20,7 @@ import static com.android.systemui.dreams.complication.Complication.COMPLICATION
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_NONE;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
@@ -54,6 +55,8 @@ public class ComplicationUtils {
return COMPLICATION_TYPE_HOME_CONTROLS;
case DreamBackend.COMPLICATION_TYPE_SMARTSPACE:
return COMPLICATION_TYPE_SMARTSPACE;
+ case DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY:
+ return COMPLICATION_TYPE_MEDIA_ENTRY;
default:
return COMPLICATION_TYPE_NONE;
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index 1166c2fc1120..deff0608bedd 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
@@ -55,6 +55,11 @@ public class DreamMediaEntryComplication implements Complication {
return mComponentFactory.create().getViewHolder();
}
+ @Override
+ public int getRequiredTypeAvailability() {
+ return COMPLICATION_TYPE_MEDIA_ENTRY;
+ }
+
/**
* Contains values/logic associated with the dream complication view.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 8f430dea37af..4818bccb1f64 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -118,6 +118,12 @@ object Flags {
*/
@JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212)
+ /**
+ * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
+ * will occur in stages. This is one stage of many to come.
+ */
+ @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true)
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = ReleasedFlag(300)
@@ -325,6 +331,9 @@ object Flags {
// 1800 - shade container
@JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true)
+ // 1900 - note task
+ @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks")
+
// Pay no attention to the reflection behind the curtain.
// ========================== Curtain ==========================
// | |
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 1c6cec22ffca..021431399ab6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -518,7 +518,7 @@ public class KeyguardService extends Service {
@PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
checkPermission();
- mKeyguardViewMediator.onStartedWakingUp(cameraGestureTriggered);
+ mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
mKeyguardLifecyclesDispatcher.dispatch(
KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason);
Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 480fd93fc761..41abb62f05cc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -342,12 +342,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
*/
private int mDelayedProfileShowingSequence;
- /**
- * If the user has disabled the keyguard, then requests to exit, this is
- * how we'll ultimately let them know whether it was successful. We use this
- * var being non-null as an indicator that there is an in progress request.
- */
- private IKeyguardExitCallback mExitSecureCallback;
private final DismissCallbackRegistry mDismissCallbackRegistry;
// the properties of the keyguard
@@ -1342,18 +1336,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
|| !mLockPatternUtils.isSecure(currentUser);
long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser());
mLockLater = false;
- if (mExitSecureCallback != null) {
- if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled");
- try {
- mExitSecureCallback.onKeyguardExitResult(false);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
- }
- mExitSecureCallback = null;
- if (!mExternallyEnabled) {
- hideLocked();
- }
- } else if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) {
+ if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) {
// If we are going to sleep but the keyguard is showing (and will continue to be
// showing, not in the process of going away) then reset its state. Otherwise, let
// this fall through and explicitly re-lock the keyguard.
@@ -1595,7 +1578,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
/**
* It will let us know when the device is waking up.
*/
- public void onStartedWakingUp(boolean cameraGestureTriggered) {
+ public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason,
+ boolean cameraGestureTriggered) {
Trace.beginSection("KeyguardViewMediator#onStartedWakingUp");
// TODO: Rename all screen off/on references to interactive/sleeping
@@ -1610,7 +1594,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence);
notifyStartedWakingUp();
}
- mUpdateMonitor.dispatchStartedWakingUp();
+ mUpdateMonitor.dispatchStartedWakingUp(pmWakeReason);
maybeSendUserPresentBroadcast();
Trace.endSection();
}
@@ -1672,13 +1656,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mExternallyEnabled = enabled;
if (!enabled && mShowing) {
- if (mExitSecureCallback != null) {
- if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring");
- // we're in the process of handling a request to verify the user
- // can get past the keyguard. ignore extraneous requests to disable / re-enable
- return;
- }
-
// hiding keyguard that is showing, remember to reshow later
if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, "
+ "disabling status bar expansion");
@@ -1692,33 +1669,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mNeedToReshowWhenReenabled = false;
updateInputRestrictedLocked();
- if (mExitSecureCallback != null) {
- if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting");
+ showLocked(null);
+
+ // block until we know the keyguard is done drawing (and post a message
+ // to unblock us after a timeout, so we don't risk blocking too long
+ // and causing an ANR).
+ mWaitingUntilKeyguardVisible = true;
+ mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING,
+ KEYGUARD_DONE_DRAWING_TIMEOUT_MS);
+ if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false");
+ while (mWaitingUntilKeyguardVisible) {
try {
- mExitSecureCallback.onKeyguardExitResult(false);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
- mExitSecureCallback = null;
- resetStateLocked();
- } else {
- showLocked(null);
-
- // block until we know the keyguard is done drawing (and post a message
- // to unblock us after a timeout, so we don't risk blocking too long
- // and causing an ANR).
- mWaitingUntilKeyguardVisible = true;
- mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS);
- if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false");
- while (mWaitingUntilKeyguardVisible) {
- try {
- wait();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible");
}
+ if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible");
}
}
}
@@ -1748,13 +1715,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
} catch (RemoteException e) {
Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
}
- } else if (mExitSecureCallback != null) {
- // already in progress with someone else
- try {
- callback.onKeyguardExitResult(false);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
- }
} else if (!isSecure()) {
// Keyguard is not secure, no need to do anything, and we don't need to reshow
@@ -2289,21 +2249,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
return;
}
setPendingLock(false); // user may have authenticated during the screen off animation
- if (mExitSecureCallback != null) {
- try {
- mExitSecureCallback.onKeyguardExitResult(true /* authenciated */);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call onKeyguardExitResult()", e);
- }
-
- mExitSecureCallback = null;
-
- // after successfully exiting securely, no need to reshow
- // the keyguard when they've released the lock
- mExternallyEnabled = true;
- mNeedToReshowWhenReenabled = false;
- updateInputRestricted();
- }
handleHide();
mUpdateMonitor.clearBiometricRecognizedWhenKeyguardDone(currentUser);
@@ -3111,7 +3056,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
pw.print(" mInputRestricted: "); pw.println(mInputRestricted);
pw.print(" mOccluded: "); pw.println(mOccluded);
pw.print(" mDelayedShowingSequence: "); pw.println(mDelayedShowingSequence);
- pw.print(" mExitSecureCallback: "); pw.println(mExitSecureCallback);
pw.print(" mDeviceInteractive: "); pw.println(mDeviceInteractive);
pw.print(" mGoingToSleep: "); pw.println(mGoingToSleep);
pw.print(" mHiding: "); pw.println(mHiding);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
new file mode 100644
index 000000000000..a069582f6692
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.keyguard.data.quickaffordance
+
+/**
+ * Unique identifier keys for all known built-in quick affordances.
+ *
+ * Please ensure uniqueness by never associating more than one class with each key.
+ */
+object BuiltInKeyguardQuickAffordanceKeys {
+ // Please keep alphabetical order of const names to simplify future maintenance.
+ const val HOME_CONTROLS = "home"
+ const val QR_CODE_SCANNER = "qr_code_scanner"
+ const val QUICK_ACCESS_WALLET = "wallet"
+ // Please keep alphabetical order of const names to simplify future maintenance.
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 83842602cbee..d3bb34cd29d4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import android.content.Intent
@@ -51,6 +51,8 @@ constructor(
private val appContext = context.applicationContext
+ override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+
override val state: Flow<KeyguardQuickAffordanceConfig.State> =
component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
if (canShowWhileLocked) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 95027d00c46c..0dd0ad797411 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import android.content.Intent
import com.android.systemui.animation.Expandable
@@ -26,8 +26,18 @@ import kotlinx.coroutines.flow.Flow
/** Defines interface that can act as data source for a single quick affordance model. */
interface KeyguardQuickAffordanceConfig {
+ /** Unique identifier for this quick affordance. It must be globally unique. */
+ val key: String
+
+ /** The observable [State] of the affordance. */
val state: Flow<State>
+ /**
+ * Notifies that the affordance was clicked by the user.
+ *
+ * @param expandable An [Expandable] to use when animating dialogs or activities
+ * @return An [OnClickedResult] telling the caller what to do next
+ */
fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index 502a6070a422..9a441392aa07 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import com.android.systemui.R
import com.android.systemui.animation.Expandable
@@ -37,6 +37,8 @@ constructor(
private val controller: QRCodeScannerController,
) : KeyguardQuickAffordanceConfig {
+ override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+
override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow {
val callback =
object : QRCodeScannerController.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index a24a0d62465f..8a1267ebadd1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import android.graphics.drawable.Drawable
import android.service.quickaccesswallet.GetWalletCardsError
@@ -44,6 +44,8 @@ constructor(
private val activityStarter: ActivityStarter,
) : KeyguardQuickAffordanceConfig {
+ override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+
override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow {
val callback =
object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index e8532ecfdc37..ab25597b077c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -20,6 +20,7 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.annotation.FloatRange
+import android.os.Trace
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -157,12 +158,36 @@ class KeyguardTransitionRepository @Inject constructor() {
value: Float,
transitionState: TransitionState
) {
+ trace(info, transitionState)
+
if (transitionState == TransitionState.FINISHED) {
currentTransitionInfo = null
}
_transitions.value = TransitionStep(info.from, info.to, value, transitionState)
}
+ private fun trace(info: TransitionInfo, transitionState: TransitionState) {
+ if (
+ transitionState != TransitionState.STARTED &&
+ transitionState != TransitionState.FINISHED
+ ) {
+ return
+ }
+ val traceName =
+ "Transition: ${info.from} -> ${info.to} " +
+ if (info.animator == null) {
+ "(manual)"
+ } else {
+ ""
+ }
+ val traceCookie = traceName.hashCode()
+ if (transitionState == TransitionState.STARTED) {
+ Trace.beginAsyncSection(traceName, traceCookie)
+ } else if (transitionState == TransitionState.FINISHED) {
+ Trace.endAsyncSection(traceName, traceCookie)
+ }
+ }
+
companion object {
private const val TAG = "KeyguardTransitionRepository"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index f663b0dd23cd..914b9fc52c1a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -21,15 +21,14 @@ import android.content.Intent
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
-import kotlin.reflect.KClass
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onStart
@@ -70,10 +69,10 @@ constructor(
* @param expandable An optional [Expandable] for the activity- or dialog-launch animation
*/
fun onQuickAffordanceClicked(
- configKey: KClass<out KeyguardQuickAffordanceConfig>,
+ configKey: String,
expandable: Expandable?,
) {
- @Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>)
+ @Suppress("UNCHECKED_CAST") val config = registry.get(configKey)
when (val result = config.onQuickAffordanceClicked(expandable)) {
is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity ->
launchQuickAffordance(
@@ -102,7 +101,7 @@ constructor(
if (index != -1) {
val visibleState = states[index] as KeyguardQuickAffordanceConfig.State.Visible
KeyguardQuickAffordanceModel.Visible(
- configKey = configs[index]::class,
+ configKey = configs[index].key,
icon = visibleState.icon,
toggle = visibleState.toggle,
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 59bb22786917..7409aec57b4c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -24,6 +24,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
/** Encapsulates business-logic related to the keyguard transitions. */
@SysUISingleton
@@ -34,4 +36,17 @@ constructor(
) {
/** AOD->LOCKSCREEN transition information. */
val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+
+ /** LOCKSCREEN->AOD transition information. */
+ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+ /**
+ * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
+ * Lockscreen (0f).
+ */
+ val dozeAmountTransition: Flow<TransitionStep> =
+ merge(
+ aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) },
+ lockscreenToAodTransition,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
index e56b25967910..fc644a9e6067 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt
@@ -18,9 +18,7 @@
package com.android.systemui.keyguard.domain.model
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
-import kotlin.reflect.KClass
/**
* Models a "quick affordance" in the keyguard bottom area (for example, a button on the
@@ -33,7 +31,7 @@ sealed class KeyguardQuickAffordanceModel {
/** A affordance is visible. */
data class Visible(
/** Identifier for the affordance this is modeling. */
- val configKey: KClass<out KeyguardQuickAffordanceConfig>,
+ val configKey: String,
/** An icon for the affordance. */
val icon: Icon,
/** The toggle state for the affordance. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt
index 94024d4a0ace..b48acb65849e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.quickaffordance
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import dagger.Binds
import dagger.Module
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt
index ad40ee7a0183..8526ada69569 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt
@@ -17,14 +17,17 @@
package com.android.systemui.keyguard.domain.quickaffordance
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
+import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import javax.inject.Inject
-import kotlin.reflect.KClass
/** Central registry of all known quick affordance configs. */
interface KeyguardQuickAffordanceRegistry<T : KeyguardQuickAffordanceConfig> {
fun getAll(position: KeyguardQuickAffordancePosition): List<T>
- fun get(configClass: KClass<out T>): T
+ fun get(key: String): T
}
class KeyguardQuickAffordanceRegistryImpl
@@ -46,8 +49,8 @@ constructor(
qrCodeScanner,
),
)
- private val configByClass =
- configsByPosition.values.flatten().associateBy { config -> config::class }
+ private val configByKey =
+ configsByPosition.values.flatten().associateBy { config -> config.key }
override fun getAll(
position: KeyguardQuickAffordancePosition,
@@ -56,8 +59,8 @@ constructor(
}
override fun get(
- configClass: KClass<out KeyguardQuickAffordanceConfig>
+ key: String,
): KeyguardQuickAffordanceConfig {
- return configByClass.getValue(configClass)
+ return configByKey.getValue(key)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
index 581dafa33df7..a18b036c5189 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.keyguard.domain.model
+package com.android.systemui.keyguard.shared.quickaffordance
/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
enum class KeyguardQuickAffordancePosition {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
index 535ca7210244..6aac9124bab9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -22,7 +22,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInterac
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
index bf598ba85932..44f48f97b62e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -18,12 +18,10 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
-import kotlin.reflect.KClass
/** Models the UI state of a keyguard quick affordance button. */
data class KeyguardQuickAffordanceViewModel(
- val configKey: KClass<out KeyguardQuickAffordanceConfig>? = null,
+ val configKey: String? = null,
val isVisible: Boolean = false,
/** Whether to animate the transition of the quick affordance from invisible to visible. */
val animateReveal: Boolean = false,
@@ -33,7 +31,7 @@ data class KeyguardQuickAffordanceViewModel(
val isActivated: Boolean = false,
) {
data class OnClickedParameters(
- val configKey: KClass<out KeyguardQuickAffordanceConfig>,
+ val configKey: String,
val expandable: Expandable?,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
new file mode 100644
index 000000000000..d247f249e2fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.UserManager
+import android.view.KeyEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.wm.shell.floating.FloatingTasks
+import java.util.Optional
+import javax.inject.Inject
+
+/**
+ * Entry point for creating and managing note.
+ *
+ * The controller decides how a note is launched based in the device state: locked or unlocked.
+ *
+ * Currently, we only support a single task per time.
+ */
+@SysUISingleton
+internal class NoteTaskController
+@Inject
+constructor(
+ private val context: Context,
+ private val intentResolver: NoteTaskIntentResolver,
+ private val optionalFloatingTasks: Optional<FloatingTasks>,
+ private val optionalKeyguardManager: Optional<KeyguardManager>,
+ private val optionalUserManager: Optional<UserManager>,
+ @NoteTaskEnabledKey private val isEnabled: Boolean,
+) {
+
+ fun handleSystemKey(keyCode: Int) {
+ if (!isEnabled) return
+
+ if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+ showNoteTask()
+ }
+ }
+
+ private fun showNoteTask() {
+ val floatingTasks = optionalFloatingTasks.getOrNull() ?: return
+ val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
+ val userManager = optionalUserManager.getOrNull() ?: return
+ val intent = intentResolver.resolveIntent() ?: return
+
+ // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
+ if (!userManager.isUserUnlocked) return
+
+ if (keyguardManager.isKeyguardLocked) {
+ context.startActivity(intent)
+ } else {
+ // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
+ floatingTasks.showOrSetStashed(intent)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt
new file mode 100644
index 000000000000..e0bf1da2f652
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import javax.inject.Qualifier
+
+/** Key associated with a [Boolean] flag that enables or disables the note task feature. */
+@Qualifier internal annotation class NoteTaskEnabledKey
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
new file mode 100644
index 000000000000..d84717da3a21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import com.android.systemui.statusbar.CommandQueue
+import com.android.wm.shell.floating.FloatingTasks
+import dagger.Lazy
+import java.util.Optional
+import javax.inject.Inject
+
+/** Class responsible to "glue" all note task dependencies. */
+internal class NoteTaskInitializer
+@Inject
+constructor(
+ private val optionalFloatingTasks: Optional<FloatingTasks>,
+ private val lazyNoteTaskController: Lazy<NoteTaskController>,
+ private val commandQueue: CommandQueue,
+ @NoteTaskEnabledKey private val isEnabled: Boolean,
+) {
+
+ private val callbacks =
+ object : CommandQueue.Callbacks {
+ override fun handleSystemKey(keyCode: Int) {
+ lazyNoteTaskController.get().handleSystemKey(keyCode)
+ }
+ }
+
+ fun initialize() {
+ if (isEnabled && optionalFloatingTasks.isPresent) {
+ commandQueue.addCallback(callbacks)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
new file mode 100644
index 000000000000..98d69910aac3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import javax.inject.Inject
+
+/**
+ * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
+ * [Intent] ready for be launched will be returned. Otherwise, returns null.
+ *
+ * TODO(b/248274123): should be revisited once the notes role is implemented.
+ */
+internal class NoteTaskIntentResolver
+@Inject
+constructor(
+ private val packageManager: PackageManager,
+) {
+
+ fun resolveIntent(): Intent? {
+ val intent = Intent(NOTES_ACTION)
+ val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
+ val infoList = packageManager.queryIntentActivities(intent, flags)
+
+ for (info in infoList) {
+ val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
+ val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
+
+ return Intent(NOTES_ACTION)
+ .setComponent(ComponentName(packageName, activityName))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ return null
+ }
+
+ private fun resolveActivityNameForNotesAction(packageName: String): String? {
+ val intent = Intent(NOTES_ACTION).setPackage(packageName)
+ val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
+ val resolveInfo = packageManager.resolveActivity(intent, flags)
+
+ val activityInfo = resolveInfo?.activityInfo ?: return null
+ if (activityInfo.name.isNullOrBlank()) return null
+ if (!activityInfo.exported) return null
+ if (!activityInfo.enabled) return null
+ if (!activityInfo.showWhenLocked) return null
+ if (!activityInfo.turnScreenOn) return null
+
+ return activityInfo.name
+ }
+
+ companion object {
+ // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
+ const val NOTES_ACTION = "android.intent.action.NOTES"
+ }
+}
+
+private val ActivityInfo.showWhenLocked: Boolean
+ get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0
+
+private val ActivityInfo.turnScreenOn: Boolean
+ get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
new file mode 100644
index 000000000000..035396a6fc76
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.UserManager
+import androidx.core.content.getSystemService
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import dagger.Module
+import dagger.Provides
+import java.util.*
+
+/** Compose all dependencies required by Note Task feature. */
+@Module
+internal class NoteTaskModule {
+
+ @[Provides NoteTaskEnabledKey]
+ fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
+ return featureFlags.isEnabled(Flags.NOTE_TASKS)
+ }
+
+ @Provides
+ fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
+ return Optional.ofNullable(context.getSystemService())
+ }
+
+ @Provides
+ fun provideOptionalUserManager(context: Context): Optional<UserManager> {
+ return Optional.ofNullable(context.getSystemService())
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 976710351a44..c189acec2930 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -649,37 +649,6 @@ public class NotificationIconContainer extends ViewGroup {
return mNumDots > 0;
}
- /**
- * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then
- * extra padding will have to be accounted for
- *
- * This method has no meaning for non-static containers
- */
- public boolean hasPartialOverflow() {
- return mNumDots > 0 && mNumDots < MAX_DOTS;
- }
-
- /**
- * Get padding that can account for extra dots up to the max. The only valid values for
- * this method are for 1 or 2 dots.
- * @return only extraDotPadding or extraDotPadding * 2
- */
- public int getPartialOverflowExtraPadding() {
- if (!hasPartialOverflow()) {
- return 0;
- }
-
- int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding);
-
- int adjustedWidth = getFinalTranslationX() + partialOverflowAmount;
- // In case we actually give too much padding...
- if (adjustedWidth > getWidth()) {
- partialOverflowAmount = getWidth() - getFinalTranslationX();
- }
-
- return partialOverflowAmount;
- }
-
// Give some extra room for btw notifications if we can
public int getNoOverflowExtraPadding() {
if (mNumDots != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
new file mode 100644
index 000000000000..9653985cb6e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.util.kotlin
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import java.util.function.Consumer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
+ * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
+ * during onViewAttached() and removing during onViewRemoved()
+ */
+@JvmOverloads
+fun <T> collectFlow(
+ view: View,
+ flow: Flow<T>,
+ consumer: Consumer<T>,
+ state: Lifecycle.State = Lifecycle.State.CREATED,
+) {
+ view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 42d7d52a71ab..44f6d03207b1 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -47,7 +47,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor;
+import com.android.systemui.wallpapers.canvas.WallpaperLocalColorExtractor;
import com.android.systemui.wallpapers.gl.EglHelper;
import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
@@ -521,7 +521,7 @@ public class ImageWallpaper extends WallpaperService {
class CanvasEngine extends WallpaperService.Engine implements DisplayListener {
private WallpaperManager mWallpaperManager;
- private final WallpaperColorExtractor mWallpaperColorExtractor;
+ private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor;
private SurfaceHolder mSurfaceHolder;
@VisibleForTesting
static final int MIN_SURFACE_WIDTH = 128;
@@ -543,9 +543,9 @@ public class ImageWallpaper extends WallpaperService {
super();
setFixedSizeAllowed(true);
setShowForAllUsers(true);
- mWallpaperColorExtractor = new WallpaperColorExtractor(
+ mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor(
mBackgroundExecutor,
- new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+ new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() {
@Override
public void onColorsProcessed(List<RectF> regions,
List<WallpaperColors> colors) {
@@ -570,7 +570,7 @@ public class ImageWallpaper extends WallpaperService {
// if the number of pages is already computed, transmit it to the color extractor
if (mPagesComputed) {
- mWallpaperColorExtractor.onPageChanged(mPages);
+ mWallpaperLocalColorExtractor.onPageChanged(mPages);
}
}
@@ -597,7 +597,7 @@ public class ImageWallpaper extends WallpaperService {
public void onDestroy() {
getDisplayContext().getSystemService(DisplayManager.class)
.unregisterDisplayListener(this);
- mWallpaperColorExtractor.cleanUp();
+ mWallpaperLocalColorExtractor.cleanUp();
unloadBitmap();
}
@@ -813,7 +813,7 @@ public class ImageWallpaper extends WallpaperService {
@VisibleForTesting
void recomputeColorExtractorMiniBitmap() {
- mWallpaperColorExtractor.onBitmapChanged(mBitmap);
+ mWallpaperLocalColorExtractor.onBitmapChanged(mBitmap);
}
@VisibleForTesting
@@ -830,14 +830,14 @@ public class ImageWallpaper extends WallpaperService {
public void addLocalColorsAreas(@NonNull List<RectF> regions) {
// this call will activate the offset notifications
// if no colors were being processed before
- mWallpaperColorExtractor.addLocalColorsAreas(regions);
+ mWallpaperLocalColorExtractor.addLocalColorsAreas(regions);
}
@Override
public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
// this call will deactivate the offset notifications
// if we are no longer processing colors
- mWallpaperColorExtractor.removeLocalColorAreas(regions);
+ mWallpaperLocalColorExtractor.removeLocalColorAreas(regions);
}
@Override
@@ -853,7 +853,7 @@ public class ImageWallpaper extends WallpaperService {
if (pages != mPages || !mPagesComputed) {
mPages = pages;
mPagesComputed = true;
- mWallpaperColorExtractor.onPageChanged(mPages);
+ mWallpaperLocalColorExtractor.onPageChanged(mPages);
}
}
@@ -881,7 +881,7 @@ public class ImageWallpaper extends WallpaperService {
.getSystemService(WindowManager.class)
.getCurrentWindowMetrics()
.getBounds();
- mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height());
+ mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height());
}
@@ -902,7 +902,7 @@ public class ImageWallpaper extends WallpaperService {
: mBitmap.isRecycled() ? "recycled"
: mBitmap.getWidth() + "x" + mBitmap.getHeight());
- mWallpaperColorExtractor.dump(prefix, fd, out, args);
+ mWallpaperLocalColorExtractor.dump(prefix, fd, out, args);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
index e2e4555bb965..6cac5c952b7c 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
@@ -45,14 +45,14 @@ import java.util.concurrent.Executor;
* It uses a background executor, and uses callbacks to inform that the work is done.
* It uses a downscaled version of the wallpaper to extract the colors.
*/
-public class WallpaperColorExtractor {
+public class WallpaperLocalColorExtractor {
private Bitmap mMiniBitmap;
@VisibleForTesting
static final int SMALL_SIDE = 128;
- private static final String TAG = WallpaperColorExtractor.class.getSimpleName();
+ private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName();
private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
@@ -70,12 +70,12 @@ public class WallpaperColorExtractor {
@Background
private final Executor mBackgroundExecutor;
- private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback;
+ private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback;
/**
* Interface to handle the callbacks after the different steps of the color extraction
*/
- public interface WallpaperColorExtractorCallback {
+ public interface WallpaperLocalColorExtractorCallback {
/**
* Callback after the colors of new regions have been extracted
* @param regions the list of new regions that have been processed
@@ -103,13 +103,13 @@ public class WallpaperColorExtractor {
/**
* Creates a new color extractor.
* @param backgroundExecutor the executor on which the color extraction will be performed
- * @param wallpaperColorExtractorCallback an interface to handle the callbacks from
+ * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from
* the color extractor.
*/
- public WallpaperColorExtractor(@Background Executor backgroundExecutor,
- WallpaperColorExtractorCallback wallpaperColorExtractorCallback) {
+ public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor,
+ WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) {
mBackgroundExecutor = backgroundExecutor;
- mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback;
+ mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback;
}
/**
@@ -157,7 +157,7 @@ public class WallpaperColorExtractor {
mBitmapWidth = bitmap.getWidth();
mBitmapHeight = bitmap.getHeight();
mMiniBitmap = createMiniBitmap(bitmap);
- mWallpaperColorExtractorCallback.onMiniBitmapUpdated();
+ mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated();
recomputeColors();
}
}
@@ -206,7 +206,7 @@ public class WallpaperColorExtractor {
boolean wasActive = isActive();
mPendingRegions.addAll(regions);
if (!wasActive && isActive()) {
- mWallpaperColorExtractorCallback.onActivated();
+ mWallpaperLocalColorExtractorCallback.onActivated();
}
processColorsInternal();
}
@@ -228,7 +228,7 @@ public class WallpaperColorExtractor {
mPendingRegions.removeAll(regions);
regions.forEach(mProcessedRegions::remove);
if (wasActive && !isActive()) {
- mWallpaperColorExtractorCallback.onDeactivated();
+ mWallpaperLocalColorExtractorCallback.onDeactivated();
}
}
}
@@ -252,7 +252,7 @@ public class WallpaperColorExtractor {
}
private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) {
- Trace.beginSection("WallpaperColorExtractor#createMiniBitmap");
+ Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap");
// if both sides of the image are larger than SMALL_SIDE, downscale the bitmap.
int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide);
@@ -359,7 +359,7 @@ public class WallpaperColorExtractor {
*/
if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return;
- Trace.beginSection("WallpaperColorExtractor#processColorsInternal");
+ Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal");
List<WallpaperColors> processedColors = new ArrayList<>();
for (int i = 0; i < mPendingRegions.size(); i++) {
RectF nextArea = mPendingRegions.get(i);
@@ -372,7 +372,7 @@ public class WallpaperColorExtractor {
mPendingRegions.clear();
Trace.endSection();
- mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
+ mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 309f1681b964..02738d5ae48b 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -49,6 +49,7 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.notetask.NoteTaskInitializer;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shared.tracing.ProtoTraceable;
import com.android.systemui.statusbar.CommandQueue;
@@ -58,7 +59,6 @@ import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.tracing.nano.SystemUiTraceProto;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.nano.WmShellTraceProto;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
@@ -113,7 +113,6 @@ public final class WMShell implements
private final Optional<Pip> mPipOptional;
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<OneHanded> mOneHandedOptional;
- private final Optional<FloatingTasks> mFloatingTasksOptional;
private final Optional<DesktopMode> mDesktopModeOptional;
private final CommandQueue mCommandQueue;
@@ -125,6 +124,7 @@ public final class WMShell implements
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final ProtoTracer mProtoTracer;
private final UserTracker mUserTracker;
+ private final NoteTaskInitializer mNoteTaskInitializer;
private final Executor mSysUiMainExecutor;
// Listeners and callbacks. Note that we prefer member variable over anonymous class here to
@@ -176,7 +176,6 @@ public final class WMShell implements
Optional<Pip> pipOptional,
Optional<SplitScreen> splitScreenOptional,
Optional<OneHanded> oneHandedOptional,
- Optional<FloatingTasks> floatingTasksOptional,
Optional<DesktopMode> desktopMode,
CommandQueue commandQueue,
ConfigurationController configurationController,
@@ -187,6 +186,7 @@ public final class WMShell implements
ProtoTracer protoTracer,
WakefulnessLifecycle wakefulnessLifecycle,
UserTracker userTracker,
+ NoteTaskInitializer noteTaskInitializer,
@Main Executor sysUiMainExecutor) {
mContext = context;
mShell = shell;
@@ -203,7 +203,7 @@ public final class WMShell implements
mWakefulnessLifecycle = wakefulnessLifecycle;
mProtoTracer = protoTracer;
mUserTracker = userTracker;
- mFloatingTasksOptional = floatingTasksOptional;
+ mNoteTaskInitializer = noteTaskInitializer;
mSysUiMainExecutor = sysUiMainExecutor;
}
@@ -226,6 +226,8 @@ public final class WMShell implements
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
mDesktopModeOptional.ifPresent(this::initDesktopMode);
+
+ mNoteTaskInitializer.initialize();
}
@VisibleForTesting
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt
new file mode 100644
index 000000000000..6c5620d42abb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.keyguard
+
+import android.os.PowerManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.GlobalSettings
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FaceWakeUpTriggersConfigTest : SysuiTestCase() {
+ @Mock lateinit var globalSettings: GlobalSettings
+ @Mock lateinit var dumpManager: DumpManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun testShouldTriggerFaceAuthOnWakeUpFrom_inConfig_returnsTrue() {
+ val faceWakeUpTriggersConfig =
+ createFaceWakeUpTriggersConfig(
+ intArrayOf(PowerManager.WAKE_REASON_POWER_BUTTON, PowerManager.WAKE_REASON_GESTURE)
+ )
+
+ assertTrue(
+ faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+ PowerManager.WAKE_REASON_POWER_BUTTON
+ )
+ )
+ assertTrue(
+ faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+ PowerManager.WAKE_REASON_GESTURE
+ )
+ )
+ assertFalse(
+ faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+ PowerManager.WAKE_REASON_APPLICATION
+ )
+ )
+ }
+
+ private fun createFaceWakeUpTriggersConfig(wakeUpTriggers: IntArray): FaceWakeUpTriggersConfig {
+ overrideResource(
+ com.android.systemui.R.array.config_face_auth_wake_up_triggers,
+ wakeUpTriggers
+ )
+
+ return FaceWakeUpTriggersConfig(mContext.getResources(), globalSettings, dumpManager)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index c6233b54c028..66be6ecd9da4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -110,6 +110,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.telephony.TelephonyListenerManager;
+import com.android.systemui.util.settings.GlobalSettings;
import org.junit.After;
import org.junit.Assert;
@@ -210,6 +211,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
private UiEventLogger mUiEventLogger;
@Mock
private PowerManager mPowerManager;
+ @Mock
+ private GlobalSettings mGlobalSettings;
+ private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
private final int mCurrentUserId = 100;
private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0);
@@ -237,8 +241,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo);
when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId);
when(mFaceManager.isHardwareDetected()).thenReturn(true);
- when(mFaceManager.hasEnrolledTemplates()).thenReturn(true);
- when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true);
when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties);
when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId);
@@ -294,6 +297,12 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
.when(ActivityManager::getCurrentUser);
ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService);
+ mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig(
+ mContext.getResources(),
+ mGlobalSettings,
+ mDumpManager
+ );
+
mTestableLooper = TestableLooper.get(this);
allowTestableLooperAsMainThread();
mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
@@ -593,7 +602,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
verify(mFaceManager).isHardwareDetected();
- verify(mFaceManager).hasEnrolledTemplates(anyInt());
+ verify(mFaceManager, never()).hasEnrolledTemplates(anyInt());
}
@Test
@@ -607,16 +616,22 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testTriesToAuthenticate_whenKeyguard() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
- mTestableLooper.processAllMessages();
keyguardIsVisible();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+ mTestableLooper.processAllMessages();
verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+ verify(mUiEventLogger).logWithInstanceIdAndPosition(
+ eq(FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP),
+ eq(0),
+ eq(null),
+ any(),
+ eq(PowerManager.WAKE_REASON_POWER_BUTTON));
}
@Test
public void skipsAuthentication_whenStatusBarShadeLocked() {
mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED);
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
@@ -630,7 +645,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
STRONG_AUTH_REQUIRED_AFTER_BOOT);
mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
@@ -684,7 +699,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(strongAuth);
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
@@ -709,7 +724,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testTriesToAuthenticate_whenTrustOnAgentKeyguard_ifBypass() {
mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
when(mKeyguardBypassController.canBypass()).thenReturn(true);
mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
@@ -721,7 +736,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testIgnoresAuth_whenTrustAgentOnKeyguard_withoutBypass() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>());
@@ -732,7 +747,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testIgnoresAuth_whenLockdown() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
@@ -744,7 +759,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testTriesToAuthenticate_whenLockout() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
@@ -768,7 +783,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testFaceAndFingerprintLockout_onlyFace() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
@@ -779,7 +794,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testFaceAndFingerprintLockout_onlyFingerprint() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
@@ -791,7 +806,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testFaceAndFingerprintLockout() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
@@ -890,7 +905,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser)))
.thenReturn(faceLockoutMode);
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
@@ -1064,7 +1079,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testOccludingAppFingerprintListeningState() {
// GIVEN keyguard isn't visible (app occluding)
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
@@ -1079,7 +1094,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testOccludingAppRequestsFingerprint() {
// GIVEN keyguard isn't visible (app occluding)
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
// WHEN an occluding app requests fp
@@ -1170,7 +1185,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
biometricsNotDisabledThroughDevicePolicyManager();
mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED);
setKeyguardBouncerVisibility(false /* isVisible */);
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
when(mKeyguardBypassController.canBypass()).thenReturn(true);
keyguardIsVisible();
@@ -1549,7 +1564,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
@Test
public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
mTestableLooper.processAllMessages();
keyguardIsVisible();
@@ -1598,6 +1613,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
}
+ @Test
+ public void testDreamingStopped_faceDoesNotRun() {
+ mKeyguardUpdateMonitor.dispatchDreamingStopped();
+ mTestableLooper.processAllMessages();
+
+ verify(mFaceManager, never()).authenticate(
+ any(), any(), any(), any(), anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testFaceWakeupTrigger_runFaceAuth_onlyOnConfiguredTriggers() {
+ // keyguard is visible
+ keyguardIsVisible();
+
+ // WHEN device wakes up from an application
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_APPLICATION);
+ mTestableLooper.processAllMessages();
+
+ // THEN face auth isn't triggered
+ verify(mFaceManager, never()).authenticate(
+ any(), any(), any(), any(), anyInt(), anyBoolean());
+
+ // WHEN device wakes up from the power button
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+ mTestableLooper.processAllMessages();
+
+ // THEN face auth is triggered
+ verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+ }
+
private void cleanupKeyguardUpdateMonitor() {
if (mKeyguardUpdateMonitor != null) {
mKeyguardUpdateMonitor.removeCallback(mTestCallback);
@@ -1718,7 +1763,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
}
private void deviceIsInteractive() {
- mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+ mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
}
private void bouncerFullyVisible() {
@@ -1768,7 +1813,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
- mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager);
+ mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager,
+ mFaceWakeUpTriggersConfig);
setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
new file mode 100644
index 000000000000..ae8f419d4e64
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.keyguard;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedStateListDrawable;
+import android.util.Pair;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.doze.util.BurnInHelperKt;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class LockIconViewControllerBaseTest extends SysuiTestCase {
+ protected static final String UNLOCKED_LABEL = "unlocked";
+ protected static final int PADDING = 10;
+
+ protected MockitoSession mStaticMockSession;
+
+ protected @Mock LockIconView mLockIconView;
+ protected @Mock AnimatedStateListDrawable mIconDrawable;
+ protected @Mock Context mContext;
+ protected @Mock Resources mResources;
+ protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
+ protected @Mock StatusBarStateController mStatusBarStateController;
+ protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ protected @Mock KeyguardViewController mKeyguardViewController;
+ protected @Mock KeyguardStateController mKeyguardStateController;
+ protected @Mock FalsingManager mFalsingManager;
+ protected @Mock AuthController mAuthController;
+ protected @Mock DumpManager mDumpManager;
+ protected @Mock AccessibilityManager mAccessibilityManager;
+ protected @Mock ConfigurationController mConfigurationController;
+ protected @Mock VibratorHelper mVibrator;
+ protected @Mock AuthRippleController mAuthRippleController;
+ protected @Mock FeatureFlags mFeatureFlags;
+ protected @Mock KeyguardTransitionRepository mTransitionRepository;
+ protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
+
+ protected LockIconViewController mUnderTest;
+
+ // Capture listeners so that they can be used to send events
+ @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
+ ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+
+ @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
+ ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+ protected KeyguardStateController.Callback mKeyguardStateCallback;
+
+ @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
+ ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+ protected StatusBarStateController.StateListener mStatusBarStateListener;
+
+ @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
+ protected AuthController.Callback mAuthControllerCallback;
+
+ @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback>
+ mKeyguardUpdateMonitorCallbackCaptor =
+ ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+ protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+
+ @Captor protected ArgumentCaptor<Point> mPointCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ mStaticMockSession = mockitoSession()
+ .mockStatic(BurnInHelperKt.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ MockitoAnnotations.initMocks(this);
+
+ setupLockIconViewMocks();
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
+ Rect windowBounds = new Rect(0, 0, 800, 1200);
+ when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
+ when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
+ when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
+ when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
+ when(mAuthController.getScaleFactor()).thenReturn(1f);
+
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+
+ mUnderTest = new LockIconViewController(
+ mLockIconView,
+ mStatusBarStateController,
+ mKeyguardUpdateMonitor,
+ mKeyguardViewController,
+ mKeyguardStateController,
+ mFalsingManager,
+ mAuthController,
+ mDumpManager,
+ mAccessibilityManager,
+ mConfigurationController,
+ mDelayableExecutor,
+ mVibrator,
+ mAuthRippleController,
+ mResources,
+ new KeyguardTransitionInteractor(mTransitionRepository),
+ new KeyguardInteractor(new FakeKeyguardRepository()),
+ mFeatureFlags
+ );
+ }
+
+ @After
+ public void tearDown() {
+ mStaticMockSession.finishMocking();
+ }
+
+ protected Pair<Float, Point> setupUdfps() {
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
+ final Point udfpsLocation = new Point(50, 75);
+ final float radius = 33f;
+ when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
+ when(mAuthController.getUdfpsRadius()).thenReturn(radius);
+
+ return new Pair(radius, udfpsLocation);
+ }
+
+ protected void setupShowLockIcon() {
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+ }
+
+ protected void captureAuthControllerCallback() {
+ verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
+ mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
+ }
+
+ protected void captureKeyguardStateCallback() {
+ verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
+ mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
+ }
+
+ protected void captureStatusBarStateListener() {
+ verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
+ mStatusBarStateListener = mStatusBarStateCaptor.getValue();
+ }
+
+ protected void captureKeyguardUpdateMonitorCallback() {
+ verify(mKeyguardUpdateMonitor).registerCallback(
+ mKeyguardUpdateMonitorCallbackCaptor.capture());
+ mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
+ }
+
+ protected void setupLockIconViewMocks() {
+ when(mLockIconView.getResources()).thenReturn(mResources);
+ when(mLockIconView.getContext()).thenReturn(mContext);
+ }
+
+ protected void resetLockIconView() {
+ reset(mLockIconView);
+ setupLockIconViewMocks();
+ }
+
+ protected void init(boolean useMigrationFlag) {
+ when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag);
+ mUnderTest.init();
+
+ verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture());
+ mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
new file mode 100644
index 000000000000..f4c2284de2d3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.keyguard;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import static com.android.keyguard.LockIconView.ICON_LOCK;
+import static com.android.keyguard.LockIconView.ICON_UNLOCK;
+
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.hardware.biometrics.BiometricSourceType;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.doze.util.BurnInHelperKt;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class LockIconViewControllerTest extends LockIconViewControllerBaseTest {
+
+ @Test
+ public void testUpdateFingerprintLocationOnInit() {
+ // GIVEN fp sensor location is available pre-attached
+ Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+
+ // WHEN lock icon view controller is initialized and attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN lock icon view location is updated to the udfps location with UDFPS radius
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING));
+ }
+
+ @Test
+ public void testUpdatePaddingBasedOnResolutionScale() {
+ // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
+ Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+ when(mAuthController.getScaleFactor()).thenReturn(5f);
+
+ // WHEN lock icon view controller is initialized and attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN lock icon view location is updated with the scaled radius
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING * 5));
+ }
+
+ @Test
+ public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
+ // GIVEN fp sensor location is not available pre-init
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+ when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+ init(/* useMigrationFlag= */false);
+ resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+ // GIVEN fp sensor location is available post-attached
+ captureAuthControllerCallback();
+ Pair<Float, Point> udfps = setupUdfps();
+
+ // WHEN all authenticators are registered
+ mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT);
+ mDelayableExecutor.runAllReady();
+
+ // THEN lock icon view location is updated with the same coordinates as auth controller vals
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING));
+ }
+
+ @Test
+ public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
+ // GIVEN fp sensor location is not available pre-init
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+ when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+ init(/* useMigrationFlag= */false);
+ resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+ // GIVEN fp sensor location is available post-attached
+ captureAuthControllerCallback();
+ Pair<Float, Point> udfps = setupUdfps();
+
+ // WHEN udfps location changes
+ mAuthControllerCallback.onUdfpsLocationChanged();
+ mDelayableExecutor.runAllReady();
+
+ // THEN lock icon view location is updated with the same coordinates as auth controller vals
+ verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+ eq(PADDING));
+ }
+
+ @Test
+ public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
+ // GIVEN Udpfs sensor location is available
+ setupUdfps();
+
+ // WHEN the view is attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN the lock icon view background should be enabled
+ verify(mLockIconView).setUseBackground(true);
+ }
+
+ @Test
+ public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
+ // GIVEN Udfps sensor location is not supported
+ when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+
+ // WHEN the view is attached
+ init(/* useMigrationFlag= */false);
+
+ // THEN the lock icon view background should be disabled
+ verify(mLockIconView).setUseBackground(false);
+ }
+
+ @Test
+ public void testUnlockIconShows_biometricUnlockedTrue() {
+ // GIVEN UDFPS sensor location is available
+ setupUdfps();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureKeyguardUpdateMonitorCallback();
+
+ // GIVEN user has unlocked with a biometric auth (ie: face auth)
+ when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+ reset(mLockIconView);
+
+ // WHEN face auth's biometric running state changes
+ mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+ BiometricSourceType.FACE);
+
+ // THEN the unlock icon is shown
+ verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
+ }
+
+ @Test
+ public void testLockIconStartState() {
+ // GIVEN lock icon state
+ setupShowLockIcon();
+
+ // WHEN lock icon controller is initialized
+ init(/* useMigrationFlag= */false);
+
+ // THEN the lock icon should show
+ verify(mLockIconView).updateIcon(ICON_LOCK, false);
+ }
+
+ @Test
+ public void testLockIcon_updateToUnlock() {
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureKeyguardStateCallback();
+ reset(mLockIconView);
+
+ // WHEN the unlocked state changes to canDismissLockScreen=true
+ when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+ mKeyguardStateCallback.onUnlockedChanged();
+
+ // THEN the unlock should show
+ verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
+ }
+
+ @Test
+ public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
+ // GIVEN udfps not enrolled
+ setupUdfps();
+ when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureStatusBarStateListener();
+ reset(mLockIconView);
+
+ // WHEN the dozing state changes
+ mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+ // THEN the icon is cleared
+ verify(mLockIconView).clearIcon();
+ }
+
+ @Test
+ public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
+ // GIVEN udfps enrolled
+ setupUdfps();
+ when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon();
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */false);
+ captureStatusBarStateListener();
+ reset(mLockIconView);
+
+ // WHEN the dozing state changes
+ mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+ // THEN the AOD lock icon should show
+ verify(mLockIconView).updateIcon(ICON_LOCK, true);
+ }
+
+ @Test
+ public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
+ // GIVEN udfps enrolled
+ setupUdfps();
+ when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+ // GIVEN burn-in offset = 5
+ int burnInOffset = 5;
+ when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
+
+ // GIVEN starting state for the lock icon (keyguard)
+ setupShowLockIcon();
+ init(/* useMigrationFlag= */false);
+ captureStatusBarStateListener();
+ reset(mLockIconView);
+
+ // WHEN dozing updates
+ mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+ mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
+
+ // THEN the view's translation is updated to use the AoD burn-in offsets
+ verify(mLockIconView).setTranslationY(burnInOffset);
+ verify(mLockIconView).setTranslationX(burnInOffset);
+ reset(mLockIconView);
+
+ // WHEN the device is no longer dozing
+ mStatusBarStateListener.onDozingChanged(false /* isDozing */);
+ mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
+
+ // THEN the view is updated to NO translation (no burn-in offsets anymore)
+ verify(mLockIconView).setTranslationY(0);
+ verify(mLockIconView).setTranslationX(0);
+
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
new file mode 100644
index 000000000000..d2c54b4cc0e7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.keyguard
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.keyguard.LockIconView.ICON_LOCK
+import com.android.systemui.doze.util.getBurnInOffset
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() {
+
+ /** After migration, replaces LockIconViewControllerTest version */
+ @Test
+ fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN udfps not enrolled
+ setupUdfps()
+ whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false)
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon()
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */ true)
+ reset(mLockIconView)
+
+ // WHEN the dozing state changes
+ mUnderTest.mIsDozingCallback.accept(true)
+
+ // THEN the icon is cleared
+ verify(mLockIconView).clearIcon()
+ }
+
+ /** After migration, replaces LockIconViewControllerTest version */
+ @Test
+ fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN udfps enrolled
+ setupUdfps()
+ whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+ // GIVEN starting state for the lock icon
+ setupShowLockIcon()
+
+ // GIVEN lock icon controller is initialized and view is attached
+ init(/* useMigrationFlag= */ true)
+ reset(mLockIconView)
+
+ // WHEN the dozing state changes
+ mUnderTest.mIsDozingCallback.accept(true)
+
+ // THEN the AOD lock icon should show
+ verify(mLockIconView).updateIcon(ICON_LOCK, true)
+ }
+
+ /** After migration, replaces LockIconViewControllerTest version */
+ @Test
+ fun testBurnInOffsetsUpdated_onDozeAmountChanged() =
+ runBlocking(IMMEDIATE) {
+ // GIVEN udfps enrolled
+ setupUdfps()
+ whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+ // GIVEN burn-in offset = 5
+ val burnInOffset = 5
+ whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset)
+
+ // GIVEN starting state for the lock icon (keyguard)
+ setupShowLockIcon()
+ init(/* useMigrationFlag= */ true)
+ reset(mLockIconView)
+
+ // WHEN dozing updates
+ mUnderTest.mIsDozingCallback.accept(true)
+ mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+
+ // THEN the view's translation is updated to use the AoD burn-in offsets
+ verify(mLockIconView).setTranslationY(burnInOffset.toFloat())
+ verify(mLockIconView).setTranslationX(burnInOffset.toFloat())
+ reset(mLockIconView)
+
+ // WHEN the device is no longer dozing
+ mUnderTest.mIsDozingCallback.accept(false)
+ mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED))
+
+ // THEN the view is updated to NO translation (no burn-in offsets anymore)
+ verify(mLockIconView).setTranslationY(0f)
+ verify(mLockIconView).setTranslationX(0f)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 19a6c66652dd..77d38c58e685 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -35,6 +35,7 @@ import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
@@ -68,6 +69,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
public MockitoRule mockito = MockitoJUnit.rule();
private Context mContextWrapper;
+ private AccessibilityManager mAccessibilityManager;
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
private AccessibilityFloatingMenuController mController;
private AccessibilityButtonTargetsObserver mTargetsObserver;
@@ -87,6 +89,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
}
};
+ mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(),
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(),
@@ -348,8 +351,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class);
final AccessibilityFloatingMenuController controller =
new AccessibilityFloatingMenuController(mContextWrapper, windowManager,
- displayManager, mTargetsObserver, mModeObserver, mKeyguardUpdateMonitor,
- featureFlags);
+ displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver,
+ mKeyguardUpdateMonitor, featureFlags);
controller.init();
return controller;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
new file mode 100644
index 000000000000..8ef65dcb2c3a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.accessibility.floatingmenu;
+
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.wm.shell.bubbles.DismissView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link DismissAnimationController}. */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class DismissAnimationControllerTest extends SysuiTestCase {
+ private DismissAnimationController mDismissAnimationController;
+ private DismissView mDismissView;
+
+ @Before
+ public void setUp() throws Exception {
+ final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+ final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+ final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+ stubWindowManager);
+ final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel,
+ stubMenuViewAppearance);
+ mDismissView = new DismissView(mContext);
+ mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView);
+ }
+
+ @Test
+ public void showDismissView_success() {
+ mDismissAnimationController.showDismissView(true);
+
+ verify(mDismissView).show();
+ }
+
+ @Test
+ public void hideDismissView_success() {
+ mDismissAnimationController.showDismissView(false);
+
+ verify(mDismissView).hide();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index dbf291c49ee5..d0bd4f7026eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -18,9 +18,16 @@ package com.android.systemui.accessibility.floatingmenu;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
import android.graphics.PointF;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
import android.view.WindowManager;
import androidx.test.filters.SmallTest;
@@ -36,6 +43,8 @@ import org.junit.runner.RunWith;
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
public class MenuAnimationControllerTest extends SysuiTestCase {
+
+ private ViewPropertyAnimator mViewPropertyAnimator;
private MenuView mMenuView;
private MenuAnimationController mMenuAnimationController;
@@ -45,7 +54,11 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
stubWindowManager);
final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
- mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
+
+ mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+ mViewPropertyAnimator = spy(mMenuView.animate());
+ doReturn(mViewPropertyAnimator).when(mMenuView).animate();
+
mMenuAnimationController = new MenuAnimationController(mMenuView);
}
@@ -58,4 +71,20 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
assertThat(mMenuView.getTranslationX()).isEqualTo(50);
assertThat(mMenuView.getTranslationY()).isEqualTo(60);
}
+
+ @Test
+ public void startShrinkAnimation_verifyAnimationEndAction() {
+ mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(View.VISIBLE));
+
+ verify(mViewPropertyAnimator).withEndAction(any(Runnable.class));
+ }
+
+ @Test
+ public void startGrowAnimation_menuCompletelyOpaque() {
+ mMenuAnimationController.startShrinkAnimation(null);
+
+ mMenuAnimationController.startGrowAnimation();
+
+ assertThat(mMenuView.getAlpha()).isEqualTo(/* completelyOpaque */ 1.0f);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index bf6d574a0f67..78ee627a9a2f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -43,6 +43,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -54,6 +55,9 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
+ @Mock
+ private DismissAnimationController.DismissCallback mStubDismissCallback;
+
private RecyclerView mStubListView;
private MenuView mMenuView;
private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate;
@@ -87,7 +91,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info);
- assertThat(info.getActionList().size()).isEqualTo(5);
+ assertThat(info.getActionList().size()).isEqualTo(6);
}
@Test
@@ -156,6 +160,17 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
}
@Test
+ public void performRemoveMenuAction_success() {
+ mMenuAnimationController.setDismissCallback(mStubDismissCallback);
+ final boolean removeMenuAction =
+ mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+ R.id.action_remove_menu, null);
+
+ assertThat(removeMenuAction).isTrue();
+ verify(mMenuAnimationController).removeMenu();
+ }
+
+ @Test
public void performFocusAction_fadeIn() {
mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
ACTION_ACCESSIBILITY_FOCUS, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
index c5b9a294fc34..4acb394bee95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -21,6 +21,8 @@ import static android.view.View.OVER_SCROLL_NEVER;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -36,6 +38,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.MotionEventHelper;
+import com.android.wm.shell.bubbles.DismissView;
import org.junit.After;
import org.junit.Before;
@@ -57,7 +60,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase {
private MenuView mStubMenuView;
private MenuListViewTouchHandler mTouchHandler;
private MenuAnimationController mMenuAnimationController;
+ private DismissAnimationController mDismissAnimationController;
private RecyclerView mStubListView;
+ private DismissView mDismissView;
@Before
public void setUp() throws Exception {
@@ -69,7 +74,11 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase {
mStubMenuView.setTranslationX(0);
mStubMenuView.setTranslationY(0);
mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView));
- mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController);
+ mDismissView = spy(new DismissView(mContext));
+ mDismissAnimationController =
+ spy(new DismissAnimationController(mDismissView, mStubMenuView));
+ mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
+ mDismissAnimationController);
final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets);
mStubListView = (RecyclerView) mStubMenuView.getChildAt(0);
mStubListView.setAdapter(stubAdapter);
@@ -88,7 +97,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase {
}
@Test
- public void onActionMoveEvent_shouldMoveToPosition() {
+ public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() {
+ doReturn(false).when(mDismissAnimationController).maybeConsumeMoveMotionEvent(
+ any(MotionEvent.class));
final int offset = 100;
final MotionEvent stubDownEvent =
mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
@@ -108,6 +119,24 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase {
}
@Test
+ public void onActionMoveEvent_shouldShowDismissView() {
+ final int offset = 100;
+ final MotionEvent stubDownEvent =
+ mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+ MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+ mStubMenuView.getTranslationY());
+ final MotionEvent stubMoveEvent =
+ mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+ MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+ mStubMenuView.getTranslationY() + offset);
+
+ mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+ mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+
+ verify(mDismissView).show();
+ }
+
+ @Test
public void dragAndDrop_shouldFlingMenuThenSpringToEdge() {
final int offset = 100;
final MotionEvent stubDownEvent =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
index 8c8d6aca7cd7..dd7ce0e06c32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
@@ -34,6 +34,7 @@ import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
+import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
@@ -59,6 +60,9 @@ public class MenuViewLayerControllerTest extends SysuiTestCase {
private WindowManager mWindowManager;
@Mock
+ private AccessibilityManager mAccessibilityManager;
+
+ @Mock
private WindowMetrics mWindowMetrics;
private MenuViewLayerController mMenuViewLayerController;
@@ -72,7 +76,8 @@ public class MenuViewLayerControllerTest extends SysuiTestCase {
when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340));
when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets());
- mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager);
+ mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager,
+ mAccessibilityManager);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 23c6ef1338b3..d20eeafde09c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -23,18 +23,25 @@ import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.Laye
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
/** Tests for {@link MenuViewLayer}. */
@RunWith(AndroidTestingRunner.class)
@@ -43,10 +50,19 @@ import org.junit.runner.RunWith;
public class MenuViewLayerTest extends SysuiTestCase {
private MenuViewLayer mMenuViewLayer;
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private IAccessibilityFloatingMenu mFloatingMenu;
+
@Before
public void setUp() throws Exception {
final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
- mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager);
+ final AccessibilityManager stubAccessibilityManager = mContext.getSystemService(
+ AccessibilityManager.class);
+ mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, stubAccessibilityManager,
+ mFloatingMenu);
}
@Test
@@ -64,4 +80,11 @@ public class MenuViewLayerTest extends SysuiTestCase {
assertThat(menuView.getVisibility()).isEqualTo(GONE);
}
+
+ @Test
+ public void tiggerDismissMenuAction_hideFloatingMenu() {
+ mMenuViewLayer.mDismissMenuAction.run();
+
+ verify(mFloatingMenu).hide();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index d1107c612977..45b8ce1ce247 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -41,10 +41,15 @@ import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
import org.junit.After
import org.junit.Rule
import org.junit.Test
@@ -80,6 +85,15 @@ class AuthContainerViewTest : SysuiTestCase() {
@Mock
lateinit var interactionJankMonitor: InteractionJankMonitor
+ private val biometricPromptRepository = FakePromptRepository()
+ private val credentialInteractor = FakeCredentialInteractor()
+ private val bpCredentialInteractor = BiometricPromptCredentialInteractor(
+ Dispatchers.Main.immediate,
+ biometricPromptRepository,
+ credentialInteractor
+ )
+ private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor)
+
private var authContainer: TestAuthContainerView? = null
@After
@@ -466,6 +480,8 @@ class AuthContainerViewTest : SysuiTestCase() {
userManager,
lockPatternUtils,
interactionJankMonitor,
+ { bpCredentialInteractor },
+ { credentialViewModel },
Handler(TestableLooper.get(this).looper),
FakeExecutor(FakeSystemClock())
) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 8e45067c8e13..4dd46edd0912 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -25,7 +25,9 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWA
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -87,6 +89,8 @@ import com.android.internal.R;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
@@ -163,6 +167,11 @@ public class AuthControllerTest extends SysuiTestCase {
private UdfpsLogger mUdfpsLogger;
@Mock
private InteractionJankMonitor mInteractionJankMonitor;
+ @Mock
+ private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
+ @Mock
+ private CredentialViewModel mCredentialViewModel;
+
@Captor
private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
@Captor
@@ -236,7 +245,7 @@ public class AuthControllerTest extends SysuiTestCase {
2 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
1 /* maxEnrollmentsPerUser */,
- fpComponentInfo,
+ faceComponentInfo,
FaceSensorProperties.TYPE_RGB,
true /* supportsFaceDetection */,
true /* supportsSelfIllumination */,
@@ -276,8 +285,6 @@ public class AuthControllerTest extends SysuiTestCase {
reset(mFingerprintManager);
reset(mFaceManager);
- when(mVibratorHelper.hasVibrator()).thenReturn(true);
-
// This test requires an uninitialized AuthController.
AuthController authController = new TestableAuthController(mContextSpy, mExecution,
mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager,
@@ -308,8 +315,6 @@ public class AuthControllerTest extends SysuiTestCase {
reset(mFingerprintManager);
reset(mFaceManager);
- when(mVibratorHelper.hasVibrator()).thenReturn(true);
-
// This test requires an uninitialized AuthController.
AuthController authController = new TestableAuthController(mContextSpy, mExecution,
mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager,
@@ -343,6 +348,36 @@ public class AuthControllerTest extends SysuiTestCase {
}
@Test
+ public void testFaceAuthEnrollmentStatus() throws RemoteException {
+ final int userId = 0;
+
+ reset(mFaceManager);
+ mAuthController.start();
+
+ verify(mFaceManager).addAuthenticatorsRegisteredCallback(
+ mFaceAuthenticatorsRegisteredCaptor.capture());
+
+ mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(
+ mFaceManager.getSensorPropertiesInternal());
+ mTestableLooper.processAllMessages();
+
+ verify(mFaceManager).registerBiometricStateListener(
+ mBiometricStateCaptor.capture());
+
+ assertFalse(mAuthController.isFaceAuthEnrolled(userId));
+
+ // Enrollments changed for an unknown sensor.
+ for (BiometricStateListener listener : mBiometricStateCaptor.getAllValues()) {
+ listener.onEnrollmentsChanged(userId,
+ 2 /* sensorId */, true /* hasEnrollments */);
+ }
+ mTestableLooper.processAllMessages();
+
+ assertTrue(mAuthController.isFaceAuthEnrolled(userId));
+ }
+
+
+ @Test
public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception {
showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */);
mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED,
@@ -981,6 +1016,7 @@ public class AuthControllerTest extends SysuiTestCase {
fingerprintManager, faceManager, udfpsControllerFactory,
sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle,
mUserManager, mLockPatternUtils, mUdfpsLogger, statusBarStateController,
+ () -> mBiometricPromptCredentialInteractor, () -> mCredentialViewModel,
mInteractionJankMonitor, mHandler, mBackgroundExecutor, vibratorHelper);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 8820c164cba4..1379a0eeebdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -22,12 +22,11 @@ import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorProperties
-import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.face.FaceSensorProperties
+import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.Bundle
-
import android.testing.ViewUtils
import android.view.LayoutInflater
@@ -83,26 +82,31 @@ internal fun AuthBiometricView?.destroyDialog() {
internal fun fingerprintSensorPropertiesInternal(
ids: List<Int> = listOf(0)
): List<FingerprintSensorPropertiesInternal> {
- val componentInfo = listOf(
+ val componentInfo =
+ listOf(
ComponentInfoInternal(
- "fingerprintSensor" /* componentId */,
- "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
- "00000001" /* serialNumber */, "" /* softwareVersion */
+ "fingerprintSensor" /* componentId */,
+ "vendor/model/revision" /* hardwareVersion */,
+ "1.01" /* firmwareVersion */,
+ "00000001" /* serialNumber */,
+ "" /* softwareVersion */
),
ComponentInfoInternal(
- "matchingAlgorithm" /* componentId */,
- "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */
+ "matchingAlgorithm" /* componentId */,
+ "" /* hardwareVersion */,
+ "" /* firmwareVersion */,
+ "" /* serialNumber */,
+ "vendor/version/revision" /* softwareVersion */
)
- )
+ )
return ids.map { id ->
FingerprintSensorPropertiesInternal(
- id,
- SensorProperties.STRENGTH_STRONG,
- 5 /* maxEnrollmentsPerUser */,
- componentInfo,
- FingerprintSensorProperties.TYPE_REAR,
- false /* resetLockoutRequiresHardwareAuthToken */
+ id,
+ SensorProperties.STRENGTH_STRONG,
+ 5 /* maxEnrollmentsPerUser */,
+ componentInfo,
+ FingerprintSensorProperties.TYPE_REAR,
+ false /* resetLockoutRequiresHardwareAuthToken */
)
}
}
@@ -111,28 +115,53 @@ internal fun fingerprintSensorPropertiesInternal(
internal fun faceSensorPropertiesInternal(
ids: List<Int> = listOf(1)
): List<FaceSensorPropertiesInternal> {
- val componentInfo = listOf(
+ val componentInfo =
+ listOf(
ComponentInfoInternal(
- "faceSensor" /* componentId */,
- "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
- "00000001" /* serialNumber */, "" /* softwareVersion */
+ "faceSensor" /* componentId */,
+ "vendor/model/revision" /* hardwareVersion */,
+ "1.01" /* firmwareVersion */,
+ "00000001" /* serialNumber */,
+ "" /* softwareVersion */
),
ComponentInfoInternal(
- "matchingAlgorithm" /* componentId */,
- "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */
+ "matchingAlgorithm" /* componentId */,
+ "" /* hardwareVersion */,
+ "" /* firmwareVersion */,
+ "" /* serialNumber */,
+ "vendor/version/revision" /* softwareVersion */
)
- )
+ )
return ids.map { id ->
FaceSensorPropertiesInternal(
- id,
- SensorProperties.STRENGTH_STRONG,
- 2 /* maxEnrollmentsPerUser */,
- componentInfo,
- FaceSensorProperties.TYPE_RGB,
- true /* supportsFaceDetection */,
- true /* supportsSelfIllumination */,
- false /* resetLockoutRequiresHardwareAuthToken */
+ id,
+ SensorProperties.STRENGTH_STRONG,
+ 2 /* maxEnrollmentsPerUser */,
+ componentInfo,
+ FaceSensorProperties.TYPE_RGB,
+ true /* supportsFaceDetection */,
+ true /* supportsSelfIllumination */,
+ false /* resetLockoutRequiresHardwareAuthToken */
)
}
}
+
+internal fun promptInfo(
+ title: String = "title",
+ subtitle: String = "sub",
+ description: String = "desc",
+ credentialTitle: String? = "cred title",
+ credentialSubtitle: String? = "cred sub",
+ credentialDescription: String? = "cred desc",
+ negativeButton: String = "neg",
+): PromptInfo {
+ val info = PromptInfo()
+ info.title = title
+ info.subtitle = subtitle
+ info.description = description
+ credentialTitle?.let { info.deviceCredentialTitle = it }
+ credentialSubtitle?.let { info.deviceCredentialSubtitle = it }
+ credentialDescription?.let { info.deviceCredentialDescription = it }
+ info.negativeButtonText = negativeButton
+ return info
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
new file mode 100644
index 000000000000..2d5614c15173
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
@@ -0,0 +1,81 @@
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptRepositoryImplTest : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var authController: AuthController
+
+ private lateinit var repository: PromptRepositoryImpl
+
+ @Before
+ fun setup() {
+ repository = PromptRepositoryImpl(authController)
+ }
+
+ @Test
+ fun isShowing() = runBlockingTest {
+ whenever(authController.isShowing).thenReturn(true)
+
+ val values = mutableListOf<Boolean>()
+ val job = launch { repository.isShowing.toList(values) }
+ assertThat(values).containsExactly(true)
+
+ withArgCaptor<AuthController.Callback> {
+ verify(authController).addCallback(capture())
+
+ value.onBiometricPromptShown()
+ assertThat(values).containsExactly(true, true)
+
+ value.onBiometricPromptDismissed()
+ assertThat(values).containsExactly(true, true, false).inOrder()
+
+ job.cancel()
+ verify(authController).removeCallback(eq(value))
+ }
+ }
+
+ @Test
+ fun setsAndUnsetsPrompt() = runBlockingTest {
+ val kind = PromptKind.PIN
+ val uid = 8
+ val challenge = 90L
+ val promptInfo = PromptInfo()
+
+ repository.setPrompt(promptInfo, uid, challenge, kind)
+
+ assertThat(repository.kind.value).isEqualTo(kind)
+ assertThat(repository.userId.value).isEqualTo(uid)
+ assertThat(repository.challenge.value).isEqualTo(challenge)
+ assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo)
+
+ repository.unsetPrompt()
+
+ assertThat(repository.promptInfo.value).isNull()
+ assertThat(repository.userId.value).isNull()
+ assertThat(repository.challenge.value).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
new file mode 100644
index 000000000000..97d3e688ed80
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
@@ -0,0 +1,216 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResourcesManager
+import android.content.pm.UserInfo
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockscreenCredential
+import com.android.internal.widget.VerifyCredentialResponse
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 22
+private const val OPERATION_ID = 100L
+private const val MAX_ATTEMPTS = 5
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class CredentialInteractorImplTest : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+ @Mock private lateinit var userManager: UserManager
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var devicePolicyResourcesManager: DevicePolicyResourcesManager
+
+ private val systemClock = FakeSystemClock()
+
+ private lateinit var interactor: CredentialInteractorImpl
+
+ @Before
+ fun setup() {
+ whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
+ whenever(lockPatternUtils.getMaximumFailedPasswordsForWipe(anyInt()))
+ .thenReturn(MAX_ATTEMPTS)
+ whenever(userManager.getUserInfo(eq(USER_ID))).thenReturn(UserInfo(USER_ID, "", 0))
+ whenever(devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(eq(USER_ID)))
+ .thenReturn(USER_ID)
+
+ interactor =
+ CredentialInteractorImpl(
+ mContext,
+ lockPatternUtils,
+ userManager,
+ devicePolicyManager,
+ systemClock
+ )
+ }
+
+ @Test
+ fun testStealthMode() {
+ for (value in listOf(true, false, false, true)) {
+ whenever(lockPatternUtils.isVisiblePatternEnabled(eq(USER_ID))).thenReturn(value)
+
+ assertThat(interactor.isStealthModeActive(USER_ID)).isEqualTo(!value)
+ }
+ }
+
+ @Test
+ fun testCredentialOwner() {
+ for (value in listOf(12, 8, 4)) {
+ whenever(userManager.getCredentialOwnerProfile(eq(USER_ID))).thenReturn(value)
+
+ assertThat(interactor.getCredentialOwnerOrSelfId(USER_ID)).isEqualTo(value)
+ }
+ }
+
+ @Test fun pinCredentialWhenGood() = pinCredential(goodCredential())
+
+ @Test fun pinCredentialWhenBad() = pinCredential(badCredential())
+
+ @Test fun pinCredentialWhenBadAndThrottled() = pinCredential(badCredential(timeout = 5_000))
+
+ private fun pinCredential(result: VerifyCredentialResponse) = runTest {
+ val usedAttempts = 1
+ whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+ .thenReturn(usedAttempts)
+ whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())).thenReturn(result)
+ whenever(lockPatternUtils.verifyGatekeeperPasswordHandle(anyLong(), anyLong(), eq(USER_ID)))
+ .thenReturn(result)
+ whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer {
+ systemClock.elapsedRealtime() + (it.arguments[1] as Int)
+ }
+
+ // wrap in an async block so the test can advance the clock if throttling credential
+ // checks prevents the method from returning
+ val statusList = mutableListOf<CredentialStatus>()
+ interactor
+ .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
+ .toList(statusList)
+
+ val last = statusList.removeLastOrNull()
+ if (result.isMatched) {
+ assertThat(statusList).isEmpty()
+ val successfulResult = last as? CredentialStatus.Success.Verified
+ assertThat(successfulResult).isNotNull()
+ assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT)
+
+ verify(lockPatternUtils).userPresent(eq(USER_ID))
+ verify(lockPatternUtils)
+ .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle))
+ } else {
+ val failedResult = last as? CredentialStatus.Fail.Error
+ assertThat(failedResult).isNotNull()
+ assertThat(failedResult!!.remainingAttempts)
+ .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1)
+ assertThat(failedResult.urgentMessage).isNull()
+
+ if (result.timeout > 0) { // failed and throttled
+ // messages are in the throttled errors, so the final Error.error is empty
+ assertThat(failedResult.error).isEmpty()
+ assertThat(statusList).isNotEmpty()
+ assertThat(statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java))
+ .hasSize(statusList.size)
+
+ verify(lockPatternUtils).setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout))
+ } else { // failed
+ assertThat(failedResult.error)
+ .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern())
+ assertThat(statusList).isEmpty()
+
+ verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+ }
+ }
+ }
+
+ @Test
+ fun pinCredentialWhenBadAndFinalAttempt() = runTest {
+ whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt()))
+ .thenReturn(badCredential())
+ whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+ .thenReturn(MAX_ATTEMPTS - 2)
+
+ val statusList = mutableListOf<CredentialStatus>()
+ interactor
+ .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
+ .toList(statusList)
+
+ val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error
+ assertThat(result).isNotNull()
+ assertThat(result!!.remainingAttempts).isEqualTo(1)
+ assertThat(result.urgentMessage).isNotEmpty()
+ assertThat(statusList).isEmpty()
+
+ verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+ }
+
+ @Test
+ fun pinCredentialWhenBadAndNoMoreAttempts() = runTest {
+ whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt()))
+ .thenReturn(badCredential())
+ whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+ .thenReturn(MAX_ATTEMPTS - 1)
+ whenever(devicePolicyResourcesManager.getString(any(), any())).thenReturn("wipe")
+
+ val statusList = mutableListOf<CredentialStatus>()
+ interactor
+ .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234"))
+ .toList(statusList)
+
+ val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error
+ assertThat(result).isNotNull()
+ assertThat(result!!.remainingAttempts).isEqualTo(0)
+ assertThat(result.urgentMessage).isNotEmpty()
+ assertThat(statusList).isEmpty()
+
+ verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+ }
+}
+
+private fun pinRequest(): BiometricPromptRequest.Credential.Pin =
+ BiometricPromptRequest.Credential.Pin(
+ promptInfo(),
+ BiometricUserInfo(USER_ID),
+ BiometricOperationInfo(OPERATION_ID)
+ )
+
+private fun goodCredential(
+ passwordHandle: Long = 90,
+ hat: ByteArray = ByteArray(69),
+): VerifyCredentialResponse =
+ VerifyCredentialResponse.Builder()
+ .setGatekeeperPasswordHandle(passwordHandle)
+ .setGatekeeperHAT(hat)
+ .build()
+
+private fun badCredential(timeout: Int = 0): VerifyCredentialResponse =
+ if (timeout > 0) {
+ VerifyCredentialResponse.fromTimeout(timeout)
+ } else {
+ VerifyCredentialResponse.fromError()
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
new file mode 100644
index 000000000000..dbcbf415221e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
@@ -0,0 +1,270 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.promptInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 22
+private const val OPERATION_ID = 100L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptCredentialInteractorTest : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ private val dispatcher = UnconfinedTestDispatcher()
+ private val biometricPromptRepository = FakePromptRepository()
+ private val credentialInteractor = FakeCredentialInteractor()
+
+ private lateinit var interactor: BiometricPromptCredentialInteractor
+
+ @Before
+ fun setup() {
+ interactor =
+ BiometricPromptCredentialInteractor(
+ dispatcher,
+ biometricPromptRepository,
+ credentialInteractor
+ )
+ }
+
+ @Test
+ fun testIsShowing() =
+ runTest(dispatcher) {
+ var showing = false
+ val job = launch { interactor.isShowing.collect { showing = it } }
+
+ biometricPromptRepository.setIsShowing(false)
+ assertThat(showing).isFalse()
+
+ biometricPromptRepository.setIsShowing(true)
+ assertThat(showing).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun testShowError() =
+ runTest(dispatcher) {
+ var error: CredentialStatus.Fail? = null
+ val job = launch { interactor.verificationError.collect { error = it } }
+
+ for (msg in listOf("once", "again")) {
+ interactor.setVerificationError(error(msg))
+ assertThat(error).isEqualTo(error(msg))
+ }
+
+ interactor.resetVerificationError()
+ assertThat(error).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun nullWhenNoPromptInfo() =
+ runTest(dispatcher) {
+ var prompt: BiometricPromptRequest? = null
+ val job = launch { interactor.prompt.collect { prompt = it } }
+
+ assertThat(prompt).isNull()
+
+ job.cancel()
+ }
+
+ @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN)
+
+ @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD)
+
+ @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN)
+
+ private fun useCredentialForPrompt(kind: Int) =
+ runTest(dispatcher) {
+ val isStealth = false
+ credentialInteractor.stealthMode = isStealth
+
+ var prompt: BiometricPromptRequest? = null
+ val job = launch { interactor.prompt.collect { prompt = it } }
+
+ val title = "what a prompt"
+ val subtitle = "s"
+ val description = "something to see"
+
+ interactor.useCredentialsForAuthentication(
+ PromptInfo().also {
+ it.title = title
+ it.description = description
+ it.subtitle = subtitle
+ },
+ kind = kind,
+ userId = USER_ID,
+ challenge = OPERATION_ID
+ )
+
+ val p = prompt as? BiometricPromptRequest.Credential
+ assertThat(p).isNotNull()
+ assertThat(p!!.title).isEqualTo(title)
+ assertThat(p.subtitle).isEqualTo(subtitle)
+ assertThat(p.description).isEqualTo(description)
+ assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+ assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ assertThat(p)
+ .isInstanceOf(
+ when (kind) {
+ Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java
+ Utils.CREDENTIAL_PASSWORD ->
+ BiometricPromptRequest.Credential.Password::class.java
+ Utils.CREDENTIAL_PATTERN ->
+ BiometricPromptRequest.Credential.Pattern::class.java
+ else -> throw Exception("wrong kind")
+ }
+ )
+ if (p is BiometricPromptRequest.Credential.Pattern) {
+ assertThat(p.stealthMode).isEqualTo(isStealth)
+ }
+
+ interactor.resetPrompt()
+
+ assertThat(prompt).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun checkCredential() =
+ runTest(dispatcher) {
+ val hat = ByteArray(4)
+ credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) }
+
+ val errors = mutableListOf<CredentialStatus.Fail?>()
+ val job = launch { interactor.verificationError.toList(errors) }
+
+ val checked =
+ interactor.checkCredential(pinRequest(), text = "1234")
+ as? CredentialStatus.Success.Verified
+
+ assertThat(checked).isNotNull()
+ assertThat(checked!!.hat).isSameInstanceAs(hat)
+ assertThat(errors.map { it?.error }).containsExactly(null)
+
+ job.cancel()
+ }
+
+ @Test
+ fun checkCredentialWhenBad() =
+ runTest(dispatcher) {
+ val errorMessage = "bad"
+ val remainingAttempts = 12
+ credentialInteractor.verifyCredentialResponse = { _ ->
+ flowOf(error(errorMessage, remainingAttempts))
+ }
+
+ val errors = mutableListOf<CredentialStatus.Fail?>()
+ val job = launch { interactor.verificationError.toList(errors) }
+
+ val checked =
+ interactor.checkCredential(pinRequest(), text = "1234")
+ as? CredentialStatus.Fail.Error
+
+ assertThat(checked).isNotNull()
+ assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+ assertThat(checked.urgentMessage).isNull()
+ assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder()
+
+ job.cancel()
+ }
+
+ @Test
+ fun checkCredentialWhenBadAndUrgentMessage() =
+ runTest(dispatcher) {
+ val error = "not so bad"
+ val urgentMessage = "really bad"
+ credentialInteractor.verifyCredentialResponse = { _ ->
+ flowOf(error(error, 10, urgentMessage))
+ }
+
+ val errors = mutableListOf<CredentialStatus.Fail?>()
+ val job = launch { interactor.verificationError.toList(errors) }
+
+ val checked =
+ interactor.checkCredential(pinRequest(), text = "1234")
+ as? CredentialStatus.Fail.Error
+
+ assertThat(checked).isNotNull()
+ assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage)
+ assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder()
+ assertThat(errors.last() as? CredentialStatus.Fail.Error)
+ .isEqualTo(error(error, 10, urgentMessage))
+
+ job.cancel()
+ }
+
+ @Test
+ fun checkCredentialWhenBadAndThrottled() =
+ runTest(dispatcher) {
+ val remainingAttempts = 3
+ val error = ":("
+ val urgentMessage = ":D"
+ credentialInteractor.verifyCredentialResponse = { _ ->
+ flow {
+ for (i in 1..3) {
+ emit(throttled("$i"))
+ delay(100)
+ }
+ emit(error(error, remainingAttempts, urgentMessage))
+ }
+ }
+ val errors = mutableListOf<CredentialStatus.Fail?>()
+ val job = launch { interactor.verificationError.toList(errors) }
+
+ val checked =
+ interactor.checkCredential(pinRequest(), text = "1234")
+ as? CredentialStatus.Fail.Error
+
+ assertThat(checked).isNotNull()
+ assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+ assertThat(checked.urgentMessage).isEqualTo(urgentMessage)
+ assertThat(errors.map { it?.error })
+ .containsExactly(null, "1", "2", "3", error)
+ .inOrder()
+
+ job.cancel()
+ }
+}
+
+private fun pinRequest(): BiometricPromptRequest.Credential.Pin =
+ BiometricPromptRequest.Credential.Pin(
+ promptInfo(),
+ BiometricUserInfo(USER_ID),
+ BiometricOperationInfo(OPERATION_ID)
+ )
+
+private fun verified(hat: ByteArray) = CredentialStatus.Success.Verified(hat)
+
+private fun throttled(error: String) = CredentialStatus.Fail.Throttled(error)
+
+private fun error(error: String? = null, remaining: Int? = null, urgentMessage: String? = null) =
+ CredentialStatus.Fail.Error(error, remaining, urgentMessage)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
new file mode 100644
index 000000000000..2eeff9fcdd8a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -0,0 +1,92 @@
+package com.android.systemui.biometrics.domain.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.biometrics.promptInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val USER_ID = 2
+private const val OPERATION_ID = 8L
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricPromptRequestTest {
+
+ @Test
+ fun biometricRequestFromPromptInfo() {
+ val title = "what"
+ val subtitle = "a"
+ val description = "request"
+
+ val request =
+ BiometricPromptRequest.Biometric(
+ promptInfo(title = title, subtitle = subtitle, description = description),
+ BiometricUserInfo(USER_ID),
+ BiometricOperationInfo(OPERATION_ID)
+ )
+
+ assertThat(request.title).isEqualTo(title)
+ assertThat(request.subtitle).isEqualTo(subtitle)
+ assertThat(request.description).isEqualTo(description)
+ assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+ assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ }
+
+ @Test
+ fun credentialRequestFromPromptInfo() {
+ val title = "what"
+ val subtitle = "a"
+ val description = "request"
+ val stealth = true
+
+ val toCheck =
+ listOf(
+ BiometricPromptRequest.Credential.Pin(
+ promptInfo(
+ title = title,
+ subtitle = subtitle,
+ description = description,
+ credentialTitle = null,
+ credentialSubtitle = null,
+ credentialDescription = null
+ ),
+ BiometricUserInfo(USER_ID),
+ BiometricOperationInfo(OPERATION_ID)
+ ),
+ BiometricPromptRequest.Credential.Password(
+ promptInfo(
+ credentialTitle = title,
+ credentialSubtitle = subtitle,
+ credentialDescription = description
+ ),
+ BiometricUserInfo(USER_ID),
+ BiometricOperationInfo(OPERATION_ID)
+ ),
+ BiometricPromptRequest.Credential.Pattern(
+ promptInfo(
+ subtitle = subtitle,
+ description = description,
+ credentialTitle = title,
+ credentialSubtitle = null,
+ credentialDescription = null
+ ),
+ BiometricUserInfo(USER_ID),
+ BiometricOperationInfo(OPERATION_ID),
+ stealth
+ )
+ )
+
+ for (request in toCheck) {
+ assertThat(request.title).isEqualTo(title)
+ assertThat(request.subtitle).isEqualTo(subtitle)
+ assertThat(request.description).isEqualTo(description)
+ assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+ assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ if (request is BiometricPromptRequest.Credential.Pattern) {
+ assertThat(request.stealthMode).isEqualTo(stealth)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
new file mode 100644
index 000000000000..d73cdfc4249f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
@@ -0,0 +1,181 @@
+package com.android.systemui.biometrics.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.promptInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val USER_ID = 9
+private const val OPERATION_ID = 10L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class CredentialViewModelTest : SysuiTestCase() {
+
+ private val dispatcher = UnconfinedTestDispatcher()
+ private val promptRepository = FakePromptRepository()
+ private val credentialInteractor = FakeCredentialInteractor()
+
+ private lateinit var viewModel: CredentialViewModel
+
+ @Before
+ fun setup() {
+ viewModel =
+ CredentialViewModel(
+ mContext,
+ BiometricPromptCredentialInteractor(
+ dispatcher,
+ promptRepository,
+ credentialInteractor
+ )
+ )
+ }
+
+ @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true)
+ @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false)
+ @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false)
+
+ private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) =
+ runTestWithKind(type) {
+ var flags: Int? = null
+ val job = launch { viewModel.inputFlags.collect { flags = it } }
+
+ if (expectFlags) {
+ assertThat(flags).isNotNull()
+ } else {
+ assertThat(flags).isNull()
+ }
+ job.cancel()
+ }
+
+ @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false)
+ @Test
+ fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false)
+ @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true)
+
+ private fun isStealthMode(type: PromptKind, expectStealth: Boolean) =
+ runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) {
+ var stealth: Boolean? = null
+ val job = launch { viewModel.stealthMode.collect { stealth = it } }
+
+ assertThat(stealth).isEqualTo(expectStealth)
+
+ job.cancel()
+ }
+
+ @Test
+ fun animatesContents() = runTestWithKind {
+ val expected = arrayOf(true, false, true)
+ val animate = mutableListOf<Boolean>()
+ val job = launch { viewModel.animateContents.toList(animate) }
+
+ for (value in expected) {
+ viewModel.setAnimateContents(value)
+ viewModel.setAnimateContents(value)
+ }
+ assertThat(animate).containsExactly(*expected).inOrder()
+
+ job.cancel()
+ }
+
+ @Test
+ fun showAndClearErrors() = runTestWithKind {
+ var error = ""
+ val job = launch { viewModel.errorMessage.collect { error = it } }
+ assertThat(error).isEmpty()
+
+ viewModel.showPatternTooShortError()
+ assertThat(error).isNotEmpty()
+
+ viewModel.resetErrorMessage()
+ assertThat(error).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun checkCredential() = runTestWithKind {
+ val hat = ByteArray(2)
+ credentialInteractor.verifyCredentialResponse = { _ ->
+ flowOf(CredentialStatus.Success.Verified(hat))
+ }
+
+ val attestations = mutableListOf<ByteArray?>()
+ val remainingAttempts = mutableListOf<RemainingAttempts?>()
+ var header: HeaderViewModel? = null
+ val job = launch {
+ launch { viewModel.validatedAttestation.toList(attestations) }
+ launch { viewModel.remainingAttempts.toList(remainingAttempts) }
+ launch { viewModel.header.collect { header = it } }
+ }
+ assertThat(header).isNotNull()
+
+ viewModel.checkCredential("p", header!!)
+
+ val attestation = attestations.removeLastOrNull()
+ assertThat(attestation).isSameInstanceAs(hat)
+ assertThat(attestations).isEmpty()
+ assertThat(remainingAttempts).containsExactly(RemainingAttempts())
+
+ job.cancel()
+ }
+
+ @Test
+ fun checkCredentialWhenBad() = runTestWithKind {
+ val remaining = 2
+ val urgentError = "wow"
+ credentialInteractor.verifyCredentialResponse = { _ ->
+ flowOf(CredentialStatus.Fail.Error("error", remaining, urgentError))
+ }
+
+ val attestations = mutableListOf<ByteArray?>()
+ val remainingAttempts = mutableListOf<RemainingAttempts?>()
+ var header: HeaderViewModel? = null
+ val job = launch {
+ launch { viewModel.validatedAttestation.toList(attestations) }
+ launch { viewModel.remainingAttempts.toList(remainingAttempts) }
+ launch { viewModel.header.collect { header = it } }
+ }
+ assertThat(header).isNotNull()
+
+ viewModel.checkCredential("1111", header!!)
+
+ assertThat(attestations).containsExactly(null)
+
+ val attemptInfo = remainingAttempts.removeLastOrNull()
+ assertThat(attemptInfo).isNotNull()
+ assertThat(attemptInfo!!.remaining).isEqualTo(remaining)
+ assertThat(attemptInfo.message).isEqualTo(urgentError)
+ assertThat(remainingAttempts).containsExactly(RemainingAttempts()) // initial value
+
+ job.cancel()
+ }
+
+ private fun runTestWithKind(
+ kind: PromptKind = PromptKind.PIN,
+ init: () -> Unit = {},
+ block: suspend TestScope.() -> Unit,
+ ) =
+ runTest(dispatcher) {
+ init()
+ promptRepository.setPrompt(promptInfo(), USER_ID, OPERATION_ID, kind)
+ block()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
index e099c9269d3f..ea16cb567028 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
@@ -20,6 +20,7 @@ import static com.android.systemui.dreams.complication.Complication.COMPLICATION
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER;
@@ -63,6 +64,8 @@ public class ComplicationUtilsTest extends SysuiTestCase {
.isEqualTo(COMPLICATION_TYPE_HOME_CONTROLS);
assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_SMARTSPACE))
.isEqualTo(COMPLICATION_TYPE_SMARTSPACE);
+ assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY))
+ .isEqualTo(COMPLICATION_TYPE_MEDIA_ENTRY);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
index 50f27ea27ae9..0295030da510 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
@@ -16,8 +16,11 @@
package com.android.systemui.dreams.complication;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,6 +35,7 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.ui.MediaCarouselController;
import com.android.systemui.media.dream.MediaDreamComplication;
@@ -51,6 +55,9 @@ import org.mockito.MockitoAnnotations;
@TestableLooper.RunWithLooper
public class DreamMediaEntryComplicationTest extends SysuiTestCase {
@Mock
+ private DreamMediaEntryComplicationComponent.Factory mComponentFactory;
+
+ @Mock
private View mView;
@Mock
@@ -89,6 +96,14 @@ public class DreamMediaEntryComplicationTest extends SysuiTestCase {
when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false);
}
+ @Test
+ public void testGetRequiredTypeAvailability() {
+ final DreamMediaEntryComplication complication =
+ new DreamMediaEntryComplication(mComponentFactory);
+ assertThat(complication.getRequiredTypeAvailability()).isEqualTo(
+ COMPLICATION_TYPE_MEDIA_ENTRY);
+ }
+
/**
* Ensures clicking media entry chip adds/removes media complication.
*/
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
deleted file mode 100644
index 27a5190367a8..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ /dev/null
@@ -1,478 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.keyguard;
-
-import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.keyguard.LockIconView.ICON_LOCK;
-import static com.android.keyguard.LockIconView.ICON_UNLOCK;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.AnimatedStateListDrawable;
-import android.hardware.biometrics.BiometricSourceType;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.util.Pair;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.KeyguardViewController;
-import com.android.keyguard.LockIconView;
-import com.android.keyguard.LockIconViewController;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.AuthRippleController;
-import com.android.systemui.doze.util.BurnInHelperKt;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class LockIconViewControllerTest extends SysuiTestCase {
- private static final String UNLOCKED_LABEL = "unlocked";
- private static final int PADDING = 10;
-
- private MockitoSession mStaticMockSession;
-
- private @Mock LockIconView mLockIconView;
- private @Mock AnimatedStateListDrawable mIconDrawable;
- private @Mock Context mContext;
- private @Mock Resources mResources;
- private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
- private @Mock StatusBarStateController mStatusBarStateController;
- private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
- private @Mock KeyguardViewController mKeyguardViewController;
- private @Mock KeyguardStateController mKeyguardStateController;
- private @Mock FalsingManager mFalsingManager;
- private @Mock AuthController mAuthController;
- private @Mock DumpManager mDumpManager;
- private @Mock AccessibilityManager mAccessibilityManager;
- private @Mock ConfigurationController mConfigurationController;
- private @Mock VibratorHelper mVibrator;
- private @Mock AuthRippleController mAuthRippleController;
- private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
-
- private LockIconViewController mLockIconViewController;
-
- // Capture listeners so that they can be used to send events
- @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
- ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
- private View.OnAttachStateChangeListener mAttachListener;
-
- @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
- ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
- private KeyguardStateController.Callback mKeyguardStateCallback;
-
- @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
- ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
- private StatusBarStateController.StateListener mStatusBarStateListener;
-
- @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
- private AuthController.Callback mAuthControllerCallback;
-
- @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback>
- mKeyguardUpdateMonitorCallbackCaptor =
- ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
- private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
-
- @Captor private ArgumentCaptor<Point> mPointCaptor;
-
- @Before
- public void setUp() throws Exception {
- mStaticMockSession = mockitoSession()
- .mockStatic(BurnInHelperKt.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
- MockitoAnnotations.initMocks(this);
-
- setupLockIconViewMocks();
- when(mContext.getResources()).thenReturn(mResources);
- when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
- Rect windowBounds = new Rect(0, 0, 800, 1200);
- when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
- when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
- when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
- when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
- when(mAuthController.getScaleFactor()).thenReturn(1f);
-
- when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
- when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-
- mLockIconViewController = new LockIconViewController(
- mLockIconView,
- mStatusBarStateController,
- mKeyguardUpdateMonitor,
- mKeyguardViewController,
- mKeyguardStateController,
- mFalsingManager,
- mAuthController,
- mDumpManager,
- mAccessibilityManager,
- mConfigurationController,
- mDelayableExecutor,
- mVibrator,
- mAuthRippleController,
- mResources
- );
- }
-
- @After
- public void tearDown() {
- mStaticMockSession.finishMocking();
- }
-
- @Test
- public void testUpdateFingerprintLocationOnInit() {
- // GIVEN fp sensor location is available pre-attached
- Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
-
- // WHEN lock icon view controller is initialized and attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN lock icon view location is updated to the udfps location with UDFPS radius
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING));
- }
-
- @Test
- public void testUpdatePaddingBasedOnResolutionScale() {
- // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
- Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
- when(mAuthController.getScaleFactor()).thenReturn(5f);
-
- // WHEN lock icon view controller is initialized and attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN lock icon view location is updated with the scaled radius
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING * 5));
- }
-
- @Test
- public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
- // GIVEN fp sensor location is not available pre-init
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
- when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- resetLockIconView(); // reset any method call counts for when we verify method calls later
-
- // GIVEN fp sensor location is available post-attached
- captureAuthControllerCallback();
- Pair<Float, Point> udfps = setupUdfps();
-
- // WHEN all authenticators are registered
- mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT);
- mDelayableExecutor.runAllReady();
-
- // THEN lock icon view location is updated with the same coordinates as auth controller vals
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING));
- }
-
- @Test
- public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
- // GIVEN fp sensor location is not available pre-init
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
- when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- resetLockIconView(); // reset any method call counts for when we verify method calls later
-
- // GIVEN fp sensor location is available post-attached
- captureAuthControllerCallback();
- Pair<Float, Point> udfps = setupUdfps();
-
- // WHEN udfps location changes
- mAuthControllerCallback.onUdfpsLocationChanged();
- mDelayableExecutor.runAllReady();
-
- // THEN lock icon view location is updated with the same coordinates as auth controller vals
- verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
- eq(PADDING));
- }
-
- @Test
- public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
- // GIVEN Udpfs sensor location is available
- setupUdfps();
-
- mLockIconViewController.init();
- captureAttachListener();
-
- // WHEN the view is attached
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN the lock icon view background should be enabled
- verify(mLockIconView).setUseBackground(true);
- }
-
- @Test
- public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
- // GIVEN Udfps sensor location is not supported
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-
- mLockIconViewController.init();
- captureAttachListener();
-
- // WHEN the view is attached
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN the lock icon view background should be disabled
- verify(mLockIconView).setUseBackground(false);
- }
-
- @Test
- public void testUnlockIconShows_biometricUnlockedTrue() {
- // GIVEN UDFPS sensor location is available
- setupUdfps();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureKeyguardUpdateMonitorCallback();
-
- // GIVEN user has unlocked with a biometric auth (ie: face auth)
- when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
- reset(mLockIconView);
-
- // WHEN face auth's biometric running state changes
- mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
- BiometricSourceType.FACE);
-
- // THEN the unlock icon is shown
- verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
- }
-
- @Test
- public void testLockIconStartState() {
- // GIVEN lock icon state
- setupShowLockIcon();
-
- // WHEN lock icon controller is initialized
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
-
- // THEN the lock icon should show
- verify(mLockIconView).updateIcon(ICON_LOCK, false);
- }
-
- @Test
- public void testLockIcon_updateToUnlock() {
- // GIVEN starting state for the lock icon
- setupShowLockIcon();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureKeyguardStateCallback();
- reset(mLockIconView);
-
- // WHEN the unlocked state changes to canDismissLockScreen=true
- when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
- mKeyguardStateCallback.onUnlockedChanged();
-
- // THEN the unlock should show
- verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
- }
-
- @Test
- public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
- // GIVEN udfps not enrolled
- setupUdfps();
- when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
-
- // GIVEN starting state for the lock icon
- setupShowLockIcon();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureStatusBarStateListener();
- reset(mLockIconView);
-
- // WHEN the dozing state changes
- mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
- // THEN the icon is cleared
- verify(mLockIconView).clearIcon();
- }
-
- @Test
- public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
- // GIVEN udfps enrolled
- setupUdfps();
- when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
- // GIVEN starting state for the lock icon
- setupShowLockIcon();
-
- // GIVEN lock icon controller is initialized and view is attached
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureStatusBarStateListener();
- reset(mLockIconView);
-
- // WHEN the dozing state changes
- mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
- // THEN the AOD lock icon should show
- verify(mLockIconView).updateIcon(ICON_LOCK, true);
- }
-
- @Test
- public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
- // GIVEN udfps enrolled
- setupUdfps();
- when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
- // GIVEN burn-in offset = 5
- int burnInOffset = 5;
- when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
-
- // GIVEN starting state for the lock icon (keyguard)
- setupShowLockIcon();
- mLockIconViewController.init();
- captureAttachListener();
- mAttachListener.onViewAttachedToWindow(mLockIconView);
- captureStatusBarStateListener();
- reset(mLockIconView);
-
- // WHEN dozing updates
- mStatusBarStateListener.onDozingChanged(true /* isDozing */);
- mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
-
- // THEN the view's translation is updated to use the AoD burn-in offsets
- verify(mLockIconView).setTranslationY(burnInOffset);
- verify(mLockIconView).setTranslationX(burnInOffset);
- reset(mLockIconView);
-
- // WHEN the device is no longer dozing
- mStatusBarStateListener.onDozingChanged(false /* isDozing */);
- mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
-
- // THEN the view is updated to NO translation (no burn-in offsets anymore)
- verify(mLockIconView).setTranslationY(0);
- verify(mLockIconView).setTranslationX(0);
-
- }
- private Pair<Float, Point> setupUdfps() {
- when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
- final Point udfpsLocation = new Point(50, 75);
- final float radius = 33f;
- when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
- when(mAuthController.getUdfpsRadius()).thenReturn(radius);
-
- return new Pair(radius, udfpsLocation);
- }
-
- private void setupShowLockIcon() {
- when(mKeyguardStateController.isShowing()).thenReturn(true);
- when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
- when(mStatusBarStateController.isDozing()).thenReturn(false);
- when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
- when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
- when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
- }
-
- private void captureAuthControllerCallback() {
- verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
- mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
- }
-
- private void captureAttachListener() {
- verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
- mAttachListener = mAttachCaptor.getValue();
- }
-
- private void captureKeyguardStateCallback() {
- verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
- mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
- }
-
- private void captureStatusBarStateListener() {
- verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
- mStatusBarStateListener = mStatusBarStateCaptor.getValue();
- }
-
- private void captureKeyguardUpdateMonitorCallback() {
- verify(mKeyguardUpdateMonitor).registerCallback(
- mKeyguardUpdateMonitorCallbackCaptor.capture());
- mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
- }
-
- private void setupLockIconViewMocks() {
- when(mLockIconView.getResources()).thenReturn(mResources);
- when(mLockIconView.getContext()).thenReturn(mContext);
- }
-
- private void resetLockIconView() {
- reset(mLockIconView);
- setupLockIconViewMocks();
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
index e99c139e9e7e..ce110084dbc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
@@ -15,10 +15,10 @@
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import com.android.systemui.animation.Expandable
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.yield
@@ -29,7 +29,9 @@ import kotlinx.coroutines.yield
* This class is abstract to force tests to provide extensions of it as the system that references
* these configs uses each implementation's class type to refer to them.
*/
-abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig {
+abstract class FakeKeyguardQuickAffordanceConfig(
+ override val key: String,
+) : KeyguardQuickAffordanceConfig {
var onClickedResult: OnClickedResult = OnClickedResult.Handled
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 9a91ea91f3a2..b120303d4c04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import androidx.test.filters.SmallTest
import com.android.systemui.R
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
index a809f0547ee6..ce8d36d5012a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import androidx.test.filters.SmallTest
import com.android.systemui.R
@@ -23,7 +23,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.controls.dagger.ControlsComponent
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 329c4db0a75c..93464400d1ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -1,26 +1,26 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import android.content.Intent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index 98dc4c4f6f76..ae9e3c7a6f04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -1,21 +1,21 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
-package com.android.systemui.keyguard.domain.quickaffordance
+package com.android.systemui.keyguard.data.quickaffordance
import android.graphics.drawable.Drawable
import android.service.quickaccesswallet.GetWalletCardsResponse
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index b4d5464d1177..114cf19d0837 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -25,11 +25,12 @@ import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -211,7 +212,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
whenever(expandable.activityLaunchController()).thenReturn(animationController)
- homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
+ homeControls =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+ ) {}
underTest =
KeyguardQuickAffordanceInteractor(
keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
@@ -224,8 +229,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
),
KeyguardQuickAffordancePosition.BOTTOM_END to
listOf(
- object : FakeKeyguardQuickAffordanceConfig() {},
- object : FakeKeyguardQuickAffordanceConfig() {},
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+ ) {},
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+ ) {},
),
),
),
@@ -260,7 +271,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
}
underTest.onQuickAffordanceClicked(
- configKey = homeControls::class,
+ configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS,
expandable = expandable,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 65fd6e576650..1a1ee8aca099 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -22,12 +22,13 @@ import com.android.internal.widget.LockPatternUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
@@ -69,9 +70,21 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
repository = FakeKeyguardRepository()
repository.setKeyguardShowing(true)
- homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
- quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {}
- qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {}
+ homeControls =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+ ) {}
+ quickAccessWallet =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+ ) {}
+ qrCodeScanner =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+ ) {}
underTest =
KeyguardQuickAffordanceInteractor(
@@ -99,7 +112,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
@Test
fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest {
- val configKey = homeControls::class
+ val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
homeControls.setState(
KeyguardQuickAffordanceConfig.State.Visible(
icon = ICON,
@@ -130,7 +143,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
@Test
fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest {
- val configKey = quickAccessWallet::class
+ val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
quickAccessWallet.setState(
KeyguardQuickAffordanceConfig.State.Visible(
icon = ICON,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt
index e68c43f4abd7..13e2768e1fd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt
@@ -17,8 +17,8 @@
package com.android.systemui.keyguard.domain.quickaffordance
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import kotlin.reflect.KClass
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
/** Fake implementation of [FakeKeyguardQuickAffordanceRegistry], for tests. */
class FakeKeyguardQuickAffordanceRegistry(
@@ -33,11 +33,8 @@ class FakeKeyguardQuickAffordanceRegistry(
}
override fun get(
- configClass: KClass<out FakeKeyguardQuickAffordanceConfig>
+ key: String,
): FakeKeyguardQuickAffordanceConfig {
- return configsByPosition.values
- .flatten()
- .associateBy { config -> config::class }
- .getValue(configClass)
+ return configsByPosition.values.flatten().associateBy { config -> config.key }.getValue(key)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index d674c89c0e14..f9be067362d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -23,14 +23,15 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
-import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition
-import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
-import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
@@ -40,7 +41,6 @@ import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlin.math.max
import kotlin.math.min
-import kotlin.reflect.KClass
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.runBlockingTest
@@ -81,9 +81,21 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
.thenReturn(RETURNED_BURN_IN_OFFSET)
- homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
- quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
- qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
+ homeControlsQuickAffordanceConfig =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
+ ) {}
+ quickAccessWalletAffordanceConfig =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+ ) {}
+ qrCodeScannerAffordanceConfig =
+ object :
+ FakeKeyguardQuickAffordanceConfig(
+ BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
+ ) {}
registry =
FakeKeyguardQuickAffordanceRegistry(
mapOf(
@@ -489,7 +501,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
private suspend fun setUpQuickAffordanceModel(
position: KeyguardQuickAffordancePosition,
testConfig: TestConfig,
- ): KClass<out FakeKeyguardQuickAffordanceConfig> {
+ ): String {
val config =
when (position) {
KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
@@ -518,13 +530,13 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
KeyguardQuickAffordanceConfig.State.Hidden
}
config.setState(state)
- return config::class
+ return config.key
}
private fun assertQuickAffordanceViewModel(
viewModel: KeyguardQuickAffordanceViewModel?,
testConfig: TestConfig,
- configKey: KClass<out FakeKeyguardQuickAffordanceConfig>,
+ configKey: String,
) {
checkNotNull(viewModel)
assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
new file mode 100644
index 000000000000..f20c6a29b840
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.Intent
+import android.os.UserManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.view.KeyEvent
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.floating.FloatingTasks
+import java.util.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskController].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskControllerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskControllerTest : SysuiTestCase() {
+
+ private val notesIntent = Intent(NOTES_ACTION)
+
+ @Mock lateinit var context: Context
+ @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+ @Mock lateinit var floatingTasks: FloatingTasks
+ @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks>
+ @Mock lateinit var keyguardManager: KeyguardManager
+ @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
+ @Mock lateinit var optionalUserManager: Optional<UserManager>
+ @Mock lateinit var userManager: UserManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+ whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks)
+ whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
+ whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
+ whenever(userManager.isUserUnlocked).thenReturn(true)
+ }
+
+ private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
+ return NoteTaskController(
+ context = context,
+ intentResolver = noteTaskIntentResolver,
+ optionalFloatingTasks = optionalFloatingTasks,
+ optionalKeyguardManager = optionalKeyguardManager,
+ optionalUserManager = optionalUserManager,
+ isEnabled = isEnabled,
+ )
+ }
+
+ @Test
+ fun handleSystemKey_keyguardIsLocked_shouldStartActivity() {
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_keyguardIsUnlocked_shouldStartFloatingTask() {
+ whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(floatingTasks).showOrSetStashed(notesIntent)
+ verify(context, never()).startActivity(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_floatingTasksIsNull_shouldDoNothing() {
+ whenever(optionalFloatingTasks.orElse(null)).thenReturn(null)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() {
+ whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_userManagerIsNull_shouldDoNothing() {
+ whenever(optionalUserManager.orElse(null)).thenReturn(null)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() {
+ whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_flagDisabled_shouldDoNothing() {
+ createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+
+ @Test
+ fun handleSystemKey_userIsLocked_shouldDoNothing() {
+ whenever(userManager.isUserUnlocked).thenReturn(false)
+
+ createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+ verify(context, never()).startActivity(notesIntent)
+ verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
new file mode 100644
index 000000000000..f344c8d9eec4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.floating.FloatingTasks
+import java.util.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskController].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInitializerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInitializerTest : SysuiTestCase() {
+
+ @Mock lateinit var commandQueue: CommandQueue
+ @Mock lateinit var floatingTasks: FloatingTasks
+ @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(optionalFloatingTasks.isPresent).thenReturn(true)
+ whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks)
+ }
+
+ private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+ return NoteTaskInitializer(
+ optionalFloatingTasks = optionalFloatingTasks,
+ lazyNoteTaskController = mock(),
+ commandQueue = commandQueue,
+ isEnabled = isEnabled,
+ )
+ }
+
+ @Test
+ fun initialize_shouldAddCallbacks() {
+ createNoteTaskInitializer().initialize()
+
+ verify(commandQueue).addCallback(any())
+ }
+
+ @Test
+ fun initialize_flagDisabled_shouldDoNothing() {
+ createNoteTaskInitializer(isEnabled = false).initialize()
+
+ verify(commandQueue, never()).addCallback(any())
+ }
+
+ @Test
+ fun initialize_floatingTasksNotPresent_shouldDoNothing() {
+ whenever(optionalFloatingTasks.isPresent).thenReturn(false)
+
+ createNoteTaskInitializer().initialize()
+
+ verify(commandQueue, never()).addCallback(any())
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
new file mode 100644
index 000000000000..dd2cc2ffc9db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notetask
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskIntentResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskIntentResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskIntentResolverTest : SysuiTestCase() {
+
+ @Mock lateinit var packageManager: PackageManager
+
+ private lateinit var resolver: NoteTaskIntentResolver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ resolver = NoteTaskIntentResolver(packageManager)
+ }
+
+ private fun createResolveInfo(
+ packageName: String = "PackageName",
+ activityInfo: ActivityInfo? = null,
+ ): ResolveInfo {
+ return ResolveInfo().apply {
+ serviceInfo =
+ ServiceInfo().apply {
+ applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
+ }
+ this.activityInfo = activityInfo
+ }
+ }
+
+ private fun createActivityInfo(
+ name: String? = "ActivityName",
+ exported: Boolean = true,
+ enabled: Boolean = true,
+ showWhenLocked: Boolean = true,
+ turnScreenOn: Boolean = true,
+ ): ActivityInfo {
+ return ActivityInfo().apply {
+ this.name = name
+ this.exported = exported
+ this.enabled = enabled
+ if (showWhenLocked) {
+ flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED
+ }
+ if (turnScreenOn) {
+ flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
+ }
+ }
+ }
+
+ private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) {
+ whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>()))
+ .thenReturn(block())
+ }
+
+ private fun givenResolveActivity(block: () -> ResolveInfo?) {
+ whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block())
+ }
+
+ @Test
+ fun resolveIntent_shouldReturnNotesIntent() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) }
+
+ val actual = resolver.resolveIntent()
+
+ val expected =
+ Intent(NOTES_ACTION)
+ .setComponent(ComponentName("PackageName", "ActivityName"))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ // Compares the string representation of both intents, as they are different instances.
+ assertThat(actual.toString()).isEqualTo(expected.toString())
+ }
+
+ @Test
+ fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity {
+ createResolveInfo(activityInfo = createActivityInfo(enabled = false))
+ }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity {
+ createResolveInfo(activityInfo = createActivityInfo(exported = false))
+ }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity {
+ createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false))
+ }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity {
+ createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false))
+ }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityInfoIsNull_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity { createResolveInfo(activityInfo = null) }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_resolveActivityIsNull_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo()) }
+ givenResolveActivity { null }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
+ givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+
+ @Test
+ fun resolveIntent_activityNotFoundForAction_shouldReturnNull() {
+ givenQueryIntentActivities { emptyList() }
+
+ val actual = resolver.resolveIntent()
+
+ assertThat(actual).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 91aecd8cf753..dceb4ff48125 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -78,6 +78,7 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
import org.junit.Assert;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -89,6 +90,7 @@ import org.mockito.junit.MockitoRule;
/**
* Tests for {@link NotificationStackScrollLayout}.
*/
+@Ignore("b/255552856")
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index e18dd3a3c846..7d5f06c890c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -140,6 +140,40 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
}
@Test
+ fun testOnUnfold_hingeAngleDecreasesBeforeInnerScreenAvailable_emitsOnlyStartAndInnerScreenAvailableEvents() {
+ setFoldState(folded = true)
+ foldUpdates.clear()
+
+ setFoldState(folded = false)
+ screenOnStatusProvider.notifyScreenTurningOn()
+ sendHingeAngleEvent(10)
+ sendHingeAngleEvent(20)
+ sendHingeAngleEvent(10)
+ screenOnStatusProvider.notifyScreenTurnedOn()
+
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
+ FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
+ }
+
+ @Test
+ fun testOnUnfold_hingeAngleDecreasesAfterInnerScreenAvailable_emitsStartInnerScreenAvailableAndStartClosingEvents() {
+ setFoldState(folded = true)
+ foldUpdates.clear()
+
+ setFoldState(folded = false)
+ screenOnStatusProvider.notifyScreenTurningOn()
+ sendHingeAngleEvent(10)
+ sendHingeAngleEvent(20)
+ screenOnStatusProvider.notifyScreenTurnedOn()
+ sendHingeAngleEvent(30)
+ sendHingeAngleEvent(40)
+ sendHingeAngleEvent(10)
+
+ assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
+ FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_START_CLOSING)
+ }
+
+ @Test
fun testOnFolded_stopsHingeAngleProvider() {
setFoldState(folded = true)
@@ -237,7 +271,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
}
@Test
- fun startClosingEvent_afterTimeout_abortEmitted() {
+ fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() {
sendHingeAngleEvent(90)
sendHingeAngleEvent(80)
@@ -269,7 +303,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
}
@Test
- fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() {
+ fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() {
sendHingeAngleEvent(180)
sendHingeAngleEvent(90)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java
index 76bff1d72141..7e8ffeb7f9e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java
@@ -54,7 +54,7 @@ import java.util.concurrent.Executor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
-public class WallpaperColorExtractorTest extends SysuiTestCase {
+public class WallpaperLocalColorExtractorTest extends SysuiTestCase {
private static final int LOW_BMP_WIDTH = 128;
private static final int LOW_BMP_HEIGHT = 128;
private static final int HIGH_BMP_WIDTH = 3000;
@@ -105,11 +105,11 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
return bitmap;
}
- private WallpaperColorExtractor getSpyWallpaperColorExtractor() {
+ private WallpaperLocalColorExtractor getSpyWallpaperLocalColorExtractor() {
- WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor(
+ WallpaperLocalColorExtractor colorExtractor = new WallpaperLocalColorExtractor(
mBackgroundExecutor,
- new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+ new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() {
@Override
public void onColorsProcessed(List<RectF> regions,
List<WallpaperColors> colors) {
@@ -132,25 +132,25 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
mDeactivatedCount++;
}
});
- WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor);
+ WallpaperLocalColorExtractor spyColorExtractor = spy(colorExtractor);
doAnswer(invocation -> {
mMiniBitmapWidth = invocation.getArgument(1);
mMiniBitmapHeight = invocation.getArgument(2);
return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight);
- }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
+ }).when(spyColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
doAnswer(invocation -> getMockBitmap(
invocation.getArgument(1),
invocation.getArgument(2)))
- .when(spyWallpaperColorExtractor)
+ .when(spyColorExtractor)
.createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)))
- .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class));
+ .when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class));
- return spyWallpaperColorExtractor;
+ return spyColorExtractor;
}
private RectF randomArea() {
@@ -180,18 +180,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
*/
@Test
public void testMiniBitmapCreation() {
- WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+ WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH);
int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT);
Bitmap bitmap = getMockBitmap(width, height);
- spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+ spyColorExtractor.onBitmapChanged(bitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight))
- .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
+ .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE);
}
}
@@ -201,18 +201,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
*/
@Test
public void testSmallMiniBitmapCreation() {
- WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+ WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH);
int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT);
Bitmap bitmap = getMockBitmap(width, height);
- spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+ spyColorExtractor.onBitmapChanged(bitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight))
- .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
+ .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE);
}
}
@@ -228,15 +228,15 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
- WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+ WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS);
int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
List<Runnable> tasks = Arrays.asList(
- () -> spyWallpaperColorExtractor.onPageChanged(nPages),
- () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
- () -> spyWallpaperColorExtractor.setDisplayDimensions(
+ () -> spyColorExtractor.onPageChanged(nPages),
+ () -> spyColorExtractor.onBitmapChanged(bitmap),
+ () -> spyColorExtractor.setDisplayDimensions(
DISPLAY_WIDTH, DISPLAY_HEIGHT),
- () -> spyWallpaperColorExtractor.addLocalColorsAreas(
+ () -> spyColorExtractor.addLocalColorsAreas(
regions));
Collections.shuffle(tasks);
tasks.forEach(Runnable::run);
@@ -245,7 +245,7 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(mColorsProcessed).isEqualTo(regions.size());
- spyWallpaperColorExtractor.removeLocalColorAreas(regions);
+ spyColorExtractor.removeLocalColorAreas(regions);
assertThat(mDeactivatedCount).isEqualTo(1);
}
}
@@ -260,7 +260,7 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
int nSimulations = 10;
for (int i = 0; i < nSimulations; i++) {
resetCounters();
- WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+ WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions = new ArrayList<>();
@@ -268,20 +268,20 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
regions.addAll(regions2);
int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
List<Runnable> tasks = Arrays.asList(
- () -> spyWallpaperColorExtractor.onPageChanged(nPages),
- () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
- () -> spyWallpaperColorExtractor.setDisplayDimensions(
+ () -> spyColorExtractor.onPageChanged(nPages),
+ () -> spyColorExtractor.onBitmapChanged(bitmap),
+ () -> spyColorExtractor.setDisplayDimensions(
DISPLAY_WIDTH, DISPLAY_HEIGHT),
- () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1));
+ () -> spyColorExtractor.removeLocalColorAreas(regions1));
- spyWallpaperColorExtractor.addLocalColorsAreas(regions);
+ spyColorExtractor.addLocalColorsAreas(regions);
assertThat(mActivatedCount).isEqualTo(1);
Collections.shuffle(tasks);
tasks.forEach(Runnable::run);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
assertThat(mDeactivatedCount).isEqualTo(0);
- spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
+ spyColorExtractor.removeLocalColorAreas(regions2);
assertThat(mDeactivatedCount).isEqualTo(1);
}
}
@@ -295,18 +295,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
@Test
public void testRecomputeColorExtraction() {
Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
- WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+ WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
List<RectF> regions = new ArrayList<>();
regions.addAll(regions1);
regions.addAll(regions2);
- spyWallpaperColorExtractor.addLocalColorsAreas(regions);
+ spyColorExtractor.addLocalColorsAreas(regions);
assertThat(mActivatedCount).isEqualTo(1);
int nPages = PAGES_LOW;
- spyWallpaperColorExtractor.onBitmapChanged(bitmap);
- spyWallpaperColorExtractor.onPageChanged(nPages);
- spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
+ spyColorExtractor.onBitmapChanged(bitmap);
+ spyColorExtractor.onPageChanged(nPages);
+ spyColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
int nSimulations = 20;
for (int i = 0; i < nSimulations; i++) {
@@ -315,22 +315,22 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
// verify that if we remove some regions, they are not recomputed after other changes
if (i == nSimulations / 2) {
regions.removeAll(regions2);
- spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
+ spyColorExtractor.removeLocalColorAreas(regions2);
}
if (Math.random() >= 0.5) {
int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH);
if (nPagesNew == nPages) continue;
nPages = nPagesNew;
- spyWallpaperColorExtractor.onPageChanged(nPagesNew);
+ spyColorExtractor.onPageChanged(nPagesNew);
} else {
Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
- spyWallpaperColorExtractor.onBitmapChanged(newBitmap);
+ spyColorExtractor.onBitmapChanged(newBitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
}
assertThat(mColorsProcessed).isEqualTo(regions.size());
}
- spyWallpaperColorExtractor.removeLocalColorAreas(regions);
+ spyColorExtractor.removeLocalColorAreas(regions);
assertThat(mDeactivatedCount).isEqualTo(1);
}
@@ -339,12 +339,12 @@ public class WallpaperColorExtractorTest extends SysuiTestCase {
resetCounters();
Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
doNothing().when(bitmap).recycle();
- WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
- spyWallpaperColorExtractor.onPageChanged(PAGES_LOW);
- spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+ WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+ spyColorExtractor.onPageChanged(PAGES_LOW);
+ spyColorExtractor.onBitmapChanged(bitmap);
assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
- spyWallpaperColorExtractor.cleanUp();
- spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
+ spyColorExtractor.cleanUp();
+ spyColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
assertThat(mColorsProcessed).isEqualTo(0);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 7af66f641837..7ae47b41d5ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -28,6 +28,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.notetask.NoteTaskInitializer;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -36,7 +37,6 @@ import com.android.systemui.tracing.ProtoTracer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.floating.FloatingTasks;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEventCallback;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -78,18 +78,31 @@ public class WMShellTest extends SysuiTestCase {
@Mock ProtoTracer mProtoTracer;
@Mock UserTracker mUserTracker;
@Mock ShellExecutor mSysUiMainExecutor;
- @Mock FloatingTasks mFloatingTasks;
+ @Mock NoteTaskInitializer mNoteTaskInitializer;
@Mock DesktopMode mDesktopMode;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip),
- Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks),
+ mWMShell = new WMShell(
+ mContext,
+ mShellInterface,
+ Optional.of(mPip),
+ Optional.of(mSplitScreen),
+ Optional.of(mOneHanded),
Optional.of(mDesktopMode),
- mCommandQueue, mConfigurationController, mKeyguardStateController,
- mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
- mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
+ mCommandQueue,
+ mConfigurationController,
+ mKeyguardStateController,
+ mKeyguardUpdateMonitor,
+ mScreenLifecycle,
+ mSysUiState,
+ mProtoTracer,
+ mWakefulnessLifecycle,
+ mUserTracker,
+ mNoteTaskInitializer,
+ mSysUiMainExecutor
+ );
}
@Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
new file mode 100644
index 000000000000..96658c61109d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
@@ -0,0 +1,48 @@
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.PromptInfo
+import com.android.systemui.biometrics.data.model.PromptKind
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Fake implementation of [PromptRepository] for tests. */
+class FakePromptRepository : PromptRepository {
+
+ private val _isShowing = MutableStateFlow(false)
+ override val isShowing = _isShowing.asStateFlow()
+
+ private val _promptInfo = MutableStateFlow<PromptInfo?>(null)
+ override val promptInfo = _promptInfo.asStateFlow()
+
+ private val _userId = MutableStateFlow<Int?>(null)
+ override val userId = _userId.asStateFlow()
+
+ private var _challenge = MutableStateFlow<Long?>(null)
+ override val challenge = _challenge.asStateFlow()
+
+ private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+ override val kind = _kind.asStateFlow()
+
+ override fun setPrompt(
+ promptInfo: PromptInfo,
+ userId: Int,
+ gatekeeperChallenge: Long?,
+ kind: PromptKind
+ ) {
+ _promptInfo.value = promptInfo
+ _userId.value = userId
+ _challenge.value = gatekeeperChallenge
+ _kind.value = kind
+ }
+
+ override fun unsetPrompt() {
+ _promptInfo.value = null
+ _userId.value = null
+ _challenge.value = null
+ _kind.value = PromptKind.ANY_BIOMETRIC
+ }
+
+ fun setIsShowing(showing: Boolean) {
+ _isShowing.value = showing
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt
new file mode 100644
index 000000000000..fbe291ebaf5d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt
@@ -0,0 +1,31 @@
+package com.android.systemui.biometrics.domain.interactor
+
+import com.android.internal.widget.LockscreenCredential
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Fake implementation of [CredentialInteractor] for tests. */
+class FakeCredentialInteractor : CredentialInteractor {
+
+ /** Sets return value for [isStealthModeActive]. */
+ var stealthMode: Boolean = false
+
+ /** Sets return value for [getCredentialOwnerOrSelfId]. */
+ var credentialOwnerId: Int? = null
+
+ override fun isStealthModeActive(userId: Int): Boolean = stealthMode
+
+ override fun getCredentialOwnerOrSelfId(userId: Int): Int = credentialOwnerId ?: userId
+
+ override fun verifyCredential(
+ request: BiometricPromptRequest.Credential,
+ credential: LockscreenCredential,
+ ): Flow<CredentialStatus> = verifyCredentialResponse(credential)
+
+ /** Sets the result value for [verifyCredential]. */
+ var verifyCredentialResponse: (credential: LockscreenCredential) -> Flow<CredentialStatus> =
+ { _ ->
+ flowOf(CredentialStatus.Fail.Error("invalid"))
+ }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index 043aff659d6c..b56818693124 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui.unfold.progress
+import android.os.Trace
import android.util.Log
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -117,6 +118,7 @@ class PhysicsBasedUnfoldTransitionProgressProvider(
if (DEBUG) {
Log.d(TAG, "onFoldUpdate = $update")
+ Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update)
}
}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 07473b30dd58..808128d16b7e 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -16,6 +16,7 @@
package com.android.systemui.unfold.updates
import android.os.Handler
+import android.os.Trace
import android.util.Log
import androidx.annotation.FloatRange
import androidx.annotation.VisibleForTesting
@@ -108,6 +109,7 @@ constructor(
private fun onHingeAngle(angle: Float) {
if (DEBUG) {
Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle")
+ Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt())
}
val isClosing = angle < lastHingeAngle
@@ -115,8 +117,16 @@ constructor(
val closingThresholdMet = closingThreshold == null || angle < closingThreshold
val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
-
- if (isClosing && closingThresholdMet && !closingEventDispatched && !isFullyOpened) {
+ val screenAvailableEventSent = isUnfoldHandled
+
+ if (isClosing // hinge angle should be decreasing since last update
+ && closingThresholdMet // hinge angle is below certain threshold
+ && !closingEventDispatched // we haven't sent closing event already
+ && !isFullyOpened // do not send closing event if we are in fully opened hinge
+ // angle range as closing threshold could overlap this range
+ && screenAvailableEventSent // do not send closing event if we are still in
+ // the process of turning on the inner display
+ ) {
notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 1df382fbd76f..f35de17088d1 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -176,6 +176,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
private boolean mSendMotionEvents;
+ private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0);
boolean mRequestFilterKeyEvents;
boolean mRetrieveInteractiveWindows;
@@ -2369,9 +2370,17 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ
}
public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+ mServiceDetectsGestures.put(displayId, mode);
mSystemSupport.setServiceDetectsGesturesEnabled(displayId, mode);
}
+ public boolean isServiceDetectsGesturesEnabled(int displayId) {
+ if (mServiceDetectsGestures.contains(displayId)) {
+ return mServiceDetectsGestures.get(displayId);
+ }
+ return false;
+ }
+
public void requestTouchExploration(int displayId) {
mSystemSupport.requestTouchExploration(displayId);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 75724bffabf8..d80117d8d8ba 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -176,6 +176,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
private int mEnabledFeatures;
+ // Display-specific features
+ private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>();
private final SparseArray<EventStreamState> mMouseStreamStates = new SparseArray<>(0);
private final SparseArray<EventStreamState> mTouchScreenStreamStates = new SparseArray<>(0);
@@ -458,7 +460,9 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
final Context displayContext = mContext.createDisplayContext(display);
final int displayId = display.getDisplayId();
-
+ if (!mServiceDetectsGestures.contains(displayId)) {
+ mServiceDetectsGestures.put(displayId, false);
+ }
if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
if (mAutoclickController == null) {
mAutoclickController = new AutoclickController(
@@ -481,6 +485,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) {
explorer.setSendMotionEventsEnabled(true);
}
+ explorer.setServiceDetectsGestures(mServiceDetectsGestures.get(displayId));
addFirstEventHandler(displayId, explorer);
mTouchExplorer.put(displayId, explorer);
}
@@ -897,6 +902,11 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo
if (mTouchExplorer.contains(displayId)) {
mTouchExplorer.get(displayId).setServiceDetectsGestures(mode);
}
+ mServiceDetectsGestures.put(displayId, mode);
+ }
+
+ public void resetServiceDetectsGestures() {
+ mServiceDetectsGestures.clear();
}
public void requestTouchExploration(int displayId) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 085a58909b6d..47b415630de8 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1695,31 +1695,34 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
private boolean scheduleNotifyMotionEvent(MotionEvent event) {
+ boolean result = false;
+ int displayId = event.getDisplayId();
synchronized (mLock) {
AccessibilityUserState state = getCurrentUserStateLocked();
for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
AccessibilityServiceConnection service = state.mBoundServices.get(i);
- if (service.mRequestTouchExplorationMode) {
+ if (service.isServiceDetectsGesturesEnabled(displayId)) {
service.notifyMotionEvent(event);
- return true;
+ result = true;
}
}
}
- return false;
+ return result;
}
private boolean scheduleNotifyTouchState(int displayId, int touchState) {
+ boolean result = false;
synchronized (mLock) {
AccessibilityUserState state = getCurrentUserStateLocked();
for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
AccessibilityServiceConnection service = state.mBoundServices.get(i);
- if (service.mRequestTouchExplorationMode) {
+ if (service.isServiceDetectsGesturesEnabled(displayId)) {
service.notifyTouchState(displayId, touchState);
- return true;
+ result = true;
}
}
}
- return false;
+ return result;
}
private void notifyClearAccessibilityCacheLocked() {
@@ -2292,8 +2295,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (!mHasInputFilter) {
mHasInputFilter = true;
if (mInputFilter == null) {
- mInputFilter = new AccessibilityInputFilter(mContext,
- AccessibilityManagerService.this);
+ mInputFilter =
+ new AccessibilityInputFilter(
+ mContext, AccessibilityManagerService.this);
}
inputFilter = mInputFilter;
setInputFilter = true;
@@ -2303,6 +2307,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
if (mHasInputFilter) {
mHasInputFilter = false;
mInputFilter.setUserAndEnabledFeatures(userState.mUserId, 0);
+ mInputFilter.resetServiceDetectsGestures();
+ if (userState.isTouchExplorationEnabledLocked()) {
+ // Service gesture detection is turned on and off on a per-display
+ // basis.
+ final ArrayList<Display> displays = getValidDisplayList();
+ for (Display display : displays) {
+ int displayId = display.getDisplayId();
+ boolean mode = userState.isServiceDetectsGesturesEnabled(displayId);
+ mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode);
+ }
+ }
inputFilter = null;
setInputFilter = true;
}
@@ -2618,6 +2633,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
Binder.restoreCallingIdentity(identity);
}
}
+ // Service gesture detection is turned on and off on a per-display
+ // basis.
+ userState.resetServiceDetectsGestures();
+ final ArrayList<Display> displays = getValidDisplayList();
+ for (AccessibilityServiceConnection service: userState.mBoundServices) {
+ for (Display display : displays) {
+ int displayId = display.getDisplayId();
+ if (service.isServiceDetectsGesturesEnabled(displayId)) {
+ userState.setServiceDetectsGesturesEnabled(displayId, true);
+ }
+ }
+ }
userState.setServiceHandlesDoubleTapLocked(serviceHandlesDoubleTapEnabled);
userState.setMultiFingerGesturesLocked(requestMultiFingerGestures);
userState.setTwoFingerPassthroughLocked(requestTwoFingerPassthrough);
@@ -4342,6 +4369,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private void setServiceDetectsGesturesInternal(int displayId, boolean mode) {
synchronized (mLock) {
+ getCurrentUserStateLocked().setServiceDetectsGesturesEnabled(displayId, mode);
if (mHasInputFilter && mInputFilter != null) {
mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 0cb7209c187a..0db169fd76c3 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -44,6 +44,7 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManagerClient;
@@ -118,6 +119,7 @@ class AccessibilityUserState {
private boolean mRequestMultiFingerGestures;
private boolean mRequestTwoFingerPassthrough;
private boolean mSendMotionEventsEnabled;
+ private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0);
private int mUserInteractiveUiTimeout;
private int mUserNonInteractiveUiTimeout;
private int mNonInteractiveUiTimeout = 0;
@@ -991,4 +993,19 @@ class AccessibilityUserState {
mFocusStrokeWidth = strokeWidth;
mFocusColor = color;
}
+
+ public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) {
+ mServiceDetectsGestures.put(displayId, mode);
+ }
+
+ public void resetServiceDetectsGestures() {
+ mServiceDetectsGestures.clear();
+ }
+
+ public boolean isServiceDetectsGesturesEnabled(int displayId) {
+ if (mServiceDetectsGestures.contains(displayId)) {
+ return mServiceDetectsGestures.get(displayId);
+ }
+ return false;
+ }
}
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index 933d2596aed8..e40f001f27d5 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -473,6 +473,18 @@ public abstract class SystemService {
}
/**
+ * The {@link UserManager#isUserVisible() user visibility} changed.
+ *
+ * <p>This callback is called before the user starts or is switched to (or after it stops), when
+ * its visibility changed because of that action.
+ *
+ * @hide
+ */
+ // NOTE: change visible to int if this method becomes a @SystemApi
+ public void onUserVisibilityChanged(@NonNull TargetUser user, boolean visible) {
+ }
+
+ /**
* Called when an existing user is stopping, for system services to finalize any per-user
* state they maintain for running users. This is called prior to sending the SHUTDOWN
* broadcast to the user; it is a good place to stop making use of any resources of that
diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java
index 1a8cf0b07cb6..83d86cdc05c6 100644
--- a/services/core/java/com/android/server/SystemServiceManager.java
+++ b/services/core/java/com/android/server/SystemServiceManager.java
@@ -75,13 +75,17 @@ public final class SystemServiceManager implements Dumpable {
// Constants used on onUser(...)
// NOTE: do not change their values, as they're used on Trace calls and changes might break
// performance tests that rely on them.
- private static final String USER_STARTING = "Start"; // Logged as onStartUser
- private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUnlockingUser
- private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUnlockedUser
- private static final String USER_SWITCHING = "Switch"; // Logged as onSwitchUser
- private static final String USER_STOPPING = "Stop"; // Logged as onStopUser
- private static final String USER_STOPPED = "Cleanup"; // Logged as onCleanupUser
- private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onCompletedEventUser
+ private static final String USER_STARTING = "Start"; // Logged as onUserStarting()
+ private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUserUnlocking()
+ private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUserUnlocked()
+ private static final String USER_SWITCHING = "Switch"; // Logged as onUserSwitching()
+ private static final String USER_STOPPING = "Stop"; // Logged as onUserStopping()
+ private static final String USER_STOPPED = "Cleanup"; // Logged as onUserStopped()
+ private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onUserCompletedEvent()
+ private static final String USER_VISIBLE = "Visible"; // Logged on onUserVisible() and
+ // onUserStarting() (when visible is true)
+ private static final String USER_INVISIBLE = "Invisible"; // Logged on onUserStopping()
+ // (when visibilityChanged is true)
// The default number of threads to use if lifecycle thread pool is enabled.
private static final int DEFAULT_MAX_USER_POOL_THREADS = 3;
@@ -350,18 +354,41 @@ public final class SystemServiceManager implements Dumpable {
/**
* Starts the given user.
*/
- public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) {
- EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId);
+ public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId,
+ boolean visible) {
+ EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId, visible ? 1 : 0);
final TargetUser targetUser = newTargetUser(userId);
synchronized (mTargetUsers) {
mTargetUsers.put(userId, targetUser);
}
+ if (visible) {
+ // Must send the user visiiblity change first, for 2 reasons:
+ // 1. Automotive need to update the user-zone mapping ASAP and it's one of the few
+ // services listening to this event (OTOH, there are manyy listeners to USER_STARTING
+ // and some can take a while to process it)
+ // 2. When a user is switched from bg to fg, the onUserVisibilityChanged() callback is
+ // called onUserSwitching(), so calling it before onUserStarting() make it more
+ // consistent with that
+ onUser(t, USER_VISIBLE, /* prevUser= */ null, targetUser);
+ }
onUser(t, USER_STARTING, /* prevUser= */ null, targetUser);
}
/**
+ * Updates the user visibility.
+ *
+ * <p><b>NOTE: </b>this method should only be called when a user that is already running become
+ * visible; if the user is starting visible, callers should call
+ * {@link #onUserStarting(TimingsTraceAndSlog, int, boolean)} instead
+ */
+ public void onUserVisible(@UserIdInt int userId) {
+ EventLog.writeEvent(EventLogTags.SSM_USER_VISIBLE, userId);
+ onUser(USER_VISIBLE, userId);
+ }
+
+ /**
* Unlocks the given user.
*/
public void onUserUnlocking(@UserIdInt int userId) {
@@ -408,9 +435,12 @@ public final class SystemServiceManager implements Dumpable {
/**
* Stops the given user.
*/
- public void onUserStopping(@UserIdInt int userId) {
- EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId);
+ public void onUserStopping(@UserIdInt int userId, boolean visibilityChanged) {
+ EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId, visibilityChanged ? 1 : 0);
onUser(USER_STOPPING, userId);
+ if (visibilityChanged) {
+ onUser(USER_INVISIBLE, userId);
+ }
}
/**
@@ -456,13 +486,12 @@ public final class SystemServiceManager implements Dumpable {
TargetUser targetUser = getTargetUser(userId);
Preconditions.checkState(targetUser != null, "No TargetUser for " + userId);
- onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null,
- targetUser);
+ onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, targetUser);
}
private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat,
@Nullable TargetUser prevUser, @NonNull TargetUser curUser) {
- onUser(t, onWhat, prevUser, curUser, /* completedEventType=*/ null);
+ onUser(t, onWhat, prevUser, curUser, /* completedEventType= */ null);
}
private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat,
@@ -534,6 +563,12 @@ public final class SystemServiceManager implements Dumpable {
threadPool.submit(getOnUserCompletedEventRunnable(
t, service, serviceName, curUser, completedEventType));
break;
+ case USER_VISIBLE:
+ service.onUserVisibilityChanged(curUser, /* visible= */ true);
+ break;
+ case USER_INVISIBLE:
+ service.onUserVisibilityChanged(curUser, /* visible= */ false);
+ break;
default:
throw new IllegalArgumentException(onWhat + " what?");
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b166adc9a828..2cf3462554f5 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3891,24 +3891,29 @@ public class ActivityManagerService extends IActivityManager.Stub
finishForceStopPackageLocked(packageName, appInfo.uid);
}
}
- final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED,
- Uri.fromParts("package", packageName, null));
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
- | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- intent.putExtra(Intent.EXTRA_UID, (appInfo != null) ? appInfo.uid : -1);
- intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId);
- final int[] visibilityAllowList =
- mPackageManagerInt.getVisibilityAllowList(packageName, resolvedUserId);
- if (isInstantApp) {
- intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
- broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent,
- null, null, null, 0, null, null, permission.ACCESS_INSTANT_APPS,
- null, false, false, resolvedUserId, false, null,
- visibilityAllowList);
- } else {
- broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent,
- null, null, null, 0, null, null, null, null, false, false,
- resolvedUserId, false, null, visibilityAllowList);
+
+ if (succeeded) {
+ final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED,
+ Uri.fromParts("package", packageName, null /* fragment */));
+ intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
+ | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.putExtra(Intent.EXTRA_UID,
+ (appInfo != null) ? appInfo.uid : INVALID_UID);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId);
+ if (isInstantApp) {
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+ }
+ final int[] visibilityAllowList = mPackageManagerInt.getVisibilityAllowList(
+ packageName, resolvedUserId);
+
+ broadcastIntentInPackage("android", null /* featureId */,
+ SYSTEM_UID, uid, pid, intent, null /* resolvedType */,
+ null /* resultToApp */, null /* resultTo */, 0 /* resultCode */,
+ null /* resultData */, null /* resultExtras */,
+ isInstantApp ? permission.ACCESS_INSTANT_APPS : null,
+ null /* bOptions */, false /* serialized */, false /* sticky */,
+ resolvedUserId, false /* allowBackgroundActivityStarts */,
+ null /* backgroundActivityStartsToken */, visibilityAllowList);
}
if (observer != null) {
@@ -8346,14 +8351,14 @@ public class ActivityManagerService extends IActivityManager.Stub
mBatteryStatsService.noteEvent(BatteryStats.HistoryItem.EVENT_USER_FOREGROUND_START,
Integer.toString(currentUserId), currentUserId);
- // On Automotive, at this point the system user has already been started and unlocked,
- // and some of the tasks we do here have already been done. So skip those in that case.
- // TODO(b/132262830, b/203885241): this workdound shouldn't be necessary once we move the
- // headless-user start logic to UserManager-land
+ // On Automotive / Headless System User Mode, at this point the system user has already been
+ // started and unlocked, and some of the tasks we do here have already been done. So skip
+ // those in that case.
+ // TODO(b/242195409): this workaround shouldn't be necessary once we move the headless-user
+ // start logic to UserManager-land
final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM;
-
if (bootingSystemUser) {
- mSystemServiceManager.onUserStarting(t, currentUserId);
+ mUserController.onSystemUserStarting();
}
synchronized (this) {
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 4590c859a909..417a0e5ede83 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -133,7 +133,7 @@ public class BroadcastConstants {
*/
public boolean MODERN_QUEUE_ENABLED = DEFAULT_MODERN_QUEUE_ENABLED;
private static final String KEY_MODERN_QUEUE_ENABLED = "modern_queue_enabled";
- private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = false;
+ private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = true;
/**
* For {@link BroadcastQueueModernImpl}: Maximum number of process queues to
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 5123517e272d..f7d24e9b8b4e 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -85,9 +85,16 @@ class BroadcastProcessQueue {
@Nullable ProcessRecord app;
/**
- * Track name to use for {@link Trace} events.
+ * Track name to use for {@link Trace} events, defined as part of upgrading
+ * into a running slot.
*/
- @Nullable String traceTrackName;
+ @Nullable String runningTraceTrackName;
+
+ /**
+ * Flag indicating if this process should be OOM adjusted, defined as part
+ * of upgrading into a running slot.
+ */
+ boolean runningOomAdjusted;
/**
* Snapshotted value of {@link ProcessRecord#getCpuDelayTime()}, typically
@@ -141,7 +148,8 @@ class BroadcastProcessQueue {
private boolean mActiveViaColdStart;
/**
- * Count of {@link #mPending} broadcasts of these various flavors.
+ * Count of {@link #mPending} and {@link #mPendingUrgent} broadcasts of
+ * these various flavors.
*/
private int mCountForeground;
private int mCountOrdered;
@@ -150,6 +158,7 @@ class BroadcastProcessQueue {
private int mCountInteractive;
private int mCountResultTo;
private int mCountInstrumented;
+ private int mCountManifest;
private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE;
private @Reason int mRunnableAtReason = REASON_EMPTY;
@@ -206,7 +215,7 @@ class BroadcastProcessQueue {
// with implicit responsiveness expectations.
final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending;
queue.addLast(newBroadcastArgs);
- onBroadcastEnqueued(record);
+ onBroadcastEnqueued(record, recordIndex);
}
/**
@@ -224,7 +233,8 @@ class BroadcastProcessQueue {
while (it.hasNext()) {
final SomeArgs args = it.next();
final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
- final Object testReceiver = testRecord.receivers.get(args.argi1);
+ final int testRecordIndex = args.argi1;
+ final Object testReceiver = testRecord.receivers.get(testRecordIndex);
if ((record.callingUid == testRecord.callingUid)
&& (record.userId == testRecord.userId)
&& record.intent.filterEquals(testRecord.intent)
@@ -233,8 +243,8 @@ class BroadcastProcessQueue {
args.arg1 = record;
args.argi1 = recordIndex;
args.argi2 = blockedUntilTerminalCount;
- onBroadcastDequeued(testRecord);
- onBroadcastEnqueued(record);
+ onBroadcastDequeued(testRecord, testRecordIndex);
+ onBroadcastEnqueued(record, recordIndex);
return true;
}
}
@@ -284,13 +294,13 @@ class BroadcastProcessQueue {
while (it.hasNext()) {
final SomeArgs args = it.next();
final BroadcastRecord record = (BroadcastRecord) args.arg1;
- final int index = args.argi1;
- if (predicate.test(record, index)) {
- consumer.accept(record, index);
+ final int recordIndex = args.argi1;
+ if (predicate.test(record, recordIndex)) {
+ consumer.accept(record, recordIndex);
if (andRemove) {
args.recycle();
it.remove();
- onBroadcastDequeued(record);
+ onBroadcastDequeued(record, recordIndex);
}
didSomething = true;
}
@@ -339,7 +349,7 @@ class BroadcastProcessQueue {
* Return if we know of an actively running "warm" process for this queue.
*/
public boolean isProcessWarm() {
- return (app != null) && (app.getThread() != null) && !app.isKilled();
+ return (app != null) && (app.getOnewayThread() != null) && !app.isKilled();
}
public int getPreferredSchedulingGroupLocked() {
@@ -385,7 +395,7 @@ class BroadcastProcessQueue {
mActiveCountSinceIdle++;
mActiveViaColdStart = false;
next.recycle();
- onBroadcastDequeued(mActive);
+ onBroadcastDequeued(mActive, mActiveIndex);
}
/**
@@ -403,7 +413,7 @@ class BroadcastProcessQueue {
/**
* Update summary statistics when the given record has been enqueued.
*/
- private void onBroadcastEnqueued(@NonNull BroadcastRecord record) {
+ private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) {
if (record.isForeground()) {
mCountForeground++;
}
@@ -425,13 +435,16 @@ class BroadcastProcessQueue {
if (record.callerInstrumented) {
mCountInstrumented++;
}
+ if (record.receivers.get(recordIndex) instanceof ResolveInfo) {
+ mCountManifest++;
+ }
invalidateRunnableAt();
}
/**
* Update summary statistics when the given record has been dequeued.
*/
- private void onBroadcastDequeued(@NonNull BroadcastRecord record) {
+ private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) {
if (record.isForeground()) {
mCountForeground--;
}
@@ -453,34 +466,37 @@ class BroadcastProcessQueue {
if (record.callerInstrumented) {
mCountInstrumented--;
}
+ if (record.receivers.get(recordIndex) instanceof ResolveInfo) {
+ mCountManifest--;
+ }
invalidateRunnableAt();
}
public void traceProcessStartingBegin() {
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- traceTrackName, toShortString() + " starting", hashCode());
+ runningTraceTrackName, toShortString() + " starting", hashCode());
}
public void traceProcessRunningBegin() {
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- traceTrackName, toShortString() + " running", hashCode());
+ runningTraceTrackName, toShortString() + " running", hashCode());
}
public void traceProcessEnd() {
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- traceTrackName, hashCode());
+ runningTraceTrackName, hashCode());
}
public void traceActiveBegin() {
final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- traceTrackName, mActive.toShortString() + " scheduled", cookie);
+ runningTraceTrackName, mActive.toShortString() + " scheduled", cookie);
}
public void traceActiveEnd() {
final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- traceTrackName, cookie);
+ runningTraceTrackName, cookie);
}
/**
@@ -540,6 +556,14 @@ class BroadcastProcessQueue {
}
/**
+ * Quickly determine if this queue has broadcasts waiting to be delivered to
+ * manifest receivers, which indicates we should request an OOM adjust.
+ */
+ public boolean isPendingManifest() {
+ return mCountManifest > 0;
+ }
+
+ /**
* Quickly determine if this queue has broadcasts that are still waiting to
* be delivered at some point in the future.
*/
@@ -807,7 +831,7 @@ class BroadcastProcessQueue {
@NeverCompile
public void dumpLocked(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw) {
- if ((mActive == null) && mPending.isEmpty()) return;
+ if ((mActive == null) && isEmpty()) return;
pw.print(toShortString());
if (isRunnable()) {
@@ -823,6 +847,10 @@ class BroadcastProcessQueue {
if (mActive != null) {
dumpRecord(now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount);
}
+ for (SomeArgs args : mPendingUrgent) {
+ final BroadcastRecord r = (BroadcastRecord) args.arg1;
+ dumpRecord(now, pw, r, args.argi1, args.argi2);
+ }
for (SomeArgs args : mPending) {
final BroadcastRecord r = (BroadcastRecord) args.arg1;
dumpRecord(now, pw, r, args.argi1, args.argi2);
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.md b/services/core/java/com/android/server/am/BroadcastQueue.md
new file mode 100644
index 000000000000..81317932ef9b
--- /dev/null
+++ b/services/core/java/com/android/server/am/BroadcastQueue.md
@@ -0,0 +1,98 @@
+# Broadcast Queue Design
+
+Broadcast intents are one of the major building blocks of the Android platform,
+generally intended for asynchronous notification of events. There are three
+flavors of intents that can be broadcast:
+
+* **Normal** broadcast intents are dispatched to relevant receivers.
+* **Ordered** broadcast intents are dispatched in a specific order to
+receivers, where each receiver has the opportunity to influence the final
+"result" of a broadcast, including aborting delivery to any remaining receivers.
+* **Sticky** broadcast intents are dispatched to relevant receivers, and are
+then retained internally for immediate dispatch to any future receivers. (This
+capability has been deprecated and its use is discouraged due to its system
+health impact.)
+
+And there are there two ways to receive these intents:
+
+* Registered receivers (via `Context.registerReceiver()` methods) are
+dynamically requested by a running app to receive intents. These requests are
+only maintained while the process is running, and are discarded at process
+death.
+* Manifest receivers (via the `<receiver>` tag in `AndroidManifest.xml`) are
+statically requested by an app to receive intents. These requests are delivered
+regardless of process running state, and have the ability to cold-start a
+process that isn't currently running.
+
+## Per-process queues
+
+The design of `BroadcastQueueModernImpl` is centered around maintaining a
+separate `BroadcastProcessQueue` instance for each potential process on the
+device. At this level, a process refers to the `android:process` attributes
+defined in `AndroidManifest.xml` files, which means it can be defined and
+populated regardless of the process state. (For example, a given
+`android:process` can have multiple `ProcessRecord`/PIDs defined as it's
+launched, killed, and relaunched over long periods of time.)
+
+Each per-process queue has the concept of a _runnable at_ timestamp when it's
+next eligible for execution, and that value can be influenced by a wide range
+of policies, such as:
+
+* Which broadcasts are pending dispatch to a given process. For example, an
+"urgent" broadcast typically results in an earlier _runnable at_ time, or a
+"delayed" broadcast typically results in a later _runnable at_ time.
+* Current state of the process or UID. For example, a "cached" process
+typically results in a later _runnable at_ time, or an "instrumented" process
+typically results in an earlier _runnable at_ time.
+* Blocked waiting for an earlier receiver to complete. For example, an
+"ordered" or "prioritized" broadcast typically results in a _not currently
+runnable_ value.
+
+Each per-process queue represents a single remote `ApplicationThread`, and we
+only dispatch a single broadcast at a time to each process to ensure developers
+see consistent ordering of broadcast events. The flexible _runnable at_
+policies above mean that no inter-process ordering guarantees are provided,
+except for those explicitly provided by "ordered" or "prioritized" broadcasts.
+
+## Parallel dispatch
+
+Given a collection of per-process queues with valid _runnable at_ timestamps,
+BroadcastQueueModernImpl is then willing to promote those _runnable_ queues
+into a _running_ state. We choose the next per-process queue to promote based
+on the sorted ordering of the _runnable at_ timestamps, selecting the
+longest-waiting process first, which aims to reduce overall broadcast dispatch
+latency.
+
+To preserve system health, at most
+`BroadcastConstants.MAX_RUNNING_PROCESS_QUEUES` processes are allowed to be in
+the _running_ state at any given time, and at most one process is allowed to be
+_cold started_ at any given time. (For background, _cold starting_ a process
+by forking and specializing the zygote is a relatively heavy operation, so
+limiting ourselves to a single pending _cold start_ reduces system-wide
+resource contention.)
+
+After each broadcast is dispatched to a given process, we consider dispatching
+any additional pending broadcasts to that process, aimed at batching dispatch
+to better amortize the cost of OOM adjustments.
+
+## Starvation considerations
+
+Careful attention is given to several types of potential resource starvation,
+along with the mechanisms of mitigation:
+
+* A per-process queue that has a delayed _runnable at_ policy applied can risk
+growing very large. This is mitigated by
+`BroadcastConstants.MAX_PENDING_BROADCASTS` bypassing any delays when the queue
+grows too large.
+* A per-process queue that has a large number of pending broadcasts can risk
+monopolizing one of the limited _runnable_ slots. This is mitigated by
+`BroadcastConstants.MAX_RUNNING_ACTIVE_BROADCASTS` being used to temporarily
+"retire" a running process to give other processes a chance to run.
+* An "urgent" broadcast dispatched to a process with a large backlog of
+"non-urgent" broadcasts can risk large dispatch latencies. This is mitigated
+by maintaining a separate `mPendingUrgent` queue of urgent events, which we
+prefer to dispatch before the normal `mPending` queue.
+* A process with a scheduled broadcast desires to execute, but heavy CPU
+contention can risk the process not receiving enough resources before an ANR
+timeout is triggered. This is mitigated by extending the "soft" ANR timeout by
+up to double the original timeout length.
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 4c831bd47ee4..9e9eb71db4e5 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -58,6 +58,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
+import android.os.BundleMerger;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
@@ -364,6 +365,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
BroadcastProcessQueue nextQueue = queue.runnableAtNext;
final long runnableAt = queue.getRunnableAt();
+ // When broadcasts are skipped or failed during list traversal, we
+ // might encounter a queue that is no longer runnable; skip it
+ if (!queue.isRunnable()) {
+ queue = nextQueue;
+ continue;
+ }
+
// If queues beyond this point aren't ready to run yet, schedule
// another pass when they'll be runnable
if (runnableAt > now && !waitingFor) {
@@ -401,7 +409,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
mRunnableHead = removeFromRunnableList(mRunnableHead, queue);
// Emit all trace events for this process into a consistent track
- queue.traceTrackName = TAG + ".mRunning[" + queueIndex + "]";
+ queue.runningTraceTrackName = TAG + ".mRunning[" + queueIndex + "]";
+ queue.runningOomAdjusted = queue.isPendingManifest();
// If we're already warm, schedule next pending broadcast now;
// otherwise we'll wait for the cold start to circle back around
@@ -415,9 +424,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
scheduleReceiverColdLocked(queue);
}
- // We've moved at least one process into running state above, so we
- // need to kick off an OOM adjustment pass
- updateOomAdj = true;
+ // Only kick off an OOM adjustment pass if needed
+ updateOomAdj |= queue.runningOomAdjusted;
// Move to considering next runnable queue
queue = nextQueue;
@@ -543,16 +551,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
}, mBroadcastConsumerSkipAndCanceled, true);
}
- final int policy = (r.options != null)
- ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
- if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) {
- forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
- // We only allow caller to remove broadcasts they enqueued
- return (r.callingUid == testRecord.callingUid)
- && (r.userId == testRecord.userId)
- && r.matchesDeliveryGroup(testRecord);
- }, mBroadcastConsumerSkipAndCanceled, true);
- }
+ applyDeliveryGroupPolicy(r);
if (r.isReplacePending()) {
// Leave the skipped broadcasts intact in queue, so that we can
@@ -609,6 +608,41 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
}
}
+ private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
+ final int policy = (r.options != null)
+ ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL;
+ final BroadcastConsumer broadcastConsumer;
+ switch (policy) {
+ case BroadcastOptions.DELIVERY_GROUP_POLICY_ALL:
+ // Older broadcasts need to be left as is in this case, so nothing more to do.
+ return;
+ case BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT:
+ broadcastConsumer = mBroadcastConsumerSkipAndCanceled;
+ break;
+ case BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED:
+ final BundleMerger extrasMerger = r.options.getDeliveryGroupExtrasMerger();
+ if (extrasMerger == null) {
+ // Extras merger is required to be able to merge the extras. So, if it's not
+ // supplied, then ignore the delivery group policy.
+ return;
+ }
+ broadcastConsumer = (record, recordIndex) -> {
+ r.intent.mergeExtras(record.intent, extrasMerger);
+ mBroadcastConsumerSkipAndCanceled.accept(record, recordIndex);
+ };
+ break;
+ default:
+ logw("Unknown delivery group policy: " + policy);
+ return;
+ }
+ forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
+ // We only allow caller to remove broadcasts they enqueued
+ return (r.callingUid == testRecord.callingUid)
+ && (r.userId == testRecord.userId)
+ && r.matchesDeliveryGroup(testRecord);
+ }, broadcastConsumer, true);
+ }
+
/**
* Schedule the currently active broadcast on the given queue when we know
* the process is cold. This kicks off a cold start and will eventually call
@@ -736,7 +770,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app);
setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED);
- final IApplicationThread thread = app.getThread();
+ final IApplicationThread thread = app.getOnewayThread();
if (thread != null) {
try {
if (receiver instanceof BroadcastFilter) {
@@ -777,7 +811,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
private void scheduleResultTo(@NonNull BroadcastRecord r) {
if ((r.resultToApp == null) || (r.resultTo == null)) return;
final ProcessRecord app = r.resultToApp;
- final IApplicationThread thread = app.getThread();
+ final IApplicationThread thread = app.getOnewayThread();
if (thread != null) {
mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(
app, OOM_ADJ_REASON_FINISH_RECEIVER);
@@ -1245,8 +1279,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
if (queue.app != null) {
queue.app.mReceivers.incrementCurReceivers();
- queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
-
// Don't bump its LRU position if it's in the background restricted.
if (mService.mInternal.getRestrictionLevel(
queue.uid) < ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
@@ -1256,7 +1288,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(queue.app,
OOM_ADJ_REASON_START_RECEIVER);
- mService.enqueueOomAdjTargetLocked(queue.app);
+ if (queue.runningOomAdjusted) {
+ queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
+ mService.enqueueOomAdjTargetLocked(queue.app);
+ }
}
}
@@ -1266,10 +1301,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
*/
private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) {
if (queue.app != null) {
- // Update during our next pass; no need for an immediate update
- mService.enqueueOomAdjTargetLocked(queue.app);
-
queue.app.mReceivers.decrementCurReceivers();
+
+ if (queue.runningOomAdjusted) {
+ mService.enqueueOomAdjTargetLocked(queue.app);
+ }
}
}
diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags
index d080036733a5..dec8b62de2ec 100644
--- a/services/core/java/com/android/server/am/EventLogTags.logtags
+++ b/services/core/java/com/android/server/am/EventLogTags.logtags
@@ -101,7 +101,7 @@ option java_package com.android.server.am
30073 uc_finish_user_stopping (userId|1|5)
30074 uc_finish_user_stopped (userId|1|5)
30075 uc_switch_user (userId|1|5)
-30076 uc_start_user_internal (userId|1|5)
+30076 uc_start_user_internal (userId|1|5),(foreground|1),(displayId|1|5)
30077 uc_unlock_user (userId|1|5)
30078 uc_finish_user_boot (userId|1|5)
30079 uc_dispatch_user_switch (oldUserId|1|5),(newUserId|1|5)
@@ -109,13 +109,14 @@ option java_package com.android.server.am
30081 uc_send_user_broadcast (userId|1|5),(IntentAction|3)
# Tags below are used by SystemServiceManager - although it's technically part of am, these are
# also user switch events and useful to be analyzed together with events above.
-30082 ssm_user_starting (userId|1|5)
+30082 ssm_user_starting (userId|1|5),(visible|1)
30083 ssm_user_switching (oldUserId|1|5),(newUserId|1|5)
30084 ssm_user_unlocking (userId|1|5)
30085 ssm_user_unlocked (userId|1|5)
-30086 ssm_user_stopping (userId|1|5)
+30086 ssm_user_stopping (userId|1|5),(visibilityChanged|1)
30087 ssm_user_stopped (userId|1|5)
30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5)
+30089 ssm_user_visible (userId|1|5)
# Foreground service start/stop events.
30100 am_foreground_service_start (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3)
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index 3b04dbb1da98..0a8c6400a6fd 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -54,6 +54,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.procstats.ProcessState;
import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.os.Zygote;
+import com.android.server.FgThread;
import com.android.server.wm.WindowProcessController;
import com.android.server.wm.WindowProcessListener;
@@ -143,6 +144,13 @@ class ProcessRecord implements WindowProcessListener {
private IApplicationThread mThread;
/**
+ * Instance of {@link #mThread} that will always meet the {@code oneway}
+ * contract, possibly by using {@link SameProcessApplicationThread}.
+ */
+ @CompositeRWLock({"mService", "mProcLock"})
+ private IApplicationThread mOnewayThread;
+
+ /**
* Always keep this application running?
*/
private volatile boolean mPersistent;
@@ -603,16 +611,27 @@ class ProcessRecord implements WindowProcessListener {
return mThread;
}
+ @GuardedBy(anyOf = {"mService", "mProcLock"})
+ IApplicationThread getOnewayThread() {
+ return mOnewayThread;
+ }
+
@GuardedBy({"mService", "mProcLock"})
public void makeActive(IApplicationThread thread, ProcessStatsService tracker) {
mProfile.onProcessActive(thread, tracker);
mThread = thread;
+ if (mPid == Process.myPid()) {
+ mOnewayThread = new SameProcessApplicationThread(thread, FgThread.getHandler());
+ } else {
+ mOnewayThread = thread;
+ }
mWindowProcessController.setThread(thread);
}
@GuardedBy({"mService", "mProcLock"})
public void makeInactive(ProcessStatsService tracker) {
mThread = null;
+ mOnewayThread = null;
mWindowProcessController.setThread(null);
mProfile.onProcessInactive(tracker);
}
diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
new file mode 100644
index 000000000000..a3c011188539
--- /dev/null
+++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.am;
+
+import android.annotation.NonNull;
+import android.app.IApplicationThread;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.res.CompatibilityInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+
+import java.util.Objects;
+
+/**
+ * Wrapper around an {@link IApplicationThread} that delegates selected calls
+ * through a {@link Handler} so they meet the {@code oneway} contract of
+ * returning immediately after dispatch.
+ */
+public class SameProcessApplicationThread extends IApplicationThread.Default {
+ private final IApplicationThread mWrapped;
+ private final Handler mHandler;
+
+ public SameProcessApplicationThread(@NonNull IApplicationThread wrapped,
+ @NonNull Handler handler) {
+ mWrapped = Objects.requireNonNull(wrapped);
+ mHandler = Objects.requireNonNull(handler);
+ }
+
+ @Override
+ public void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo,
+ int resultCode, String data, Bundle extras, boolean sync, int sendingUser,
+ int processState) {
+ mHandler.post(() -> {
+ try {
+ mWrapped.scheduleReceiver(intent, info, compatInfo, resultCode, data, extras, sync,
+ sendingUser, processState);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Override
+ public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode,
+ String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser,
+ int processState) {
+ mHandler.post(() -> {
+ try {
+ mWrapped.scheduleRegisteredReceiver(receiver, intent, resultCode, data, extras,
+ ordered, sticky, sendingUser, processState);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 3c26116e8ad2..dcc7a8ea4e44 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -100,6 +100,7 @@ import android.util.EventLog;
import android.util.IntArray;
import android.util.Pair;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
@@ -174,6 +175,9 @@ class UserController implements Handler.Callback {
static final int START_USER_SWITCH_FG_MSG = 120;
static final int COMPLETE_USER_SWITCH_MSG = 130;
static final int USER_COMPLETED_EVENT_MSG = 140;
+ static final int USER_VISIBLE_MSG = 150;
+
+ private static final int NO_ARG2 = 0;
// Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if
// the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not
@@ -421,6 +425,17 @@ class UserController implements Handler.Callback {
/** @see #getLastUserUnlockingUptime */
private volatile long mLastUserUnlockingUptime = 0;
+ /**
+ * List of visible users (as defined by {@link UserManager#isUserVisible()}).
+ *
+ * <p>It's only used to call {@link SystemServiceManager} when the visibility is changed upon
+ * the user starting or stopping.
+ *
+ * <p>Note: only the key is used, not the value.
+ */
+ @GuardedBy("mLock")
+ private final SparseBooleanArray mVisibleUsers = new SparseBooleanArray();
+
UserController(ActivityManagerService service) {
this(new Injector(service));
}
@@ -1050,11 +1065,27 @@ class UserController implements Handler.Callback {
// instead.
userManagerInternal.unassignUserFromDisplay(userId);
+ final boolean visibilityChanged;
+ boolean visibleBefore;
+ synchronized (mLock) {
+ visibleBefore = mVisibleUsers.get(userId);
+ if (visibleBefore) {
+ if (DEBUG_MU) {
+ Slogf.d(TAG, "Removing %d from mVisibleUsers", userId);
+ }
+ mVisibleUsers.delete(userId);
+ visibilityChanged = true;
+ } else {
+ visibilityChanged = false;
+ }
+ }
+
updateStartedUserArrayLU();
final boolean allowDelayedLockingCopied = allowDelayedLocking;
Runnable finishUserStoppingAsync = () ->
- mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied));
+ mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied,
+ visibilityChanged));
if (mInjector.getUserManager().isPreCreated(userId)) {
finishUserStoppingAsync.run();
@@ -1092,7 +1123,7 @@ class UserController implements Handler.Callback {
}
private void finishUserStopping(final int userId, final UserState uss,
- final boolean allowDelayedLocking) {
+ final boolean allowDelayedLocking, final boolean visibilityChanged) {
EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId);
synchronized (mLock) {
if (uss.state != UserState.STATE_STOPPING) {
@@ -1109,7 +1140,7 @@ class UserController implements Handler.Callback {
mInjector.batteryStatsServiceNoteEvent(
BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH,
Integer.toString(userId), userId);
- mInjector.getSystemServiceManager().onUserStopping(userId);
+ mInjector.getSystemServiceManager().onUserStopping(userId, visibilityChanged);
Runnable finishUserStoppedAsync = () ->
mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking));
@@ -1513,16 +1544,17 @@ class UserController implements Handler.Callback {
private boolean startUserInternal(@UserIdInt int userId, int displayId, boolean foreground,
@Nullable IProgressListener unlockListener, @NonNull TimingsTraceAndSlog t) {
if (DEBUG_MU) {
- Slogf.i(TAG, "Starting user %d on display %d %s", userId, displayId,
+ Slogf.i(TAG, "Starting user %d on display %d%s", userId, displayId,
foreground ? " in foreground" : "");
}
- if (displayId != Display.DEFAULT_DISPLAY) {
+ boolean onSecondaryDisplay = displayId != Display.DEFAULT_DISPLAY;
+ if (onSecondaryDisplay) {
Preconditions.checkArgument(!foreground, "Cannot start user %d in foreground AND "
+ "on secondary display (%d)", userId, displayId);
}
- // TODO(b/239982558): log display id (or use a new event)
- EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId);
+ EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId, foreground ? 1 : 0,
+ displayId);
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
@@ -1571,8 +1603,9 @@ class UserController implements Handler.Callback {
return false;
}
- if (foreground && userInfo.preCreated) {
- Slogf.w(TAG, "Cannot start pre-created user #" + userId + " as foreground");
+ if ((foreground || onSecondaryDisplay) && userInfo.preCreated) {
+ Slogf.w(TAG, "Cannot start pre-created user #" + userId + " in foreground or on "
+ + "secondary display");
return false;
}
@@ -1656,6 +1689,28 @@ class UserController implements Handler.Callback {
}
t.traceEnd();
+ // Need to call UM when user is on background, as there are some cases where the user
+ // cannot be started in background on a secondary display (for example, if user is a
+ // profile).
+ // TODO(b/253103846): it's also explicitly checking if the user is the USER_SYSTEM, as
+ // the UM call would return true during boot (when CarService / BootUserInitializer
+ // calls AM.startUserInBackground() because the system user is still the current user.
+ // TODO(b/244644281): another fragility of this check is that it must wait to call
+ // UMI.isUserVisible() until the user state is check, as that method checks if the
+ // profile of the current user is started. We should fix that dependency so the logic
+ // belongs to just one place (like UserDisplayAssigner)
+ boolean visible = foreground
+ || userId != UserHandle.USER_SYSTEM
+ && mInjector.getUserManagerInternal().isUserVisible(userId);
+ if (visible) {
+ synchronized (mLock) {
+ if (DEBUG_MU) {
+ Slogf.d(TAG, "Adding %d to mVisibleUsers", userId);
+ }
+ mVisibleUsers.put(userId, true);
+ }
+ }
+
// Make sure user is in the started state. If it is currently
// stopping, we need to knock that off.
if (uss.state == UserState.STATE_STOPPING) {
@@ -1692,8 +1747,15 @@ class UserController implements Handler.Callback {
// Booting up a new user, need to tell system services about it.
// Note that this is on the same handler as scheduling of broadcasts,
// which is important because it needs to go first.
- mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, 0));
+ mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId,
+ visible ? 1 : 0));
t.traceEnd();
+ } else if (visible) {
+ // User was already running and became visible (for example, when switching to a
+ // user that was started in the background before), so it's necessary to explicitly
+ // notify the services (while when the user starts from BOOTING, USER_START_MSG
+ // takes care of that.
+ mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBLE_MSG, userId, NO_ARG2));
}
t.traceBegin("sendMessages");
@@ -2110,6 +2172,11 @@ class UserController implements Handler.Callback {
mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0));
stopGuestOrEphemeralUserIfBackground(oldUserId);
stopUserOnSwitchIfEnforced(oldUserId);
+ if (oldUserId == UserHandle.USER_SYSTEM) {
+ // System user is never stopped, but its visibility is changed (as it is brought to the
+ // background)
+ updateSystemUserVisibility(/* visible= */ false);
+ }
t.traceEnd(); // end continueUserSwitch
}
@@ -2413,9 +2480,7 @@ class UserController implements Handler.Callback {
void setAllowUserUnlocking(boolean allowed) {
mAllowUserUnlocking = allowed;
if (DEBUG_MU) {
- // TODO(b/245335748): use Slogf.d instead
- // Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed);
- android.util.Slog.d(TAG, "setAllowUserUnlocking():" + allowed, new Exception());
+ Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed);
}
}
@@ -2457,10 +2522,34 @@ class UserController implements Handler.Callback {
}
void onSystemReady() {
+ if (DEBUG_MU) {
+ Slogf.d(TAG, "onSystemReady()");
+
+ }
updateCurrentProfileIds();
mInjector.reportCurWakefulnessUsageEvent();
}
+ // TODO(b/242195409): remove this method if initial system user boot logic is refactored?
+ void onSystemUserStarting() {
+ updateSystemUserVisibility(/* visible= */ !UserManager.isHeadlessSystemUserMode());
+ }
+
+ private void updateSystemUserVisibility(boolean visible) {
+ if (DEBUG_MU) {
+ Slogf.d(TAG, "updateSystemUserVisibility(): visible=%b", visible);
+ }
+ int userId = UserHandle.USER_SYSTEM;
+ synchronized (mLock) {
+ if (visible) {
+ mVisibleUsers.put(userId, true);
+ } else {
+ mVisibleUsers.delete(userId);
+ }
+ }
+ mInjector.onUserStarting(userId, visible);
+ }
+
/**
* Refreshes the list of users related to the current user when either a
* user switch happens or when a new related user is started in the
@@ -2846,6 +2935,9 @@ class UserController implements Handler.Callback {
proto.end(uToken);
}
}
+ for (int i = 0; i < mVisibleUsers.size(); i++) {
+ proto.write(UserControllerProto.VISIBLE_USERS_ARRAY, mVisibleUsers.keyAt(i));
+ }
proto.end(token);
}
}
@@ -2899,7 +2991,8 @@ class UserController implements Handler.Callback {
if (mSwitchingToSystemUserMessage != null) {
pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage);
}
- pw.println(" mLastUserUnlockingUptime:" + mLastUserUnlockingUptime);
+ pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime);
+ pw.println(" mVisibleUsers: " + mVisibleUsers);
}
}
@@ -2936,8 +3029,7 @@ class UserController implements Handler.Callback {
logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER,
USER_LIFECYCLE_EVENT_STATE_BEGIN);
- mInjector.getSystemServiceManager().onUserStarting(
- TimingsTraceAndSlog.newAsyncLog(), msg.arg1);
+ mInjector.onUserStarting(/* userId= */ msg.arg1, /* visible= */ msg.arg2 == 1);
scheduleOnUserCompletedEvent(msg.arg1,
UserCompletedEventType.EVENT_TYPE_USER_STARTING,
USER_COMPLETED_EVENT_DELAY_MS);
@@ -3018,6 +3110,9 @@ class UserController implements Handler.Callback {
case COMPLETE_USER_SWITCH_MSG:
completeUserSwitch(msg.arg1);
break;
+ case USER_VISIBLE_MSG:
+ mInjector.getSystemServiceManager().onUserVisible(/* userId= */ msg.arg1);
+ break;
}
return false;
}
@@ -3539,5 +3634,10 @@ class UserController implements Handler.Callback {
boolean isUsersOnSecondaryDisplaysEnabled() {
return UserManager.isUsersOnSecondaryDisplaysEnabled();
}
+
+ void onUserStarting(int userId, boolean visible) {
+ getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId,
+ visible);
+ }
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 2761ec04aa7e..7a5b58413014 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -600,8 +600,9 @@ public class FaceService extends SystemService {
}
try {
final SensorProps[] props = face.getSensorProps();
- final FaceProvider provider = new FaceProvider(getContext(), props, instance,
- mLockoutResetDispatcher, BiometricContext.getInstance(getContext()));
+ final FaceProvider provider = new FaceProvider(getContext(),
+ mBiometricStateCallback, props, instance, mLockoutResetDispatcher,
+ BiometricContext.getInstance(getContext()));
providers.add(provider);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
@@ -612,14 +613,14 @@ public class FaceService extends SystemService {
}
@android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
- @Override // Binder call
public void registerAuthenticators(
@NonNull List<FaceSensorPropertiesInternal> hidlSensors) {
mRegistry.registerAll(() -> {
final List<ServiceProvider> providers = new ArrayList<>();
for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
providers.add(
- Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher));
+ Face10.newInstance(getContext(), mBiometricStateCallback,
+ hidlSensor, mLockoutResetDispatcher));
}
providers.addAll(getAidlProviders());
return providers;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
index 73c272f7a779..cfbb5dce4c2b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
@@ -16,8 +16,6 @@
package com.android.server.biometrics.sensors.face.aidl;
-import static android.Manifest.permission.TEST_BIOMETRIC;
-
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.ITestSession;
@@ -33,7 +31,6 @@ import android.os.RemoteException;
import android.util.Slog;
import com.android.server.biometrics.HardwareAuthTokenUtils;
-import com.android.server.biometrics.Utils;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.face.FaceUtils;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index c12994c993e6..6488185c727d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,9 +52,11 @@ import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
+import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
import com.android.server.biometrics.sensors.InvalidationRequesterClient;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.PerformanceTracker;
@@ -81,6 +83,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
private boolean mTestHalEnabled;
@NonNull private final Context mContext;
+ @NonNull private final BiometricStateCallback mBiometricStateCallback;
@NonNull private final String mHalInstanceName;
@NonNull @VisibleForTesting
final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports
@@ -122,11 +125,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
}
}
- public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props,
+ public FaceProvider(@NonNull Context context,
+ @NonNull BiometricStateCallback biometricStateCallback,
+ @NonNull SensorProps[] props,
@NonNull String halInstanceName,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull BiometricContext biometricContext) {
mContext = context;
+ mBiometricStateCallback = biometricStateCallback;
mHalInstanceName = halInstanceName;
mSensors = new SparseArray<>();
mHandler = new Handler(Looper.getMainLooper());
@@ -363,16 +369,18 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
createLogger(BiometricsProtoEnums.ACTION_ENROLL,
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, maxTemplatesPerUser, debugConsent);
- scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
- @Override
- public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
- boolean success) {
- if (success) {
- scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
- scheduleInvalidationRequest(sensorId, userId);
- }
- }
- });
+ scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+ mBiometricStateCallback, new ClientMonitorCallback() {
+ @Override
+ public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
+ boolean success) {
+ ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
+ if (success) {
+ scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
+ scheduleInvalidationRequest(sensorId, userId);
+ }
+ }
+ }));
});
return id;
}
@@ -396,7 +404,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
token, id, callback, userId, opPackageName, sensorId,
createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
mBiometricContext, isStrongBiometric);
- scheduleForSensor(sensorId, client);
+ scheduleForSensor(sensorId, client, mBiometricStateCallback);
});
return id;
@@ -424,7 +432,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
mBiometricContext, isStrongBiometric,
mUsageStats, mSensors.get(sensorId).getLockoutCache(),
allowBackgroundAuthentication, isKeyguardBypassEnabled, biometricStrength);
- scheduleForSensor(sensorId, client);
+ scheduleForSensor(sensorId, client, mBiometricStateCallback);
});
}
@@ -479,7 +487,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext,
mSensors.get(sensorId).getAuthenticatorIds());
- scheduleForSensor(sensorId, client);
+ scheduleForSensor(sensorId, client, mBiometricStateCallback);
});
}
@@ -568,7 +576,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
if (favorHalEnrollments) {
client.setFavorHalEnrollments();
}
- scheduleForSensor(sensorId, client, callback);
+ scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback,
+ mBiometricStateCallback));
});
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
index 14af216a9dc5..7a6a274f8dd7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java
@@ -16,8 +16,6 @@
package com.android.server.biometrics.sensors.face.hidl;
-import static android.Manifest.permission.TEST_BIOMETRIC;
-
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.biometrics.ITestSession;
@@ -30,7 +28,6 @@ import android.os.Binder;
import android.os.RemoteException;
import android.util.Slog;
-import com.android.server.biometrics.Utils;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.face.FaceUtils;
@@ -53,6 +50,7 @@ public class BiometricTestSessionImpl extends ITestSession.Stub {
@NonNull private final Set<Integer> mEnrollmentIds;
@NonNull private final Random mRandom;
+
private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() {
@Override
public void onEnrollResult(Face face, int remaining) {
@@ -116,7 +114,8 @@ public class BiometricTestSessionImpl extends ITestSession.Stub {
};
BiometricTestSessionImpl(@NonNull Context context, int sensorId,
- @NonNull ITestSessionCallback callback, @NonNull Face10 face10,
+ @NonNull ITestSessionCallback callback,
+ @NonNull Face10 face10,
@NonNull Face10.HalResultController halResultController) {
mContext = context;
mSensorId = sensorId;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index c0a119ff5f1e..0e0ee1966024 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -62,8 +62,10 @@ import com.android.server.biometrics.sensors.AuthenticationConsumer;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricNotificationUtils;
import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
+import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
import com.android.server.biometrics.sensors.EnumerateConsumer;
import com.android.server.biometrics.sensors.ErrorConsumer;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -110,6 +112,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
private boolean mTestHalEnabled;
@NonNull private final FaceSensorPropertiesInternal mSensorProperties;
+ @NonNull private final BiometricStateCallback mBiometricStateCallback;
@NonNull private final Context mContext;
@NonNull private final BiometricScheduler mScheduler;
@NonNull private final Handler mHandler;
@@ -336,6 +339,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
@VisibleForTesting
Face10(@NonNull Context context,
+ @NonNull BiometricStateCallback biometricStateCallback,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull Handler handler,
@@ -343,6 +347,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
@NonNull BiometricContext biometricContext) {
mSensorProperties = sensorProps;
mContext = context;
+ mBiometricStateCallback = biometricStateCallback;
mSensorId = sensorProps.sensorId;
mScheduler = scheduler;
mHandler = handler;
@@ -366,11 +371,12 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
}
public static Face10 newInstance(@NonNull Context context,
+ @NonNull BiometricStateCallback biometricStateCallback,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher) {
final Handler handler = new Handler(Looper.getMainLooper());
- return new Face10(context, sensorProps, lockoutResetDispatcher, handler,
- new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
+ return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher,
+ handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,
null /* gestureAvailabilityTracker */),
BiometricContext.getInstance(context));
}
@@ -615,8 +621,19 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() {
@Override
+ public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
+ mBiometricStateCallback.onClientStarted(clientMonitor);
+ }
+
+ @Override
+ public void onBiometricAction(int action) {
+ mBiometricStateCallback.onBiometricAction(action);
+ }
+
+ @Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
+ mBiometricStateCallback.onClientFinished(clientMonitor, success);
if (success) {
// Update authenticatorIds
scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId());
@@ -661,7 +678,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient),
mBiometricContext, isStrongBiometric, mLockoutTracker,
mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled);
- mScheduler.scheduleClientMonitor(client);
+ mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
});
}
@@ -696,7 +713,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
createLogger(BiometricsProtoEnums.ACTION_REMOVE,
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, mAuthenticatorIds);
- mScheduler.scheduleClientMonitor(client);
+ mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
});
}
@@ -714,7 +731,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
createLogger(BiometricsProtoEnums.ACTION_REMOVE,
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, mAuthenticatorIds);
- mScheduler.scheduleClientMonitor(client);
+ mScheduler.scheduleClientMonitor(client, mBiometricStateCallback);
});
}
@@ -806,14 +823,15 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
BiometricsProtoEnums.CLIENT_UNKNOWN),
mBiometricContext, enrolledList,
FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds);
- mScheduler.scheduleClientMonitor(client, callback);
+ mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback,
+ mBiometricStateCallback));
});
}
@Override
public void scheduleInternalCleanup(int sensorId, int userId,
@Nullable ClientMonitorCallback callback) {
- scheduleInternalCleanup(userId, callback);
+ scheduleInternalCleanup(userId, mBiometricStateCallback);
}
@Override
@@ -1011,7 +1029,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider {
@Override
public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
@NonNull String opPackageName) {
- return new BiometricTestSessionImpl(mContext, mSensorId, callback, this,
- mHalResultController);
+ return new BiometricTestSessionImpl(mContext, mSensorId, callback,
+ this, mHalResultController);
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 17ba07f2c2bd..628c16afed5c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -384,28 +384,18 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
mBiometricContext,
mSensors.get(sensorId).getSensorProperties(),
mUdfpsOverlayController, mSidefpsController, maxTemplatesPerUser, enrollReason);
- scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
-
- @Override
- public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) {
- mBiometricStateCallback.onClientStarted(clientMonitor);
- }
-
- @Override
- public void onBiometricAction(int action) {
- mBiometricStateCallback.onBiometricAction(action);
- }
-
+ scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(
+ mBiometricStateCallback, new ClientMonitorCallback() {
@Override
public void onClientFinished(@NonNull BaseClientMonitor clientMonitor,
boolean success) {
- mBiometricStateCallback.onClientFinished(clientMonitor, success);
+ ClientMonitorCallback.super.onClientFinished(clientMonitor, success);
if (success) {
scheduleLoadAuthenticatorIdsForUser(sensorId, userId);
scheduleInvalidationRequest(sensorId, userId);
}
}
- });
+ }));
});
return id;
}
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 6795b6b4158d..45b0f0a6d04a 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -79,6 +79,7 @@ import android.net.NetworkScore;
import android.net.RouteInfo;
import android.net.UidRangeParcel;
import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
import android.net.VpnManager;
import android.net.VpnProfileState;
import android.net.VpnService;
@@ -226,6 +227,16 @@ public class Vpn {
private static final int VPN_DEFAULT_SCORE = 101;
/**
+ * The reset session timer for data stall. If a session has not successfully revalidated after
+ * the delay, the session will be torn down and restarted in an attempt to recover. Delay
+ * counter is reset on successful validation only.
+ *
+ * <p>If retries have exceeded the length of this array, the last entry in the array will be
+ * used as a repeating interval.
+ */
+ private static final long[] DATA_STALL_RESET_DELAYS_SEC = {30L, 60L, 120L, 240L, 480L, 960L};
+
+ /**
* The initial token value of IKE session.
*/
private static final int STARTING_TOKEN = -1;
@@ -271,6 +282,7 @@ public class Vpn {
private final UserManager mUserManager;
private final VpnProfileStore mVpnProfileStore;
+ protected boolean mDataStallSuspected = false;
@VisibleForTesting
VpnProfileStore getVpnProfileStore() {
@@ -522,12 +534,30 @@ public class Vpn {
@NonNull LinkProperties lp,
@NonNull NetworkScore score,
@NonNull NetworkAgentConfig config,
- @Nullable NetworkProvider provider) {
+ @Nullable NetworkProvider provider,
+ @Nullable ValidationStatusCallback callback) {
return new VpnNetworkAgentWrapper(
- context, looper, logTag, nc, lp, score, config, provider);
+ context, looper, logTag, nc, lp, score, config, provider, callback);
+ }
+
+ /**
+ * Get the length of time to wait before resetting the ike session when a data stall is
+ * suspected.
+ */
+ public long getDataStallResetSessionSeconds(int count) {
+ if (count >= DATA_STALL_RESET_DELAYS_SEC.length) {
+ return DATA_STALL_RESET_DELAYS_SEC[DATA_STALL_RESET_DELAYS_SEC.length - 1];
+ } else {
+ return DATA_STALL_RESET_DELAYS_SEC[count];
+ }
}
}
+ @VisibleForTesting
+ interface ValidationStatusCallback {
+ void onValidationStatus(int status);
+ }
+
public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd,
@UserIdInt int userId, VpnProfileStore vpnProfileStore) {
this(looper, context, new Dependencies(), netService, netd, userId, vpnProfileStore,
@@ -1460,6 +1490,11 @@ public class Vpn {
@GuardedBy("this")
private void agentConnect() {
+ agentConnect(null /* validationCallback */);
+ }
+
+ @GuardedBy("this")
+ private void agentConnect(@Nullable ValidationStatusCallback validationCallback) {
LinkProperties lp = makeLinkProperties();
// VPN either provide a default route (IPv4 or IPv6 or both), or they are a split tunnel
@@ -1507,7 +1542,7 @@ public class Vpn {
mNetworkAgent = mDeps.newNetworkAgent(mContext, mLooper, NETWORKTYPE /* logtag */,
mNetworkCapabilities, lp,
new NetworkScore.Builder().setLegacyInt(VPN_DEFAULT_SCORE).build(),
- networkAgentConfig, mNetworkProvider);
+ networkAgentConfig, mNetworkProvider, validationCallback);
final long token = Binder.clearCallingIdentity();
try {
mNetworkAgent.register();
@@ -2723,7 +2758,7 @@ public class Vpn {
@Nullable private ScheduledFuture<?> mScheduledHandleNetworkLostFuture;
@Nullable private ScheduledFuture<?> mScheduledHandleRetryIkeSessionFuture;
-
+ @Nullable private ScheduledFuture<?> mScheduledHandleDataStallFuture;
/** Signal to ensure shutdown is honored even if a new Network is connected. */
private boolean mIsRunning = true;
@@ -2750,6 +2785,14 @@ public class Vpn {
private boolean mMobikeEnabled = false;
/**
+ * The number of attempts to reset the IKE session since the last successful connection.
+ *
+ * <p>This variable controls the retry delay, and is reset when the VPN pass network
+ * validation.
+ */
+ private int mDataStallRetryCount = 0;
+
+ /**
* The number of attempts since the last successful connection.
*
* <p>This variable controls the retry delay, and is reset when a new IKE session is
@@ -2931,7 +2974,7 @@ public class Vpn {
if (isSettingsVpnLocked()) {
prepareStatusIntent();
}
- agentConnect();
+ agentConnect(this::onValidationStatus);
return; // Link properties are already sent.
} else {
// Underlying networks also set in agentConnect()
@@ -3200,18 +3243,52 @@ public class Vpn {
// Ignore stale runner.
if (mVpnRunner != Vpn.IkeV2VpnRunner.this) return;
- // Handle the report only for current VPN network.
+ // Handle the report only for current VPN network. If data stall is already
+ // reported, ignoring the other reports. It means that the stall is not
+ // recovered by MOBIKE and should be on the way to reset the ike session.
if (mNetworkAgent != null
- && mNetworkAgent.getNetwork().equals(report.getNetwork())) {
+ && mNetworkAgent.getNetwork().equals(report.getNetwork())
+ && !mDataStallSuspected) {
Log.d(TAG, "Data stall suspected");
// Trigger MOBIKE.
maybeMigrateIkeSession(mActiveNetwork);
+ mDataStallSuspected = true;
}
}
}
}
+ public void onValidationStatus(int status) {
+ if (status == NetworkAgent.VALIDATION_STATUS_VALID) {
+ // No data stall now. Reset it.
+ mExecutor.execute(() -> {
+ mDataStallSuspected = false;
+ mDataStallRetryCount = 0;
+ if (mScheduledHandleDataStallFuture != null) {
+ Log.d(TAG, "Recovered from stall. Cancel pending reset action.");
+ mScheduledHandleDataStallFuture.cancel(false /* mayInterruptIfRunning */);
+ mScheduledHandleDataStallFuture = null;
+ }
+ });
+ } else {
+ // Skip other invalid status if the scheduled recovery exists.
+ if (mScheduledHandleDataStallFuture != null) return;
+
+ mScheduledHandleDataStallFuture = mExecutor.schedule(() -> {
+ if (mDataStallSuspected) {
+ Log.d(TAG, "Reset session to recover stalled network");
+ // This will reset old state if it exists.
+ startIkeSession(mActiveNetwork);
+ }
+
+ // Reset mScheduledHandleDataStallFuture since it's already run on executor
+ // thread.
+ mScheduledHandleDataStallFuture = null;
+ }, mDeps.getDataStallResetSessionSeconds(mDataStallRetryCount++), TimeUnit.SECONDS);
+ }
+ }
+
/**
* Handles loss of the default underlying network
*
@@ -4339,6 +4416,7 @@ public class Vpn {
// un-finalized.
@VisibleForTesting
public static class VpnNetworkAgentWrapper extends NetworkAgent {
+ private final ValidationStatusCallback mCallback;
/** Create an VpnNetworkAgentWrapper */
public VpnNetworkAgentWrapper(
@NonNull Context context,
@@ -4348,8 +4426,10 @@ public class Vpn {
@NonNull LinkProperties lp,
@NonNull NetworkScore score,
@NonNull NetworkAgentConfig config,
- @Nullable NetworkProvider provider) {
+ @Nullable NetworkProvider provider,
+ @Nullable ValidationStatusCallback callback) {
super(context, looper, logTag, nc, lp, score, config, provider);
+ mCallback = callback;
}
/** Update the LinkProperties */
@@ -4371,6 +4451,13 @@ public class Vpn {
public void onNetworkUnwanted() {
// We are user controlled, not driven by NetworkRequest.
}
+
+ @Override
+ public void onValidationStatus(int status, Uri redirectUri) {
+ if (mCallback != null) {
+ mCallback.onValidationStatus(status);
+ }
+ }
}
/**
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 7e80b7d5b0ac..e907ebfa6471 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -127,6 +127,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.display.BrightnessSynchronizer;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.AnimationThread;
import com.android.server.DisplayThread;
@@ -151,6 +152,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
+
/**
* Manages attached displays.
* <p>
@@ -1900,6 +1902,14 @@ public final class DisplayManagerService extends SystemService {
if (displayDevice == null) {
return;
}
+ if (mLogicalDisplayMapper.getDisplayLocked(displayDevice)
+ .getDisplayInfoLocked().type == Display.TYPE_INTERNAL) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BRIGHTNESS_CONFIGURATION_UPDATED,
+ c.getCurve().first,
+ c.getCurve().second,
+ // should not be logged for virtual displays
+ uniqueId);
+ }
mPersistentDataStore.setBrightnessConfigurationForDisplayLocked(c, displayDevice,
userSerial, packageName);
} finally {
diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS
index 8534fabb5576..84324f2524fc 100644
--- a/services/core/java/com/android/server/pm/OWNERS
+++ b/services/core/java/com/android/server/pm/OWNERS
@@ -3,8 +3,6 @@ hackbod@google.com
jsharkey@android.com
jsharkey@google.com
narayan@google.com
-svetoslavganov@android.com
-svetoslavganov@google.com
include /PACKAGE_MANAGER_OWNERS
# apex support
@@ -26,16 +24,10 @@ per-file PackageManagerServiceCompilerMapping.java = file:dex/OWNERS
per-file PackageUsage.java = file:dex/OWNERS
# multi user / cross profile
-per-file CrossProfileAppsServiceImpl.java = omakoto@google.com, yamasani@google.com
-per-file CrossProfileAppsService.java = omakoto@google.com, yamasani@google.com
-per-file CrossProfileIntentFilter.java = omakoto@google.com, yamasani@google.com
-per-file CrossProfileIntentResolver.java = omakoto@google.com, yamasani@google.com
+per-file CrossProfile* = file:MULTIUSER_AND_ENTERPRISE_OWNERS
per-file RestrictionsSet.java = file:MULTIUSER_AND_ENTERPRISE_OWNERS
-per-file UserManager* = file:/MULTIUSER_OWNERS
per-file UserRestriction* = file:MULTIUSER_AND_ENTERPRISE_OWNERS
-per-file UserSystemPackageInstaller* = file:/MULTIUSER_OWNERS
-per-file UserTypeDetails.java = file:/MULTIUSER_OWNERS
-per-file UserTypeFactory.java = file:/MULTIUSER_OWNERS
+per-file User* = file:/MULTIUSER_OWNERS
# security
per-file KeySetHandle.java = cbrubaker@google.com, nnk@google.com
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index a6fac4d60fe7..c4e122d4497d 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4131,6 +4131,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_DEMO_APP_2:
case KeyEvent.KEYCODE_DEMO_APP_3:
case KeyEvent.KEYCODE_DEMO_APP_4: {
+ // TODO(b/254604589): Dispatch KeyEvent to System UI.
+ sendSystemKeyToStatusBarAsync(keyCode);
+
// Just drop if keys are not intercepted for direct key.
result &= ~ACTION_PASS_TO_USER;
break;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index ba414cb593ef..5b7b8f4ca21f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -33,6 +33,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import android.annotation.NonNull;
@@ -43,6 +44,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Bundle;
+import android.os.BundleMerger;
import android.os.HandlerThread;
import android.os.UserHandle;
import android.provider.Settings;
@@ -57,12 +59,15 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
import java.util.List;
@SmallTest
@RunWith(MockitoJUnitRunner.class)
public class BroadcastQueueModernImplTest {
private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID;
+ private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1;
@Mock ActivityManagerService mAms;
@Mock ProcessRecord mProcess;
@@ -87,6 +92,10 @@ public class BroadcastQueueModernImplTest {
mHandlerThread.start();
mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+ mConstants.DELAY_URGENT_MILLIS = -120_000;
+ mConstants.DELAY_NORMAL_MILLIS = 10_000;
+ mConstants.DELAY_CACHED_MILLIS = 120_000;
+
mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
mConstants, mConstants);
@@ -467,6 +476,62 @@ public class BroadcastQueueModernImplTest {
List.of(musicVolumeChanged, alarmVolumeChanged, timeTick));
}
+ /**
+ * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MERGED works as expected.
+ */
+ @Test
+ public void testDeliveryGroupPolicy_merged() {
+ final BundleMerger extrasMerger = new BundleMerger();
+ extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+ BundleMerger.STRATEGY_ARRAY_APPEND);
+
+ final Intent packageChangedForUid = createPackageChangedIntent(TEST_UID,
+ List.of("com.testuid.component1"));
+ final BroadcastOptions optionsPackageChangedForUid = BroadcastOptions.makeBasic();
+ optionsPackageChangedForUid.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+ optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID));
+ optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger);
+
+ final Intent secondPackageChangedForUid = createPackageChangedIntent(TEST_UID,
+ List.of("com.testuid.component2", "com.testuid.component3"));
+
+ final Intent packageChangedForUid2 = createPackageChangedIntent(TEST_UID2,
+ List.of("com.testuid2.component1"));
+ final BroadcastOptions optionsPackageChangedForUid2 = BroadcastOptions.makeBasic();
+ optionsPackageChangedForUid.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+ optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID2));
+ optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger);
+
+ // Halt all processing so that we get a consistent view
+ mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid,
+ optionsPackageChangedForUid));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid2,
+ optionsPackageChangedForUid2));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(secondPackageChangedForUid,
+ optionsPackageChangedForUid));
+
+ final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ final Intent expectedPackageChangedForUid = createPackageChangedIntent(TEST_UID,
+ List.of("com.testuid.component2", "com.testuid.component3",
+ "com.testuid.component1"));
+ // Verify that packageChangedForUid and secondPackageChangedForUid broadcasts
+ // have been merged.
+ verifyPendingRecords(queue, List.of(packageChangedForUid2, expectedPackageChangedForUid));
+ }
+
+ private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
+ final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+ packageChangedIntent.putExtra(Intent.EXTRA_UID, uid);
+ packageChangedIntent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+ componentNameList.toArray());
+ return packageChangedIntent;
+ }
+
private void verifyPendingRecords(BroadcastProcessQueue queue,
List<Intent> intents) {
for (int i = 0; i < intents.size(); i++) {
@@ -477,9 +542,45 @@ public class BroadcastQueueModernImplTest {
+ ", actual_extras=" + actualIntent.getExtras()
+ ", expected_extras=" + expectedIntent.getExtras();
assertTrue(errMsg, actualIntent.filterEquals(expectedIntent));
- assertTrue(errMsg, Bundle.kindofEquals(
- actualIntent.getExtras(), expectedIntent.getExtras()));
+ assertBundleEquals(expectedIntent.getExtras(), actualIntent.getExtras());
}
assertTrue(queue.isEmpty());
}
+
+ private void assertBundleEquals(Bundle expected, Bundle actual) {
+ final String errMsg = "expected=" + expected + ", actual=" + actual;
+ if (expected == actual) {
+ return;
+ } else if (expected == null || actual == null) {
+ fail(errMsg);
+ }
+ if (!expected.keySet().equals(actual.keySet())) {
+ fail(errMsg);
+ }
+ for (String key : expected.keySet()) {
+ final Object expectedValue = expected.get(key);
+ final Object actualValue = actual.get(key);
+ if (expectedValue == actualValue) {
+ continue;
+ } else if (expectedValue == null || actualValue == null) {
+ fail(errMsg);
+ }
+ assertEquals(errMsg, expectedValue.getClass(), actualValue.getClass());
+ if (expectedValue.getClass().isArray()) {
+ assertEquals(errMsg, Array.getLength(expectedValue), Array.getLength(actualValue));
+ for (int i = 0; i < Array.getLength(expectedValue); ++i) {
+ assertEquals(errMsg, Array.get(expectedValue, i), Array.get(actualValue, i));
+ }
+ } else if (expectedValue instanceof ArrayList) {
+ final ArrayList<?> expectedList = (ArrayList<?>) expectedValue;
+ final ArrayList<?> actualList = (ArrayList<?>) actualValue;
+ assertEquals(errMsg, expectedList.size(), actualList.size());
+ for (int i = 0; i < expectedList.size(); ++i) {
+ assertEquals(errMsg, expectedList.get(i), actualList.get(i));
+ }
+ } else {
+ assertEquals(errMsg, expectedValue, actualValue);
+ }
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index d9a26c68f3ed..e1a4c1dd7256 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -888,7 +888,7 @@ public class BroadcastQueueTest {
}) {
// Confirm expected OOM adjustments; we were invoked once to upgrade
// and once to downgrade
- assertEquals(ActivityManager.PROCESS_STATE_RECEIVER,
+ assertEquals(String.valueOf(receiverApp), ActivityManager.PROCESS_STATE_RECEIVER,
receiverApp.mState.getReportedProcState());
verify(mAms, times(2)).enqueueOomAdjTargetLocked(eq(receiverApp));
@@ -897,8 +897,8 @@ public class BroadcastQueueTest {
// cold-started apps to be thawed, but the modern stack does
} else {
// Confirm that app was thawed
- verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(receiverApp),
- eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER));
+ verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce()).unfreezeTemporarily(
+ eq(receiverApp), eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER));
// Confirm that we added package to process
verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName),
@@ -1599,4 +1599,39 @@ public class BroadcastQueueTest {
assertTrue(mQueue.isBeyondBarrierLocked(afterFirst));
assertTrue(mQueue.isBeyondBarrierLocked(afterSecond));
}
+
+ /**
+ * Verify that we OOM adjust for manifest receivers.
+ */
+ @Test
+ public void testOomAdjust_Manifest() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+ List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE),
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_RED))));
+
+ waitForIdle();
+ verify(mAms, atLeastOnce()).enqueueOomAdjTargetLocked(any());
+ }
+
+ /**
+ * Verify that we never OOM adjust for registered receivers.
+ */
+ @Test
+ public void testOomAdjust_Registered() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+ List.of(makeRegisteredReceiver(receiverApp),
+ makeRegisteredReceiver(receiverApp),
+ makeRegisteredReceiver(receiverApp))));
+
+ waitForIdle();
+ verify(mAms, never()).enqueueOomAdjTargetLocked(any());
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 0b776a3e6642..fe92a1dbdac1 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.UserHandle.USER_SYSTEM;
import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -246,7 +247,7 @@ public class UserControllerTest {
mUserController.setInitialConfig(/* userSwitchUiEnabled= */ false,
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
- mUserController.startUser(TEST_USER_ID, true /* foreground */);
+ mUserController.startUser(TEST_USER_ID, /* foreground= */ true);
verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
@@ -258,6 +259,8 @@ public class UserControllerTest {
assertFalse(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ true));
// Make sure no intents have been fired for pre-created users.
assertTrue(mInjector.mSentIntents.isEmpty());
+
+ verifyUserNeverAssignedToDisplay();
}
@Test
@@ -280,6 +283,8 @@ public class UserControllerTest {
// binder calls, but their side effects (in this case, that the user is stopped right away)
assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes())
.containsExactly(USER_START_MSG);
+
+ verifyUserAssignedToDisplay(TEST_PRE_CREATED_USER_ID, Display.DEFAULT_DISPLAY);
}
private void startUserAssertions(
@@ -303,6 +308,7 @@ public class UserControllerTest {
assertEquals("User must be in STATE_BOOTING", UserState.STATE_BOOTING, userState.state);
assertEquals("Unexpected old user id", 0, reportMsg.arg1);
assertEquals("Unexpected new user id", TEST_USER_ID, reportMsg.arg2);
+ verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY);
}
@Test
@@ -313,6 +319,8 @@ public class UserControllerTest {
mUserController.startUserInForeground(NONEXIST_USER_ID);
verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean());
verify(mInjector.getWindowManager()).setSwitchingUser(false);
+
+ verifyUserNeverAssignedToDisplay();
}
@Test
@@ -395,6 +403,7 @@ public class UserControllerTest {
verify(mInjector, times(0)).dismissKeyguard(any(), anyString());
verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
continueUserSwitchAssertions(TEST_USER_ID, false);
+ verifyOnUserStarting(USER_SYSTEM, /* visible= */ false);
}
@Test
@@ -403,7 +412,7 @@ public class UserControllerTest {
mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
// Start user -- this will update state of mUserController
- mUserController.startUser(TEST_USER_ID, true);
+ mUserController.startUser(TEST_USER_ID, /* foreground=*/ true);
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -415,6 +424,7 @@ public class UserControllerTest {
verify(mInjector, times(1)).dismissKeyguard(any(), anyString());
verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
continueUserSwitchAssertions(TEST_USER_ID, false);
+ verifyOnUserStarting(USER_SYSTEM, /* visible= */ false);
}
@Test
@@ -423,7 +433,7 @@ public class UserControllerTest {
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
// Start user -- this will update state of mUserController
- mUserController.startUser(TEST_USER_ID, true);
+ mUserController.startUser(TEST_USER_ID, /* foreground=*/ true);
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -521,6 +531,7 @@ public class UserControllerTest {
assertFalse(mUserController.canStartMoreUsers());
assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1, TEST_USER_ID2}),
mUserController.getRunningUsersLU());
+ verifyOnUserStarting(USER_SYSTEM, /* visible= */ false);
}
/**
@@ -530,7 +541,7 @@ public class UserControllerTest {
*/
@Test
public void testUserLockingFromUserSwitchingForMultipleUsersDelayedLockingMode()
- throws InterruptedException, RemoteException {
+ throws Exception {
mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true);
@@ -645,6 +656,8 @@ public class UserControllerTest {
setUpUser(TEST_USER_ID1, 0);
assertThrows(IllegalArgumentException.class,
() -> mUserController.startProfile(TEST_USER_ID1));
+
+ verifyUserNeverAssignedToDisplay();
}
@Test
@@ -660,6 +673,8 @@ public class UserControllerTest {
setUpUser(TEST_USER_ID1, UserInfo.FLAG_PROFILE | UserInfo.FLAG_DISABLED, /* preCreated= */
false, UserManager.USER_TYPE_PROFILE_MANAGED);
assertThat(mUserController.startProfile(TEST_USER_ID1)).isFalse();
+
+ verifyUserNeverAssignedToDisplay();
}
@Test
@@ -949,6 +964,10 @@ public class UserControllerTest {
verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplay(userId);
}
+ private void verifyOnUserStarting(@UserIdInt int userId, boolean visible) {
+ verify(mInjector).onUserStarting(userId, visible);
+ }
+
// Should be public to allow mocking
private static class TestInjector extends UserController.Injector {
public final TestHandler mHandler;
@@ -1084,6 +1103,11 @@ public class UserControllerTest {
protected LockPatternUtils getLockPatternUtils() {
return mLockPatternUtilsMock;
}
+
+ @Override
+ void onUserStarting(@UserIdInt int userId, boolean visible) {
+ Log.i(TAG, "onUserStarting(" + userId + ", " + visible + ")");
+ }
}
private static class TestHandler extends Handler {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 12b8264fc20c..41f743367aeb 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -39,6 +39,7 @@ import androidx.test.filters.SmallTest;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.HalClientMonitor;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -63,6 +64,8 @@ public class FaceProviderTest {
private IFace mDaemon;
@Mock
private BiometricContext mBiometricContext;
+ @Mock
+ private BiometricStateCallback mBiometricStateCallback;
private SensorProps[] mSensorProps;
private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -91,8 +94,8 @@ public class FaceProviderTest {
mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
- mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG,
- mLockoutResetDispatcher, mBiometricContext);
+ mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback,
+ mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext);
}
@SuppressWarnings("rawtypes")
@@ -140,11 +143,13 @@ public class FaceProviderTest {
TestableFaceProvider(@NonNull IFace daemon,
@NonNull Context context,
+ @NonNull BiometricStateCallback biometricStateCallback,
@NonNull SensorProps[] props,
@NonNull String halInstanceName,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull BiometricContext biometricContext) {
- super(context, props, halInstanceName, lockoutResetDispatcher, biometricContext);
+ super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
+ biometricContext);
mDaemon = daemon;
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
index 116d2d5a66a0..a2cade7ad797 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
@@ -43,6 +43,7 @@ import androidx.test.filters.SmallTest;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.sensors.BiometricScheduler;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import org.junit.Before;
@@ -73,6 +74,8 @@ public class Face10Test {
private BiometricScheduler mScheduler;
@Mock
private BiometricContext mBiometricContext;
+ @Mock
+ private BiometricStateCallback mBiometricStateCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -103,8 +106,8 @@ public class Face10Test {
resetLockoutRequiresChallenge);
Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST"));
- mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mHandler, mScheduler,
- mBiometricContext);
+ mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps,
+ mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext);
mBinder = new Binder();
}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 439eaa69e771..ef693b5278a0 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -3942,6 +3942,10 @@ public class SubscriptionManager {
* may provide one. Or, a carrier may decide to provide the phone number via source
* {@link #PHONE_NUMBER_SOURCE_CARRIER carrier} if neither source UICC nor IMS is available.
*
+ * <p>The availability and correctness of the phone number depends on the underlying source
+ * and the network etc. Additional verification is needed to use this number for
+ * security-related or other sensitive scenarios.
+ *
* @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
* for the default one.
* @param source the source of the phone number, one of the PHONE_NUMBER_SOURCE_* constants.
@@ -4175,18 +4179,18 @@ public class SubscriptionManager {
*/
@SystemApi
@RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
- public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
+ public void setSubscriptionUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
if (!isValidSubscriptionId(subscriptionId)) {
- throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: "
- + subscriptionId);
+ throw new IllegalArgumentException("[setSubscriptionUserHandle]: "
+ + "Invalid subscriptionId: " + subscriptionId);
}
try {
ISub iSub = TelephonyManager.getSubscriptionService();
if (iSub != null) {
- iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName());
+ iSub.setSubscriptionUserHandle(userHandle, subscriptionId);
} else {
- throw new IllegalStateException("[setUserHandle]: "
+ throw new IllegalStateException("[setSubscriptionUserHandle]: "
+ "subscription service unavailable");
}
} catch (RemoteException ex) {
@@ -4211,18 +4215,18 @@ public class SubscriptionManager {
*/
@SystemApi
@RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
- public @Nullable UserHandle getUserHandle(int subscriptionId) {
+ public @Nullable UserHandle getSubscriptionUserHandle(int subscriptionId) {
if (!isValidSubscriptionId(subscriptionId)) {
- throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: "
- + subscriptionId);
+ throw new IllegalArgumentException("[getSubscriptionUserHandle]: "
+ + "Invalid subscriptionId: " + subscriptionId);
}
try {
ISub iSub = TelephonyManager.getSubscriptionService();
if (iSub != null) {
- return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName());
+ return iSub.getSubscriptionUserHandle(subscriptionId);
} else {
- throw new IllegalStateException("[getUserHandle]: "
+ throw new IllegalStateException("[getSubscriptionUserHandle]: "
+ "subscription service unavailable");
}
} catch (RemoteException ex) {
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 0211a7f5c5c5..4752cca8bd6c 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -323,22 +323,20 @@ interface ISub {
*
* @param userHandle the user handle for this subscription
* @param subId the unique SubscriptionInfo index in database
- * @param callingPackage The package making the IPC.
*
* @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
* @throws IllegalArgumentException if subId is invalid.
*/
- int setUserHandle(in UserHandle userHandle, int subId, String callingPackage);
+ int setSubscriptionUserHandle(in UserHandle userHandle, int subId);
/**
* Get UserHandle for this subscription
*
* @param subId the unique SubscriptionInfo index in database
- * @param callingPackage the package making the IPC
* @return userHandle associated with this subscription.
*
- * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION
+ * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
* @throws IllegalArgumentException if subId is invalid.
*/
- UserHandle getUserHandle(int subId, String callingPackage);
+ UserHandle getSubscriptionUserHandle(int subId);
}