summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/content/pm/IShortcutService.aidl2
-rw-r--r--core/java/android/content/pm/PackageManager.java12
-rw-r--r--core/java/android/inputmethodservice/InputMethodService.java27
-rw-r--r--core/java/android/os/Binder.java25
-rw-r--r--core/java/android/provider/CallLog.java12
-rw-r--r--core/java/android/view/ViewRootImpl.java10
-rw-r--r--core/java/android/view/WindowManager.java15
-rw-r--r--core/java/android/window/TaskFragmentOperation.java47
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java7
-rw-r--r--core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java4
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl13
-rw-r--r--core/res/res/values/config.xml6
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--core/tests/coretests/src/android/os/AidlTest.java33
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java16
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java4
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java5
-rw-r--r--media/java/android/media/audiofx/Visualizer.java10
-rw-r--r--packages/SystemUI/Android.bp17
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt95
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt111
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt357
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt348
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt270
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt849
-rw-r--r--packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt10
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt20
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt70
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt167
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt437
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt145
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt336
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt16
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt3
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml23
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml23
-rw-r--r--packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml31
-rw-r--r--packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml31
-rw-r--r--packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml47
-rw-r--r--packages/SystemUI/res/drawable/ic_person_outline.xml26
-rw-r--r--packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml2
-rw-r--r--packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml2
-rw-r--r--packages/SystemUI/res/drawable/media_output_icon_volume.xml3
-rw-r--r--packages/SystemUI/res/drawable/media_output_title_icon_area.xml8
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml29
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml25
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml25
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml30
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml30
-rw-r--r--packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml3
-rw-r--r--packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml2
-rw-r--r--packages/SystemUI/res/layout/immersive_mode_cling.xml6
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_card_button.xml26
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_item_v2.xml89
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_v2.xml109
-rw-r--r--packages/SystemUI/res/values-ldrtl/dimens.xml20
-rw-r--r--packages/SystemUI/res/values/colors.xml3
-rw-r--r--packages/SystemUI/res/values/dimens.xml5
-rw-r--r--packages/SystemUI/res/values/ids.xml4
-rw-r--r--packages/SystemUI/res/values/strings.xml50
-rw-r--r--packages/SystemUI/res/values/styles.xml20
-rw-r--r--packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt8
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java3
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt85
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt196
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt81
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt168
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt148
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt177
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt196
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt157
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt327
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt288
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt79
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt108
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt150
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt664
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt65
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt367
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt539
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java63
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java38
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java32
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java45
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java590
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java167
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java54
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt113
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt111
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt167
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt91
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt34
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt263
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt277
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java78
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt92
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt223
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt72
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt47
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt191
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt323
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt530
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt68
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt127
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt226
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt69
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt49
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt825
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt322
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt85
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt52
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt87
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt90
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt245
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java148
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt (renamed from packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt)19
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt11
-rw-r--r--services/core/java/com/android/server/PersistentDataBlockService.java5
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java8
-rw-r--r--services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java16
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java15
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java5
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController.java6
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController2.java4
-rw-r--r--services/core/java/com/android/server/display/HighBrightnessModeController.java43
-rw-r--r--services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java3
-rw-r--r--services/core/java/com/android/server/display/TEST_MAPPING21
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java12
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java11
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java8
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java37
-rw-r--r--services/core/java/com/android/server/media/projection/mediaprojection.md5
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java51
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java58
-rw-r--r--services/core/java/com/android/server/pm/InstallPackageHelper.java49
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java12
-rw-r--r--services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java49
-rw-r--r--services/core/java/com/android/server/powerstats/PowerStatsLogger.java20
-rw-r--r--services/core/java/com/android/server/powerstats/PowerStatsService.java28
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java14
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java26
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java6
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java46
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java4
-rw-r--r--services/core/java/com/android/server/wm/Dimmer.java15
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java2
-rw-r--r--services/core/java/com/android/server/wm/DisplayPolicy.java88
-rw-r--r--services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java7
-rw-r--r--services/core/java/com/android/server/wm/LetterboxConfiguration.java29
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java25
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java11
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerShellCommand.java12
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java6
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java8
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp13
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java11
-rw-r--r--services/tests/displayservicetests/TEST_MAPPING10
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java6
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java7
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java60
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java265
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java5
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DimmerTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java19
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java21
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java7
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java38
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java30
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java7
-rw-r--r--telephony/java/android/telephony/SubscriptionManager.java9
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt13
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt152
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt8
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt12
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt86
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml8
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml8
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java4
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java32
-rw-r--r--tests/Input/src/com/android/test/input/MotionPredictorTest.kt2
292 files changed, 9878 insertions, 7785 deletions
diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl
index c9735b05cba4..86087cb0c0ef 100644
--- a/core/java/android/content/pm/IShortcutService.aidl
+++ b/core/java/android/content/pm/IShortcutService.aidl
@@ -61,7 +61,7 @@ interface IShortcutService {
void resetThrottling(); // system only API for developer opsions
- void onApplicationActive(String packageName, int userId); // system only API for sysUI
+ oneway void onApplicationActive(String packageName, int userId); // system only API for sysUI
byte[] getBackupPayload(int user);
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 66aadac6295d..9933c8be6015 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2354,6 +2354,7 @@ public abstract class PackageManager {
USER_MIN_ASPECT_RATIO_4_3,
USER_MIN_ASPECT_RATIO_16_9,
USER_MIN_ASPECT_RATIO_3_2,
+ USER_MIN_ASPECT_RATIO_FULLSCREEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface UserMinAspectRatio {}
@@ -2375,8 +2376,9 @@ public abstract class PackageManager {
/**
* Aspect ratio override code: user forces app to the aspect ratio of the device display size.
- * This will be the portrait aspect ratio of the device if the app is portrait or the landscape
- * aspect ratio of the device if the app is landscape.
+ * This will be the portrait aspect ratio of the device if the app has fixed portrait
+ * orientation or the landscape aspect ratio of the device if the app has fixed landscape
+ * orientation.
*
* @hide
*/
@@ -2400,6 +2402,12 @@ public abstract class PackageManager {
*/
public static final int USER_MIN_ASPECT_RATIO_3_2 = 5;
+ /**
+ * Aspect ratio override code: user forces app to fullscreen
+ * @hide
+ */
+ public static final int USER_MIN_ASPECT_RATIO_FULLSCREEN = 6;
+
/** @hide */
@IntDef(flag = true, prefix = { "DELETE_" }, value = {
DELETE_KEEP_DATA,
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 44fed677318c..7b5dd55f385b 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -807,9 +807,11 @@ public class InputMethodService extends AbstractInputMethodService {
onUnbindInput();
mInputBinding = null;
mInputConnection = null;
- // free-up cached InkWindow surface on detaching from current client.
+
if (mInkWindow != null) {
- removeHandwritingInkWindow();
+ finishStylusHandwriting();
+ // free-up InkWindow surface after timeout.
+ scheduleStylusWindowIdleTimeout();
}
}
@@ -1020,6 +1022,7 @@ public class InputMethodService extends AbstractInputMethodService {
mOnPreparedStylusHwCalled = true;
}
if (onStartStylusHandwriting()) {
+ cancelStylusWindowIdleTimeout();
mPrivOps.onStylusHandwritingReady(requestId, Process.myPid());
} else {
Log.i(TAG, "IME is not ready. Can't start Stylus Handwriting");
@@ -1109,7 +1112,7 @@ public class InputMethodService extends AbstractInputMethodService {
*/
@Override
public void removeStylusHandwritingWindow() {
- InputMethodService.this.removeStylusHandwritingWindow();
+ InputMethodService.this.finishAndRemoveStylusHandwritingWindow();
}
/**
@@ -2667,21 +2670,15 @@ public class InputMethodService extends AbstractInputMethodService {
* Typically, this is called when {@link InkWindow} should no longer be holding a surface in
* memory.
*/
- private void removeStylusHandwritingWindow() {
+ private void finishAndRemoveStylusHandwritingWindow() {
+ cancelStylusWindowIdleTimeout();
+ mOnPreparedStylusHwCalled = false;
+ mStylusWindowIdleTimeoutRunnable = null;
if (mInkWindow != null) {
if (mHandwritingRequestId.isPresent()) {
// if handwriting session is still ongoing. This shouldn't happen.
finishStylusHandwriting();
}
- removeHandwritingInkWindow();
- }
- }
-
- private void removeHandwritingInkWindow() {
- cancelStylusWindowIdleTimeout();
- mOnPreparedStylusHwCalled = false;
- mStylusWindowIdleTimeoutRunnable = null;
- if (mInkWindow != null) {
mInkWindow.hide(true /* remove */);
mInkWindow.destroy();
mInkWindow = null;
@@ -2707,7 +2704,7 @@ public class InputMethodService extends AbstractInputMethodService {
private Runnable getStylusWindowIdleTimeoutRunnable() {
if (mStylusWindowIdleTimeoutRunnable == null) {
mStylusWindowIdleTimeoutRunnable = () -> {
- removeHandwritingInkWindow();
+ finishAndRemoveStylusHandwritingWindow();
mStylusWindowIdleTimeoutRunnable = null;
};
}
@@ -2986,8 +2983,6 @@ public class InputMethodService extends AbstractInputMethodService {
ImeTracing.getInstance().triggerServiceDump(
"InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
null /* icProto */);
- ImeTracker.forLogging().onProgress(mCurStatsToken,
- ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
mPrivOps.applyImeVisibilityAsync(setVisible
? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
}
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 00676f3cb746..01e8fea1019d 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -926,16 +926,19 @@ public class Binder implements IBinder {
* @hide
*/
@VisibleForTesting
- public final @NonNull String getTransactionTraceName(int transactionCode) {
+ public final @Nullable String getTransactionTraceName(int transactionCode) {
+ final boolean isInterfaceUserDefined = getMaxTransactionId() == 0;
if (mTransactionTraceNames == null) {
- final int highestId = Math.min(getMaxTransactionId(), TRANSACTION_TRACE_NAME_ID_LIMIT);
+ final int highestId = isInterfaceUserDefined ? TRANSACTION_TRACE_NAME_ID_LIMIT
+ : Math.min(getMaxTransactionId(), TRANSACTION_TRACE_NAME_ID_LIMIT);
mSimpleDescriptor = getSimpleDescriptor();
mTransactionTraceNames = new AtomicReferenceArray(highestId + 1);
}
- final int index = transactionCode - FIRST_CALL_TRANSACTION;
- if (index < 0 || index >= mTransactionTraceNames.length()) {
- return mSimpleDescriptor + "#" + transactionCode;
+ final int index = isInterfaceUserDefined
+ ? transactionCode : transactionCode - FIRST_CALL_TRANSACTION;
+ if (index >= mTransactionTraceNames.length() || index < 0) {
+ return null;
}
String transactionTraceName = mTransactionTraceNames.getAcquire(index);
@@ -1300,19 +1303,9 @@ public class Binder implements IBinder {
final boolean hasFullyQualifiedName = getMaxTransactionId() > 0;
final String transactionTraceName;
- if (tagEnabled && hasFullyQualifiedName) {
+ if (tagEnabled) {
// If tracing enabled and we have a fully qualified name, fetch the name
transactionTraceName = getTransactionTraceName(code);
- } else if (tagEnabled && isStackTrackingEnabled()) {
- // If tracing is enabled and we *don't* have a fully qualified name, fetch the
- // 'best effort' name only for stack tracking. This works around noticeable perf impact
- // on low latency binder calls (<100us). The tracing call itself is between (1-10us) and
- // the perf impact can be quite noticeable while benchmarking such binder calls.
- // The primary culprits are ContentProviders and Cursors which convenienty don't
- // autogenerate their AIDL and hence will not have a fully qualified name.
- //
- // TODO(b/253426478): Relax this constraint after a more robust fix
- transactionTraceName = getTransactionTraceName(code);
} else {
transactionTraceName = null;
}
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index ac6b2b23cdf5..3c5757dd5615 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -1985,13 +1985,14 @@ public class CallLog {
Log.w(LOG_TAG, "Failed to insert into call log; null result uri.");
}
+ int numDeleted;
if (values.containsKey(PHONE_ACCOUNT_ID)
&& !TextUtils.isEmpty(values.getAsString(PHONE_ACCOUNT_ID))
&& values.containsKey(PHONE_ACCOUNT_COMPONENT_NAME)
&& !TextUtils.isEmpty(values.getAsString(PHONE_ACCOUNT_COMPONENT_NAME))) {
// Only purge entries for the same phone account.
- resolver.delete(uri, "_id IN " +
- "(SELECT _id FROM calls"
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls"
+ " WHERE " + PHONE_ACCOUNT_COMPONENT_NAME + " = ?"
+ " AND " + PHONE_ACCOUNT_ID + " = ?"
+ " ORDER BY " + DEFAULT_SORT_ORDER
@@ -2001,14 +2002,15 @@ public class CallLog {
});
} else {
// No valid phone account specified, so default to the old behavior.
- resolver.delete(uri, "_id IN " +
- "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ " LIMIT -1 OFFSET 500)", null);
}
+ Log.i(LOG_TAG, "addEntry: cleaned up " + numDeleted + " old entries");
return result;
} catch (IllegalArgumentException e) {
- Log.w(LOG_TAG, "Failed to insert calllog", e);
+ Log.e(LOG_TAG, "Failed to insert calllog", e);
// Even though we make sure the target user is running and decrypted before calling
// this method, there's a chance that the user just got shut down, in which case
// we'll still get "IllegalArgumentException: Unknown URL content://call_log/calls".
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9d2bb58cdf0f..2f12fecb7fd0 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -311,6 +311,16 @@ public final class ViewRootImpl implements ViewParent,
SystemProperties.getBoolean("persist.wm.debug.client_transient", false);
/**
+ * Whether the client (system UI) is handling the immersive confirmation window. If
+ * {@link CLIENT_TRANSIENT} is set to true, the immersive confirmation window will always be the
+ * client instance and this flag will be ignored. Otherwise, the immersive confirmation window
+ * can be switched freely by this flag.
+ * @hide
+ */
+ public static final boolean CLIENT_IMMERSIVE_CONFIRMATION =
+ SystemProperties.getBoolean("persist.wm.debug.client_immersive_confirmation", false);
+
+ /**
* Whether the client should compute the window frame on its own.
* @hide
*/
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index d5f2aa3b3631..65677cd7c3fd 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3099,6 +3099,16 @@ public interface WindowManager extends ViewManager {
public static final int PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE = 1 << 16;
/**
+ * Flag to indicate that this window is a immersive mode confirmation window. The window
+ * should be ignored when calculating insets control. This is used for prompt window
+ * triggered by insets visibility changes. If it can take over the insets control, the
+ * visibility will change unexpectedly and the window may dismiss itself. Power button panic
+ * handling will be disabled when this window exists.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW = 1 << 17;
+
+ /**
* Flag to indicate that any window added by an application process that is of type
* {@link #TYPE_TOAST} or that requires
* {@link android.app.AppOpsManager#OP_SYSTEM_ALERT_WINDOW} permission should be hidden when
@@ -3242,6 +3252,7 @@ public interface WindowManager extends ViewManager {
PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME,
PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS,
PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
+ PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
@@ -3326,6 +3337,10 @@ public interface WindowManager extends ViewManager {
equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
name = "SUSTAINED_PERFORMANCE_MODE"),
@ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
+ equals = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
+ name = "IMMERSIVE_CONFIRMATION_WINDOW"),
+ @ViewDebug.FlagToString(
mask = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
equals = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"),
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index e153bb70a7ca..43fa0be6c1b7 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -80,6 +80,14 @@ public final class TaskFragmentOperation implements Parcelable {
*/
public static final int OP_TYPE_REORDER_TO_FRONT = 10;
+ /**
+ * Sets the activity navigation to be isolated, where the activity navigation on the
+ * TaskFragment is separated from the rest activities in the Task. Activities cannot be
+ * started on an isolated TaskFragment unless the activities are launched from the same
+ * TaskFragment or explicitly requested to.
+ */
+ public static final int OP_TYPE_SET_ISOLATED_NAVIGATION = 11;
+
@IntDef(prefix = { "OP_TYPE_" }, value = {
OP_TYPE_UNKNOWN,
OP_TYPE_CREATE_TASK_FRAGMENT,
@@ -92,7 +100,8 @@ public final class TaskFragmentOperation implements Parcelable {
OP_TYPE_SET_COMPANION_TASK_FRAGMENT,
OP_TYPE_SET_ANIMATION_PARAMS,
OP_TYPE_SET_RELATIVE_BOUNDS,
- OP_TYPE_REORDER_TO_FRONT
+ OP_TYPE_REORDER_TO_FRONT,
+ OP_TYPE_SET_ISOLATED_NAVIGATION
})
@Retention(RetentionPolicy.SOURCE)
public @interface OperationType {}
@@ -118,11 +127,14 @@ public final class TaskFragmentOperation implements Parcelable {
@Nullable
private final TaskFragmentAnimationParams mAnimationParams;
+ private final boolean mIsolatedNav;
+
private TaskFragmentOperation(@OperationType int opType,
@Nullable TaskFragmentCreationParams taskFragmentCreationParams,
@Nullable IBinder activityToken, @Nullable Intent activityIntent,
@Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken,
- @Nullable TaskFragmentAnimationParams animationParams) {
+ @Nullable TaskFragmentAnimationParams animationParams,
+ boolean isolatedNav) {
mOpType = opType;
mTaskFragmentCreationParams = taskFragmentCreationParams;
mActivityToken = activityToken;
@@ -130,6 +142,7 @@ public final class TaskFragmentOperation implements Parcelable {
mBundle = bundle;
mSecondaryFragmentToken = secondaryFragmentToken;
mAnimationParams = animationParams;
+ mIsolatedNav = isolatedNav;
}
private TaskFragmentOperation(Parcel in) {
@@ -140,6 +153,7 @@ public final class TaskFragmentOperation implements Parcelable {
mBundle = in.readBundle(getClass().getClassLoader());
mSecondaryFragmentToken = in.readStrongBinder();
mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
+ mIsolatedNav = in.readBoolean();
}
@Override
@@ -151,6 +165,7 @@ public final class TaskFragmentOperation implements Parcelable {
dest.writeBundle(mBundle);
dest.writeStrongBinder(mSecondaryFragmentToken);
dest.writeTypedObject(mAnimationParams, flags);
+ dest.writeBoolean(mIsolatedNav);
}
@NonNull
@@ -223,6 +238,14 @@ public final class TaskFragmentOperation implements Parcelable {
return mAnimationParams;
}
+ /**
+ * Returns whether the activity navigation on this TaskFragment is isolated. This is only
+ * useful when the op type is {@link OP_TYPE_SET_ISOLATED_NAVIGATION}.
+ */
+ public boolean isIsolatedNav() {
+ return mIsolatedNav;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -245,6 +268,7 @@ public final class TaskFragmentOperation implements Parcelable {
if (mAnimationParams != null) {
sb.append(", animationParams=").append(mAnimationParams);
}
+ sb.append(", isolatedNav=").append(mIsolatedNav);
sb.append('}');
return sb.toString();
@@ -253,7 +277,7 @@ public final class TaskFragmentOperation implements Parcelable {
@Override
public int hashCode() {
return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
- mBundle, mSecondaryFragmentToken, mAnimationParams);
+ mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav);
}
@Override
@@ -268,7 +292,8 @@ public final class TaskFragmentOperation implements Parcelable {
&& Objects.equals(mActivityIntent, other.mActivityIntent)
&& Objects.equals(mBundle, other.mBundle)
&& Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken)
- && Objects.equals(mAnimationParams, other.mAnimationParams);
+ && Objects.equals(mAnimationParams, other.mAnimationParams)
+ && mIsolatedNav == other.mIsolatedNav;
}
@Override
@@ -300,6 +325,8 @@ public final class TaskFragmentOperation implements Parcelable {
@Nullable
private TaskFragmentAnimationParams mAnimationParams;
+ private boolean mIsolatedNav;
+
/**
* @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
*/
@@ -363,12 +390,22 @@ public final class TaskFragmentOperation implements Parcelable {
}
/**
+ * Sets the activity navigation of this TaskFragment to be isolated.
+ */
+ @NonNull
+ public Builder setIsolatedNav(boolean isolatedNav) {
+ mIsolatedNav = isolatedNav;
+ return this;
+ }
+
+ /**
* Constructs the {@link TaskFragmentOperation}.
*/
@NonNull
public TaskFragmentOperation build() {
return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken,
- mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams);
+ mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams,
+ mIsolatedNav);
}
}
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 50f393b53277..2445daf89b64 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -31,6 +31,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERS
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.PermissionChecker.PID_UNKNOWN;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
@@ -356,6 +357,12 @@ public class ResolverActivity extends Activity implements
// flag set, we are now losing it. That should be a very rare case
// and we can live with this.
intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
+ intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ }
return intent;
}
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 8a5c7ef18621..30ebbe2bb111 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -386,8 +386,12 @@ public final class InputMethodPrivilegedOperations {
@Nullable ImeTracker.Token statsToken) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
return;
}
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
try {
ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
} catch (RemoteException e) {
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index d2564fb9c268..c6f5086b8346 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -63,6 +63,19 @@ oneway interface IStatusBar
void cancelPreloadRecentApps();
void showScreenPinningRequest(int taskId);
+ /**
+ * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed
+ * status should be saved without user clicking on the button. This could happen when a user
+ * swipe on the edge with the confirmation prompt showing.
+ */
+ void confirmImmersivePrompt();
+
+ /**
+ * Notify system UI the immersive mode changed. This shall be removed when client immersive is
+ * enabled.
+ */
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2a5cf268134d..d1cca395597b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2694,6 +2694,9 @@
backlight values -->
<bool name="config_displayBrightnessBucketsInDoze">false</bool>
+ <!-- True to skip the fade animation on display off event -->
+ <bool name="config_displayColorFadeDisabled">false</bool>
+
<!-- Power Management: Specifies whether to decouple the auto-suspend state of the
device from the display on/off state.
@@ -5727,6 +5730,9 @@
<!-- Whether per-app user aspect ratio override settings is enabled -->
<bool name="config_appCompatUserAppAspectRatioSettingsIsEnabled">false</bool>
+ <!-- Whether per-app fullscreen override option is allowed in user aspect ratio settings -->
+ <bool name="config_appCompatUserAppAspectRatioFullscreenIsEnabled">false</bool>
+
<!-- Whether sending compat fake focus for split screen resumed activities is enabled.
Needed because some game engines wait to get focus before drawing the content of
the app which isn't guaranteed by default in multi-window modes. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index af203d3e566a..08c404b40ccc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3855,6 +3855,7 @@
<java-symbol type="bool" name="config_dozeSupportsAodWallpaper" />
<java-symbol type="bool" name="config_displayBlanksAfterDoze" />
<java-symbol type="bool" name="config_displayBrightnessBucketsInDoze" />
+ <java-symbol type="bool" name="config_displayColorFadeDisabled" />
<java-symbol type="integer" name="config_storageManagerDaystoRetainDefault" />
<java-symbol type="string" name="config_headlineFontFamily" />
<java-symbol type="string" name="config_headlineFontFamilyMedium" />
@@ -4536,6 +4537,7 @@
<!-- Whether per-app user aspect ratio override settings is enabled -->
<java-symbol type="bool" name="config_appCompatUserAppAspectRatioSettingsIsEnabled" />
+ <java-symbol type="bool" name="config_appCompatUserAppAspectRatioFullscreenIsEnabled" />
<java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
diff --git a/core/tests/coretests/src/android/os/AidlTest.java b/core/tests/coretests/src/android/os/AidlTest.java
index 5f54b093e5e5..d0c3470c4c1f 100644
--- a/core/tests/coretests/src/android/os/AidlTest.java
+++ b/core/tests/coretests/src/android/os/AidlTest.java
@@ -28,12 +28,14 @@ public class AidlTest extends TestCase {
private IAidlTest mRemote;
private AidlObject mLocal;
+ private NonAutoGeneratedObject mNonAutoGenerated;
@Override
protected void setUp() throws Exception {
super.setUp();
mLocal = new AidlObject();
mRemote = IAidlTest.Stub.asInterface(mLocal);
+ mNonAutoGenerated = new NonAutoGeneratedObject("NonAutoGeneratedObject");
}
private static boolean check(TestParcelable p, int n, String s) {
@@ -84,6 +86,12 @@ public class AidlTest extends TestCase {
}
}
+ private static class NonAutoGeneratedObject extends Binder {
+ NonAutoGeneratedObject(String descriptor) {
+ super(descriptor);
+ }
+ }
+
private static class AidlObject extends IAidlTest.Stub {
public IInterface queryLocalInterface(String descriptor) {
// overriding this to return null makes asInterface always
@@ -194,7 +202,7 @@ public class AidlTest extends TestCase {
TestParcelable[] a1, TestParcelable[] a2) {
return null;
}
-
+
public void voidSecurityException() {
throw new SecurityException("gotcha!");
}
@@ -396,7 +404,7 @@ public class AidlTest extends TestCase {
assertEquals("s2[1]", s2[1]);
assertEquals("s2[2]", s2[2]);
}
-
+
@SmallTest
public void testVoidSecurityException() throws Exception {
boolean good = false;
@@ -407,7 +415,7 @@ public class AidlTest extends TestCase {
}
assertEquals(good, true);
}
-
+
@SmallTest
public void testIntSecurityException() throws Exception {
boolean good = false;
@@ -420,7 +428,7 @@ public class AidlTest extends TestCase {
}
@SmallTest
- public void testGetTransactionName() throws Exception {
+ public void testGetTransactionNameAutoGenerated() throws Exception {
assertEquals(15, mLocal.getMaxTransactionId());
assertEquals("booleanArray",
@@ -430,12 +438,21 @@ public class AidlTest extends TestCase {
assertEquals("parcelableIn",
mLocal.getTransactionName(IAidlTest.Stub.TRANSACTION_parcelableIn));
- assertEquals("IAidlTest:booleanArray",
+ assertEquals("AIDL::java::IAidlTest::booleanArray::server",
mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_booleanArray));
- assertEquals("IAidlTest:voidSecurityException",
+ assertEquals("AIDL::java::IAidlTest::voidSecurityException::server",
mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_voidSecurityException));
- assertEquals("IAidlTest:parcelableIn",
+ assertEquals("AIDL::java::IAidlTest::parcelableIn::server",
mLocal.getTransactionTraceName(IAidlTest.Stub.TRANSACTION_parcelableIn));
}
-}
+ @SmallTest
+ public void testGetTransactionNameNonAutoGenerated() throws Exception {
+ assertEquals(0, mNonAutoGenerated.getMaxTransactionId());
+
+ assertEquals("AIDL::java::NonAutoGeneratedObject::#0::server",
+ mNonAutoGenerated.getTransactionTraceName(0));
+ assertEquals("AIDL::java::NonAutoGeneratedObject::#1::server",
+ mNonAutoGenerated.getTransactionTraceName(1));
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index d94e8e426c4b..4d73c20fe39f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -17,7 +17,9 @@
package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -340,6 +342,20 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.deleteTaskFragment(fragmentToken);
}
+ void reorderTaskFragmentToFront(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REORDER_TO_FRONT).build();
+ wct.addTaskFragmentOperation(fragmentToken, operation);
+ }
+
+ void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, boolean isolatedNav) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build();
+ wct.addTaskFragmentOperation(fragmentToken, operation);
+ }
+
void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index a2f75e099465..f95f3ffb4df3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -250,6 +250,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Updates the Split
final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
final WindowContainerTransaction wct = transactionRecord.getTransaction();
+
+ mPresenter.setTaskFragmentIsolatedNavigation(wct,
+ splitPinContainer.getSecondaryContainer().getTaskFragmentToken(),
+ true /* isolatedNav */);
mPresenter.updateSplitContainer(splitPinContainer, wct);
transactionRecord.apply(false /* shouldApplyIndependently */);
updateCallbackIfNecessary();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 4dafbd17f379..5de6acfcc9db 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -17,7 +17,6 @@
package androidx.window.extensions.embedding;
import static android.content.pm.PackageManager.MATCH_ALL;
-import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
import android.app.Activity;
import android.app.ActivityThread;
@@ -40,7 +39,6 @@ import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentCreationParams;
-import android.window.TaskFragmentOperation;
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
@@ -427,10 +425,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
final SplitPinContainer pinnedContainer =
container.getTaskContainer().getSplitPinContainer();
if (pinnedContainer != null) {
- final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
- OP_TYPE_REORDER_TO_FRONT).build();
- wct.addTaskFragmentOperation(
- pinnedContainer.getSecondaryContainer().getTaskFragmentToken(), operation);
+ reorderTaskFragmentToFront(wct,
+ pinnedContainer.getSecondaryContainer().getTaskFragmentToken());
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 39f861de1ba0..5cf9175073c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -50,7 +50,7 @@ class ActivityEmbeddingAnimationAdapter {
final SurfaceControl mLeash;
/** Area in absolute coordinate that the animation surface shouldn't go beyond. */
@NonNull
- private final Rect mWholeAnimationBounds = new Rect();
+ final Rect mWholeAnimationBounds = new Rect();
/**
* Area in absolute coordinate that should represent all the content to show for this window.
* This should be the end bounds for opening window, and start bounds for closing window in case
@@ -229,20 +229,7 @@ class ActivityEmbeddingAnimationAdapter {
mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y);
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
t.setAlpha(mLeash, mTransformation.getAlpha());
-
- // The following applies an inverse scale to the clip-rect so that it crops "after" the
- // scale instead of before.
- mVecs[1] = mVecs[2] = 0;
- mVecs[0] = mVecs[3] = 1;
- mTransformation.getMatrix().mapVectors(mVecs);
- mVecs[0] = 1.f / mVecs[0];
- mVecs[3] = 1.f / mVecs[3];
- final Rect clipRect = mTransformation.getClipRect();
- mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
- mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
- mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
- mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
- t.setCrop(mLeash, mRect);
+ t.setWindowCrop(mLeash, mWholeAnimationBounds.width(), mWholeAnimationBounds.height());
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 1793a3d0feb4..4640106b5f1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -26,7 +26,6 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
-import android.view.animation.ClipRectAnimation;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.ScaleAnimation;
@@ -189,14 +188,6 @@ class ActivityEmbeddingAnimationSpec {
startBounds.top - endBounds.top, 0);
endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
endSet.addAnimation(endTranslate);
- // The end leash is resizing, we should update the window crop based on the clip rect.
- final Rect startClip = new Rect(startBounds);
- final Rect endClip = new Rect(endBounds);
- startClip.offsetTo(0, 0);
- endClip.offsetTo(0, 0);
- final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
- clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
- endSet.addAnimation(clipAnim);
endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
parentBounds.height());
endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 65727b6145e4..51e7be0f8a24 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -939,6 +939,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb
* Sets both shelf visibility and its height.
*/
private void setShelfHeight(boolean visible, int height) {
+ if (mEnablePipKeepClearAlgorithm) {
+ // turn this into Launcher keep clear area registration instead
+ setLauncherKeepClearAreaHeight(visible, height);
+ return;
+ }
if (!mIsKeyguardShowingOrAnimating) {
setShelfHeightLocked(visible, height);
}
diff --git a/media/java/android/media/audiofx/Visualizer.java b/media/java/android/media/audiofx/Visualizer.java
index f3dfeb665cc7..2795cfe4ba61 100644
--- a/media/java/android/media/audiofx/Visualizer.java
+++ b/media/java/android/media/audiofx/Visualizer.java
@@ -455,11 +455,13 @@ public class Visualizer {
* a number of consecutive 8-bit (unsigned) mono PCM samples equal to the capture size returned
* by {@link #getCaptureSize()}.
* <p>This method must be called when the Visualizer is enabled.
- * @param waveform array of bytes where the waveform should be returned
+ * @param waveform array of bytes where the waveform should be returned, array length must be
+ * at least equals to the capture size returned by {@link #getCaptureSize()}.
* @return {@link #SUCCESS} in case of success,
* {@link #ERROR_NO_MEMORY}, {@link #ERROR_INVALID_OPERATION} or {@link #ERROR_DEAD_OBJECT}
* in case of failure.
* @throws IllegalStateException
+ * @throws IllegalArgumentException
*/
public int getWaveForm(byte[] waveform)
throws IllegalStateException {
@@ -467,6 +469,12 @@ public class Visualizer {
if (mState != STATE_ENABLED) {
throw(new IllegalStateException("getWaveForm() called in wrong state: "+mState));
}
+ int captureSize = getCaptureSize();
+ if (captureSize > waveform.length) {
+ throw(new IllegalArgumentException("getWaveForm() called with illegal size: "
+ + waveform.length + " expecting at least "
+ + captureSize + " bytes"));
+ }
return native_getWaveForm(waveform);
}
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 7be60431b91b..e2599a3583c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -228,6 +228,17 @@ filegroup {
}
filegroup {
+ name: "SystemUI-test-fakes",
+ srcs: [
+ /* Status bar fakes */
+ "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt",
+ ],
+ path: "tests/src",
+}
+
+filegroup {
name: "SystemUI-tests-robolectric-pilots",
srcs: [
/* Keyguard converted tests */
@@ -291,6 +302,11 @@ filegroup {
"tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt",
"tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt",
"tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt",
+
+ /* Status bar wifi converted tests */
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt",
],
path: "tests/src",
}
@@ -449,6 +465,7 @@ android_robolectric_test {
"tests/robolectric/src/**/*.kt",
"tests/robolectric/src/**/*.java",
":SystemUI-tests-utils",
+ ":SystemUI-test-fakes",
":SystemUI-tests-robolectric-pilots",
],
static_libs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index dbfa192f5ec4..37b1ee543e46 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -32,11 +32,10 @@ import android.view.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-import android.widget.FrameLayout
import com.android.app.animation.Interpolators
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CujType
-import com.android.systemui.animation.view.LaunchableFrameLayout
+import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked
import kotlin.math.roundToInt
@@ -622,96 +621,12 @@ private class AnimatedDialog(
viewGroupWithBackground
} else {
- // We will make the dialog window (and therefore its DecorView) fullscreen to make
- // it possible to animate outside its bounds.
- //
- // Before that, we add a new View as a child of the DecorView with the same size and
- // gravity as that DecorView, then we add all original children of the DecorView to
- // that new View. Finally we remove the background of the DecorView and add it to
- // the new View, then we make the DecorView fullscreen. This new View now acts as a
- // fake (non fullscreen) window.
- //
- // On top of that, we also add a fullscreen transparent background between the
- // DecorView and the view that we added so that we can dismiss the dialog when this
- // view is clicked. This is necessary because DecorView overrides onTouchEvent and
- // therefore we can't set the click listener directly on the (now fullscreen)
- // DecorView.
- val fullscreenTransparentBackground = FrameLayout(dialog.context)
- decorView.addView(
- fullscreenTransparentBackground,
- 0 /* index */,
- FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- )
-
- val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
- dialogContentWithBackground.background = decorView.background
-
- // Make the window background transparent. Note that setting the window (or
- // DecorView) background drawable to null leads to issues with background color (not
- // being transparent) or with insets that are not refreshed. Therefore we need to
- // set it to something not null, hence we are using android.R.color.transparent
- // here.
- window.setBackgroundDrawableResource(android.R.color.transparent)
-
- // Close the dialog when clicking outside of it.
- fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
- dialogContentWithBackground.isClickable = true
-
- // Make sure the transparent and dialog backgrounds are not focusable by
- // accessibility
- // features.
- fullscreenTransparentBackground.importantForAccessibility =
- View.IMPORTANT_FOR_ACCESSIBILITY_NO
- dialogContentWithBackground.importantForAccessibility =
- View.IMPORTANT_FOR_ACCESSIBILITY_NO
-
- fullscreenTransparentBackground.addView(
- dialogContentWithBackground,
- FrameLayout.LayoutParams(
- window.attributes.width,
- window.attributes.height,
- window.attributes.gravity
- )
- )
-
- // Move all original children of the DecorView to the new View we just added.
- for (i in 1 until decorView.childCount) {
- val view = decorView.getChildAt(1)
- decorView.removeViewAt(1)
- dialogContentWithBackground.addView(view)
- }
-
- // Make the window fullscreen and add a layout listener to ensure it stays
- // fullscreen.
- window.setLayout(MATCH_PARENT, MATCH_PARENT)
- decorViewLayoutListener =
- View.OnLayoutChangeListener {
- v,
- left,
- top,
- right,
- bottom,
- oldLeft,
- oldTop,
- oldRight,
- oldBottom ->
- if (
- window.attributes.width != MATCH_PARENT ||
- window.attributes.height != MATCH_PARENT
- ) {
- // The dialog size changed, copy its size to dialogContentWithBackground
- // and make the dialog window full screen again.
- val layoutParams = dialogContentWithBackground.layoutParams
- layoutParams.width = window.attributes.width
- layoutParams.height = window.attributes.height
- dialogContentWithBackground.layoutParams = layoutParams
- window.setLayout(MATCH_PARENT, MATCH_PARENT)
- }
- }
- decorView.addOnLayoutChangeListener(decorViewLayoutListener)
-
+ val (dialogContentWithBackground, decorViewLayoutListener) =
+ dialog.maybeForceFullscreen()!!
+ this.decorViewLayoutListener = decorViewLayoutListener
dialogContentWithBackground
}
+
this.dialogContentWithBackground = dialogContentWithBackground
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
index 428856dc5f30..0f63548b6f0c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
@@ -18,6 +18,9 @@ package com.android.systemui.util
import android.app.Dialog
import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
import android.window.OnBackInvokedDispatcher
import com.android.systemui.animation.back.BackAnimationSpec
import com.android.systemui.animation.back.BackTransformation
@@ -25,6 +28,7 @@ import com.android.systemui.animation.back.applyTo
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
+import com.android.systemui.animation.view.LaunchableFrameLayout
/**
* Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
@@ -49,3 +53,110 @@ fun Dialog.registerAnimationOnBackInvoked(
),
)
}
+
+/**
+ * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
+ * outside its bounds. No-op if the dialog is already fullscreen.
+ *
+ * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
+ * and a layout listener. The new view matches the original dialog DecorView in size, position, and
+ * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
+ * layout listener is listening to changes to the modified DecorView. It is the responsibility of
+ * the caller to deregister the listener when the dialog is dismissed.
+ */
+fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
+ // Create the dialog so that its onCreate() method is called, which usually sets the dialog
+ // content.
+ create()
+
+ val window = window!!
+ val decorView = window.decorView as ViewGroup
+
+ val isWindowFullscreen =
+ window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
+ if (isWindowFullscreen) {
+ return null
+ }
+
+ // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible
+ // to animate outside its bounds.
+ //
+ // Before that, we add a new View as a child of the DecorView with the same size and gravity as
+ // that DecorView, then we add all original children of the DecorView to that new View. Finally
+ // we remove the background of the DecorView and add it to the new View, then we make the
+ // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window.
+ //
+ // On top of that, we also add a fullscreen transparent background between the DecorView and the
+ // view that we added so that we can dismiss the dialog when this view is clicked. This is
+ // necessary because DecorView overrides onTouchEvent and therefore we can't set the click
+ // listener directly on the (now fullscreen) DecorView.
+ val fullscreenTransparentBackground = FrameLayout(context)
+ decorView.addView(
+ fullscreenTransparentBackground,
+ 0 /* index */,
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ )
+
+ val dialogContentWithBackground = LaunchableFrameLayout(context)
+ dialogContentWithBackground.background = decorView.background
+
+ // Make the window background transparent. Note that setting the window (or DecorView)
+ // background drawable to null leads to issues with background color (not being transparent) or
+ // with insets that are not refreshed. Therefore we need to set it to something not null, hence
+ // we are using android.R.color.transparent here.
+ window.setBackgroundDrawableResource(android.R.color.transparent)
+
+ // Close the dialog when clicking outside of it.
+ fullscreenTransparentBackground.setOnClickListener { dismiss() }
+ dialogContentWithBackground.isClickable = true
+
+ // Make sure the transparent and dialog backgrounds are not focusable by accessibility
+ // features.
+ fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+
+ fullscreenTransparentBackground.addView(
+ dialogContentWithBackground,
+ FrameLayout.LayoutParams(
+ window.attributes.width,
+ window.attributes.height,
+ window.attributes.gravity
+ )
+ )
+
+ // Move all original children of the DecorView to the new View we just added.
+ for (i in 1 until decorView.childCount) {
+ val view = decorView.getChildAt(1)
+ decorView.removeViewAt(1)
+ dialogContentWithBackground.addView(view)
+ }
+
+ // Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
+ window.setLayout(MATCH_PARENT, MATCH_PARENT)
+ val decorViewLayoutListener =
+ View.OnLayoutChangeListener {
+ v,
+ left,
+ top,
+ right,
+ bottom,
+ oldLeft,
+ oldTop,
+ oldRight,
+ oldBottom ->
+ if (
+ window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT
+ ) {
+ // The dialog size changed, copy its size to dialogContentWithBackground and make
+ // the dialog window full screen again.
+ val layoutParams = dialogContentWithBackground.layoutParams
+ layoutParams.width = window.attributes.width
+ layoutParams.height = window.attributes.height
+ dialogContentWithBackground.layoutParams = layoutParams
+ window.setLayout(MATCH_PARENT, MATCH_PARENT)
+ }
+ }
+ decorView.addOnLayoutChangeListener(decorViewLayoutListener)
+
+ return dialogContentWithBackground to decorViewLayoutListener
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt
deleted file mode 100644
index a80a1f934dab..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * 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.compose.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.filter
-
-/** Library-wide switch to turn on debug logging. */
-internal const val DebugLog = false
-
-@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
-@Retention(AnnotationRetention.BINARY)
-annotation class ExperimentalPagerApi
-
-/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */
-@ExperimentalPagerApi
-object PagerDefaults {
- /**
- * Remember the default [FlingBehavior] that represents the scroll curve.
- *
- * @param state The [PagerState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- */
- @Composable
- fun flingBehavior(
- state: PagerState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
- ): FlingBehavior =
- rememberSnappingFlingBehavior(
- lazyListState = state.lazyListState,
- decayAnimationSpec = decayAnimationSpec,
- snapAnimationSpec = snapAnimationSpec,
- )
-
- @Deprecated(
- "Replaced with PagerDefaults.flingBehavior()",
- ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)")
- )
- @Composable
- fun rememberPagerFlingConfig(
- state: PagerState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
- ): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec)
-}
-
-/**
- * A horizontally scrolling layout that allows users to flip between items to the left and right.
- *
- * @param count the number of pages.
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the pager's state.
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item
- * is located at the end.
- * @param itemSpacing horizontal spacing to add between items.
- * @param flingBehavior logic describing fling behavior.
- * @param key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param content a block which describes the content. Inside this block you can reference
- * [PagerScope.currentPage] and other properties in [PagerScope].
- * @sample com.google.accompanist.sample.pager.HorizontalPagerSample
- */
-@ExperimentalPagerApi
-@Composable
-fun HorizontalPager(
- count: Int,
- modifier: Modifier = Modifier,
- state: PagerState = rememberPagerState(),
- reverseLayout: Boolean = false,
- itemSpacing: Dp = 0.dp,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- key: ((page: Int) -> Any)? = null,
- contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- Pager(
- count = count,
- state = state,
- modifier = modifier,
- isVertical = false,
- reverseLayout = reverseLayout,
- itemSpacing = itemSpacing,
- verticalAlignment = verticalAlignment,
- flingBehavior = flingBehavior,
- key = key,
- contentPadding = contentPadding,
- content = content
- )
-}
-
-/**
- * A vertically scrolling layout that allows users to flip between items to the top and bottom.
- *
- * @param count the number of pages.
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the pager's state.
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item
- * is located at the bottom.
- * @param itemSpacing vertical spacing to add between items.
- * @param flingBehavior logic describing fling behavior.
- * @param key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param content a block which describes the content. Inside this block you can reference
- * [PagerScope.currentPage] and other properties in [PagerScope].
- * @sample com.google.accompanist.sample.pager.VerticalPagerSample
- */
-@ExperimentalPagerApi
-@Composable
-fun VerticalPager(
- count: Int,
- modifier: Modifier = Modifier,
- state: PagerState = rememberPagerState(),
- reverseLayout: Boolean = false,
- itemSpacing: Dp = 0.dp,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
- horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- key: ((page: Int) -> Any)? = null,
- contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- Pager(
- count = count,
- state = state,
- modifier = modifier,
- isVertical = true,
- reverseLayout = reverseLayout,
- itemSpacing = itemSpacing,
- horizontalAlignment = horizontalAlignment,
- flingBehavior = flingBehavior,
- key = key,
- contentPadding = contentPadding,
- content = content
- )
-}
-
-@ExperimentalPagerApi
-@Composable
-internal fun Pager(
- count: Int,
- modifier: Modifier,
- state: PagerState,
- reverseLayout: Boolean,
- itemSpacing: Dp,
- isVertical: Boolean,
- flingBehavior: FlingBehavior,
- key: ((page: Int) -> Any)?,
- contentPadding: PaddingValues,
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- require(count >= 0) { "pageCount must be >= 0" }
-
- // Provide our PagerState with access to the SnappingFlingBehavior animation target
- // TODO: can this be done in a better way?
- state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget }
-
- LaunchedEffect(count) {
- state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
- }
-
- // Once a fling (scroll) has finished, notify the state
- LaunchedEffect(state) {
- // When a 'scroll' has finished, notify the state
- snapshotFlow { state.isScrollInProgress }
- .filter { !it }
- .collect { state.onScrollFinished() }
- }
-
- val pagerScope = remember(state) { PagerScopeImpl(state) }
-
- // We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
- // as normal
- val consumeFlingNestedScrollConnection =
- ConsumeFlingNestedScrollConnection(
- consumeHorizontal = !isVertical,
- consumeVertical = isVertical,
- )
-
- if (isVertical) {
- LazyColumn(
- state = state.lazyListState,
- verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
- horizontalAlignment = horizontalAlignment,
- flingBehavior = flingBehavior,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding,
- modifier = modifier,
- ) {
- items(
- count = count,
- key = key,
- ) { page ->
- Box(
- Modifier
- // We don't any nested flings to continue in the pager, so we add a
- // connection which consumes them.
- // See: https://github.com/google/accompanist/issues/347
- .nestedScroll(connection = consumeFlingNestedScrollConnection)
- // Constraint the content to be <= than the size of the pager.
- .fillParentMaxHeight()
- .wrapContentSize()
- ) {
- pagerScope.content(page)
- }
- }
- }
- } else {
- LazyRow(
- state = state.lazyListState,
- verticalAlignment = verticalAlignment,
- horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
- flingBehavior = flingBehavior,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding,
- modifier = modifier,
- ) {
- items(
- count = count,
- key = key,
- ) { page ->
- Box(
- Modifier
- // We don't any nested flings to continue in the pager, so we add a
- // connection which consumes them.
- // See: https://github.com/google/accompanist/issues/347
- .nestedScroll(connection = consumeFlingNestedScrollConnection)
- // Constraint the content to be <= than the size of the pager.
- .fillParentMaxWidth()
- .wrapContentSize()
- ) {
- pagerScope.content(page)
- }
- }
- }
- }
-}
-
-private class ConsumeFlingNestedScrollConnection(
- private val consumeHorizontal: Boolean,
- private val consumeVertical: Boolean,
-) : NestedScrollConnection {
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset =
- when (source) {
- // We can consume all resting fling scrolls so that they don't propagate up to the
- // Pager
- NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
- else -> Offset.Zero
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- // We can consume all post fling velocity on the main-axis
- // so that it doesn't propagate up to the Pager
- return available.consume(consumeHorizontal, consumeVertical)
- }
-}
-
-private fun Offset.consume(
- consumeHorizontal: Boolean,
- consumeVertical: Boolean,
-): Offset =
- Offset(
- x = if (consumeHorizontal) this.x else 0f,
- y = if (consumeVertical) this.y else 0f,
- )
-
-private fun Velocity.consume(
- consumeHorizontal: Boolean,
- consumeVertical: Boolean,
-): Velocity =
- Velocity(
- x = if (consumeHorizontal) this.x else 0f,
- y = if (consumeVertical) this.y else 0f,
- )
-
-/** Scope for [HorizontalPager] content. */
-@ExperimentalPagerApi
-@Stable
-interface PagerScope {
- /** Returns the current selected page */
- val currentPage: Int
-
- /** The current offset from the start of [currentPage], as a ratio of the page width. */
- val currentPageOffset: Float
-}
-
-@ExperimentalPagerApi
-private class PagerScopeImpl(
- private val state: PagerState,
-) : PagerScope {
- override val currentPage: Int
- get() = state.currentPage
- override val currentPageOffset: Float
- get() = state.currentPageOffset
-}
-
-/**
- * Calculate the offset for the given [page] from the current scroll position. This is useful when
- * using the scroll position to apply effects or animations to items.
- *
- * The returned offset can positive or negative, depending on whether which direction the [page] is
- * compared to the current scroll position.
- *
- * @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
- */
-@ExperimentalPagerApi
-fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
- return (currentPage + currentPageOffset) - page
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt
deleted file mode 100644
index 1822a68f1e77..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * 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.compose.pager
-
-import androidx.annotation.FloatRange
-import androidx.annotation.IntRange
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import kotlin.math.absoluteValue
-import kotlin.math.roundToInt
-
-@Deprecated(
- "Replaced with rememberPagerState(initialPage) and count parameter on Pager composables",
- ReplaceWith("rememberPagerState(initialPage)"),
- level = DeprecationLevel.ERROR,
-)
-@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
-@ExperimentalPagerApi
-@Composable
-inline fun rememberPagerState(
- @IntRange(from = 0) pageCount: Int,
- @IntRange(from = 0) initialPage: Int = 0,
- @FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f,
- @IntRange(from = 1) initialOffscreenLimit: Int = 1,
- infiniteLoop: Boolean = false
-): PagerState {
- return rememberPagerState(initialPage = initialPage)
-}
-
-/**
- * Creates a [PagerState] that is remembered across compositions.
- *
- * Changes to the provided values for [initialPage] will **not** result in the state being recreated
- * or changed in any way if it has already been created.
- *
- * @param initialPage the initial value for [PagerState.currentPage]
- */
-@ExperimentalPagerApi
-@Composable
-fun rememberPagerState(
- @IntRange(from = 0) initialPage: Int = 0,
-): PagerState =
- rememberSaveable(saver = PagerState.Saver) {
- PagerState(
- currentPage = initialPage,
- )
- }
-
-/**
- * A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
- *
- * In most cases, this will be created via [rememberPagerState].
- *
- * @param currentPage the initial value for [PagerState.currentPage]
- */
-@ExperimentalPagerApi
-@Stable
-class PagerState(
- @IntRange(from = 0) currentPage: Int = 0,
-) : ScrollableState {
- // Should this be public?
- internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
-
- private var _currentPage by mutableStateOf(currentPage)
-
- private val currentLayoutPageInfo: LazyListItemInfo?
- get() =
- lazyListState.layoutInfo.visibleItemsInfo
- .asSequence()
- .filter { it.offset <= 0 && it.offset + it.size > 0 }
- .lastOrNull()
-
- private val currentLayoutPageOffset: Float
- get() =
- currentLayoutPageInfo?.let { current ->
- // We coerce since itemSpacing can make the offset > 1f.
- // We don't want to count spacing in the offset so cap it to 1f
- (-current.offset / current.size.toFloat()).coerceIn(0f, 1f)
- }
- ?: 0f
-
- /**
- * [InteractionSource] that will be used to dispatch drag events when this list is being
- * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
- * [isScrollInProgress].
- */
- val interactionSource: InteractionSource
- get() = lazyListState.interactionSource
-
- /** The number of pages to display. */
- @get:IntRange(from = 0)
- val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount }
-
- /**
- * The index of the currently selected page. This may not be the page which is currently
- * displayed on screen.
- *
- * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
- */
- @get:IntRange(from = 0)
- var currentPage: Int
- get() = _currentPage
- internal set(value) {
- if (value != _currentPage) {
- _currentPage = value
- }
- }
-
- /**
- * The current offset from the start of [currentPage], as a ratio of the page width.
- *
- * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
- */
- val currentPageOffset: Float by derivedStateOf {
- currentLayoutPageInfo?.let {
- // The current page offset is the current layout page delta from `currentPage`
- // (which is only updated after a scroll/animation).
- // We calculate this by looking at the current layout page + it's offset,
- // then subtracting the 'current page'.
- it.index + currentLayoutPageOffset - _currentPage
- }
- ?: 0f
- }
-
- /** The target page for any on-going animations. */
- private var animationTargetPage: Int? by mutableStateOf(null)
-
- internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null)
-
- /**
- * The target page for any on-going animations or scrolls by the user. Returns the current page
- * if a scroll or animation is not currently in progress.
- */
- val targetPage: Int
- get() =
- animationTargetPage
- ?: flingAnimationTarget?.invoke()
- ?: when {
- // If a scroll isn't in progress, return the current page
- !isScrollInProgress -> currentPage
- // If the offset is 0f (or very close), return the current page
- currentPageOffset.absoluteValue < 0.001f -> currentPage
- // If we're offset towards the start, guess the previous page
- currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0)
- // If we're offset towards the end, guess the next page
- else -> (currentPage + 1).coerceAtMost(pageCount - 1)
- }
-
- @Deprecated(
- "Replaced with animateScrollToPage(page, pageOffset)",
- ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)")
- )
- @Suppress("UNUSED_PARAMETER")
- suspend fun animateScrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- animationSpec: AnimationSpec<Float> = spring(),
- initialVelocity: Float = 0f,
- skipPages: Boolean = true,
- ) {
- animateScrollToPage(page = page, pageOffset = pageOffset)
- }
-
- /**
- * Animate (smooth scroll) to the given page to the middle of the viewport.
- *
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param page the page to animate to. Must be between 0 and [pageCount] (inclusive).
- * @param pageOffset the percentage of the page width to offset, from the start of [page]. Must
- * be in the range 0f..1f.
- */
- suspend fun animateScrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- ) {
- requireCurrentPage(page, "page")
- requireCurrentPageOffset(pageOffset, "pageOffset")
- try {
- animationTargetPage = page
-
- if (pageOffset <= 0.005f) {
- // If the offset is (close to) zero, just call animateScrollToItem and we're done
- lazyListState.animateScrollToItem(index = page)
- } else {
- // Else we need to figure out what the offset is in pixels...
-
- var target =
- lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
-
- if (target != null) {
- // If we have access to the target page layout, we can calculate the pixel
- // offset from the size
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (target.size * pageOffset).roundToInt()
- )
- } else {
- // If we don't, we use the current page size as a guide
- val currentSize = currentLayoutPageInfo!!.size
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (currentSize * pageOffset).roundToInt()
- )
-
- // The target should be visible now
- target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page }
-
- if (target.size != currentSize) {
- // If the size we used for calculating the offset differs from the actual
- // target page size, we need to scroll again. This doesn't look great,
- // but there's not much else we can do.
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (target.size * pageOffset).roundToInt()
- )
- }
- }
- }
- } finally {
- // We need to manually call this, as the `animateScrollToItem` call above will happen
- // in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
- // the change. This is especially true when running unit tests.
- onScrollFinished()
- }
- }
-
- /**
- * Instantly brings the item at [page] to the middle of the viewport.
- *
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param page the page to snap to. Must be between 0 and [pageCount] (inclusive).
- */
- suspend fun scrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- ) {
- requireCurrentPage(page, "page")
- requireCurrentPageOffset(pageOffset, "pageOffset")
- try {
- animationTargetPage = page
-
- // First scroll to the given page. It will now be laid out at offset 0
- lazyListState.scrollToItem(index = page)
-
- // If we have a start spacing, we need to offset (scroll) by that too
- if (pageOffset > 0.0001f) {
- scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } }
- }
- } finally {
- // We need to manually call this, as the `scroll` call above will happen in 1 frame,
- // which is usually too fast for the LaunchedEffect in Pager to detect the change.
- // This is especially true when running unit tests.
- onScrollFinished()
- }
- }
-
- internal fun onScrollFinished() {
- // Then update the current page to our layout page
- currentPage = currentLayoutPageInfo?.index ?: 0
- // Clear the animation target page
- animationTargetPage = null
- }
-
- override suspend fun scroll(
- scrollPriority: MutatePriority,
- block: suspend ScrollScope.() -> Unit
- ) = lazyListState.scroll(scrollPriority, block)
-
- override fun dispatchRawDelta(delta: Float): Float {
- return lazyListState.dispatchRawDelta(delta)
- }
-
- override val isScrollInProgress: Boolean
- get() = lazyListState.isScrollInProgress
-
- override fun toString(): String =
- "PagerState(" +
- "pageCount=$pageCount, " +
- "currentPage=$currentPage, " +
- "currentPageOffset=$currentPageOffset" +
- ")"
-
- private fun requireCurrentPage(value: Int, name: String) {
- if (pageCount == 0) {
- require(value == 0) { "$name must be 0 when pageCount is 0" }
- } else {
- require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" }
- }
- }
-
- private fun requireCurrentPageOffset(value: Float, name: String) {
- if (pageCount == 0) {
- require(value == 0f) { "$name must be 0f when pageCount is 0" }
- } else {
- require(value in 0f..1f) { "$name must be >= 0 and <= 1" }
- }
- }
-
- companion object {
- /** The default [Saver] implementation for [PagerState]. */
- val Saver: Saver<PagerState, *> =
- listSaver(
- save = {
- listOf<Any>(
- it.currentPage,
- )
- },
- restore = {
- PagerState(
- currentPage = it[0] as Int,
- )
- }
- )
- }
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt
deleted file mode 100644
index 98140295306a..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * 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.compose.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import kotlin.math.abs
-
-/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */
-internal object SnappingFlingBehaviorDefaults {
- /** TODO */
- val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f)
-}
-
-/**
- * Create and remember a snapping [FlingBehavior] to be used with [LazyListState].
- *
- * @param lazyListState The [LazyListState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- *
- * TODO: move this to a new module and make it public
- */
-@Composable
-internal fun rememberSnappingFlingBehavior(
- lazyListState: LazyListState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
-): SnappingFlingBehavior =
- remember(lazyListState, decayAnimationSpec, snapAnimationSpec) {
- SnappingFlingBehavior(
- lazyListState = lazyListState,
- decayAnimationSpec = decayAnimationSpec,
- snapAnimationSpec = snapAnimationSpec,
- )
- }
-
-/**
- * A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via
- * [rememberSnappingFlingBehavior].
- *
- * @param lazyListState The [LazyListState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- */
-internal class SnappingFlingBehavior(
- private val lazyListState: LazyListState,
- private val decayAnimationSpec: DecayAnimationSpec<Float>,
- private val snapAnimationSpec: AnimationSpec<Float>,
-) : FlingBehavior {
- /** The target item index for any on-going animations. */
- var animationTarget: Int? by mutableStateOf(null)
- private set
-
- override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
- val itemInfo = currentItemInfo ?: return initialVelocity
-
- // If the decay fling can scroll past the current item, fling with decay
- return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) {
- performDecayFling(initialVelocity, itemInfo)
- } else {
- // Otherwise we 'spring' to current/next item
- performSpringFling(
- index =
- when {
- // If the velocity is greater than 1 item per second (velocity is px/s),
- // spring
- // in the relevant direction
- initialVelocity > itemInfo.size -> {
- (itemInfo.index + 1).coerceAtMost(
- lazyListState.layoutInfo.totalItemsCount - 1
- )
- }
- initialVelocity < -itemInfo.size -> itemInfo.index
- // If the velocity is 0 (or less than the size of the item), spring to
- // whichever item is closest to the snap point
- itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1
- else -> itemInfo.index
- },
- initialVelocity = initialVelocity,
- )
- }
- }
-
- private suspend fun ScrollScope.performDecayFling(
- initialVelocity: Float,
- startItem: LazyListItemInfo,
- ): Float {
- val index =
- when {
- initialVelocity > 0 -> startItem.index + 1
- else -> startItem.index
- }
- val forward = index > (currentItemInfo?.index ?: return initialVelocity)
-
- // Update the animationTarget
- animationTarget = index
-
- var velocityLeft = initialVelocity
- var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity,
- )
- .animateDecay(decayAnimationSpec) {
- val delta = value - lastValue
- val consumed = scrollBy(delta)
- lastValue = value
- velocityLeft = this.velocity
-
- val current = currentItemInfo
- if (current == null) {
- cancelAnimation()
- return@animateDecay
- }
-
- if (
- !forward &&
- (current.index < index || current.index == index && current.offset >= 0)
- ) {
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (
- forward &&
- (current.index > index || current.index == index && current.offset <= 0)
- ) {
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (abs(delta - consumed) > 0.5f) {
- // avoid rounding errors and stop if anything is unconsumed
- cancelAnimation()
- }
- }
- animationTarget = null
- return velocityLeft
- }
-
- private suspend fun ScrollScope.performSpringFling(
- index: Int,
- scrollOffset: Int = 0,
- initialVelocity: Float = 0f,
- ): Float {
- // If we don't have a current layout, we can't snap
- val initialItem = currentItemInfo ?: return initialVelocity
-
- val forward = index > initialItem.index
- // We add 10% on to the size of the current item, to compensate for any item spacing, etc
- val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f
-
- // Update the animationTarget
- animationTarget = index
-
- var velocityLeft = initialVelocity
- var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity,
- )
- .animateTo(
- targetValue = target,
- animationSpec = snapAnimationSpec,
- ) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue =
- if (forward) {
- value.coerceAtMost(target)
- } else {
- value.coerceAtLeast(target)
- }
- val delta = coercedValue - lastValue
- val consumed = scrollBy(delta)
- lastValue = coercedValue
- velocityLeft = this.velocity
-
- val current = currentItemInfo
- if (current == null) {
- cancelAnimation()
- return@animateTo
- }
-
- if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) {
- // If we've scrolled to/past the item, stop the animation. We may also need to
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (abs(delta - consumed) > 0.5f) {
- // avoid rounding errors and stop if anything is unconsumed
- cancelAnimation()
- }
- }
- animationTarget = null
- return velocityLeft
- }
-
- private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int {
- return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0
- }
-
- private val currentItemInfo: LazyListItemInfo?
- get() =
- lazyListState.layoutInfo.visibleItemsInfo
- .asSequence()
- .filter { it.offset <= 0 && it.offset + it.size > 0 }
- .lastOrNull()
-}
-
-private fun scrolledPastItem(
- initialVelocity: Float,
- currentItem: LazyListItemInfo,
- targetIndex: Int,
- targetScrollOffset: Int = 0,
-): Boolean {
- return if (initialVelocity > 0) {
- // forward
- currentItem.index > targetIndex ||
- (currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset)
- } else {
- // backwards
- currentItem.index < targetIndex ||
- (currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset)
- }
-}
-
-private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem(
- currentItem: LazyListItemInfo,
- initialVelocity: Float,
-): Boolean {
- val targetValue =
- calculateTargetValue(
- initialValue = currentItem.offset.toFloat(),
- initialVelocity = initialVelocity,
- )
- return when {
- // forward. We add 10% onto the size to cater for any item spacing
- initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f)
- // backwards. We add 10% onto the size to cater for any item spacing
- else -> targetValue >= (currentItem.size * 0.1f)
- }
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt b/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
deleted file mode 100644
index 946e77959b1c..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
+++ /dev/null
@@ -1,849 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.compose.swipeable
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.foundation.gestures.DraggableState
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import com.android.compose.swipeable.SwipeableDefaults.AnimationSpec
-import com.android.compose.swipeable.SwipeableDefaults.StandardResistanceFactor
-import com.android.compose.swipeable.SwipeableDefaults.VelocityThreshold
-import com.android.compose.swipeable.SwipeableDefaults.resistanceConfig
-import com.android.compose.ui.util.lerp
-import kotlin.math.PI
-import kotlin.math.abs
-import kotlin.math.sign
-import kotlin.math.sin
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.launch
-
-/**
- * State of the [swipeable] modifier.
- *
- * This contains necessary information about any ongoing swipe or animation and provides methods to
- * change the state either immediately or by starting an animation. To create and remember a
- * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- *
- * TODO(b/272311106): this is a fork from material. Unfork it when Swipeable.kt reaches material3.
- */
-@Stable
-open class SwipeableState<T>(
- initialValue: T,
- internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true }
-) {
- /**
- * The current value of the state.
- *
- * If no swipe or animation is in progress, this corresponds to the anchor at which the
- * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
- * the last anchor at which the [swipeable] was settled before the swipe or animation started.
- */
- var currentValue: T by mutableStateOf(initialValue)
- private set
-
- /** Whether the state is currently animating. */
- var isAnimationRunning: Boolean by mutableStateOf(false)
- private set
-
- /**
- * The current position (in pixels) of the [swipeable].
- *
- * You should use this state to offset your content accordingly. The recommended way is to use
- * `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
- */
- val offset: State<Float>
- get() = offsetState
-
- /** The amount by which the [swipeable] has been swiped past its bounds. */
- val overflow: State<Float>
- get() = overflowState
-
- // Use `Float.NaN` as a placeholder while the state is uninitialised.
- private val offsetState = mutableStateOf(0f)
- private val overflowState = mutableStateOf(0f)
-
- // the source of truth for the "real"(non ui) position
- // basically position in bounds + overflow
- private val absoluteOffset = mutableStateOf(0f)
-
- // current animation target, if animating, otherwise null
- private val animationTarget = mutableStateOf<Float?>(null)
-
- internal var anchors by mutableStateOf(emptyMap<Float, T>())
-
- private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
- snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1)
-
- internal var minBound = Float.NEGATIVE_INFINITY
- internal var maxBound = Float.POSITIVE_INFINITY
-
- internal fun ensureInit(newAnchors: Map<Float, T>) {
- if (anchors.isEmpty()) {
- // need to do initial synchronization synchronously :(
- val initialOffset = newAnchors.getOffset(currentValue)
- requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
- offsetState.value = initialOffset
- absoluteOffset.value = initialOffset
- }
- }
-
- internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) {
- if (oldAnchors.isEmpty()) {
- // If this is the first time that we receive anchors, then we need to initialise
- // the state so we snap to the offset associated to the initial value.
- minBound = newAnchors.keys.minOrNull()!!
- maxBound = newAnchors.keys.maxOrNull()!!
- val initialOffset = newAnchors.getOffset(currentValue)
- requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
- snapInternalToOffset(initialOffset)
- } else if (newAnchors != oldAnchors) {
- // If we have received new anchors, then the offset of the current value might
- // have changed, so we need to animate to the new offset. If the current value
- // has been removed from the anchors then we animate to the closest anchor
- // instead. Note that this stops any ongoing animation.
- minBound = Float.NEGATIVE_INFINITY
- maxBound = Float.POSITIVE_INFINITY
- val animationTargetValue = animationTarget.value
- // if we're in the animation already, let's find it a new home
- val targetOffset =
- if (animationTargetValue != null) {
- // first, try to map old state to the new state
- val oldState = oldAnchors[animationTargetValue]
- val newState = newAnchors.getOffset(oldState)
- // return new state if exists, or find the closes one among new anchors
- newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
- } else {
- // we're not animating, proceed by finding the new anchors for an old value
- val actualOldValue = oldAnchors[offset.value]
- val value = if (actualOldValue == currentValue) currentValue else actualOldValue
- newAnchors.getOffset(value)
- ?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!!
- }
- try {
- animateInternalToOffset(targetOffset, animationSpec)
- } catch (c: CancellationException) {
- // If the animation was interrupted for any reason, snap as a last resort.
- snapInternalToOffset(targetOffset)
- } finally {
- currentValue = newAnchors.getValue(targetOffset)
- minBound = newAnchors.keys.minOrNull()!!
- maxBound = newAnchors.keys.maxOrNull()!!
- }
- }
- }
-
- internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
-
- internal var velocityThreshold by mutableStateOf(0f)
-
- internal var resistance: ResistanceConfig? by mutableStateOf(null)
-
- internal val draggableState = DraggableState {
- val newAbsolute = absoluteOffset.value + it
- val clamped = newAbsolute.coerceIn(minBound, maxBound)
- val overflow = newAbsolute - clamped
- val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
- offsetState.value = clamped + resistanceDelta
- overflowState.value = overflow
- absoluteOffset.value = newAbsolute
- }
-
- private suspend fun snapInternalToOffset(target: Float) {
- draggableState.drag { dragBy(target - absoluteOffset.value) }
- }
-
- private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
- draggableState.drag {
- var prevValue = absoluteOffset.value
- animationTarget.value = target
- isAnimationRunning = true
- try {
- Animatable(prevValue).animateTo(target, spec) {
- dragBy(this.value - prevValue)
- prevValue = this.value
- }
- } finally {
- animationTarget.value = null
- isAnimationRunning = false
- }
- }
- }
-
- /**
- * The target value of the state.
- *
- * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
- * swipe finished. If an animation is running, this is the target value of that animation.
- * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
- */
- val targetValue: T
- get() {
- // TODO(calintat): Track current velocity (b/149549482) and use that here.
- val target =
- animationTarget.value
- ?: computeTarget(
- offset = offset.value,
- lastValue = anchors.getOffset(currentValue) ?: offset.value,
- anchors = anchors.keys,
- thresholds = thresholds,
- velocity = 0f,
- velocityThreshold = Float.POSITIVE_INFINITY
- )
- return anchors[target] ?: currentValue
- }
-
- /**
- * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
- *
- * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
- */
- val progress: SwipeProgress<T>
- get() {
- val bounds = findBounds(offset.value, anchors.keys)
- val from: T
- val to: T
- val fraction: Float
- when (bounds.size) {
- 0 -> {
- from = currentValue
- to = currentValue
- fraction = 1f
- }
- 1 -> {
- from = anchors.getValue(bounds[0])
- to = anchors.getValue(bounds[0])
- fraction = 1f
- }
- else -> {
- val (a, b) =
- if (direction > 0f) {
- bounds[0] to bounds[1]
- } else {
- bounds[1] to bounds[0]
- }
- from = anchors.getValue(a)
- to = anchors.getValue(b)
- fraction = (offset.value - a) / (b - a)
- }
- }
- return SwipeProgress(from, to, fraction)
- }
-
- /**
- * The direction in which the [swipeable] is moving, relative to the current [currentValue].
- *
- * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
- * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
- */
- val direction: Float
- get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
-
- /**
- * Set the state without any animation and suspend until it's set
- *
- * @param targetValue The new target value to set [currentValue] to.
- */
- suspend fun snapTo(targetValue: T) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- val targetOffset = anchors.getOffset(targetValue)
- requireNotNull(targetOffset) { "The target value must have an associated anchor." }
- snapInternalToOffset(targetOffset)
- currentValue = targetValue
- }
- }
-
- /**
- * Set the state to the target value by starting an animation.
- *
- * @param targetValue The new value to animate to.
- * @param anim The animation that will be used to animate to the new value.
- */
- suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- try {
- val targetOffset = anchors.getOffset(targetValue)
- requireNotNull(targetOffset) { "The target value must have an associated anchor." }
- animateInternalToOffset(targetOffset, anim)
- } finally {
- val endOffset = absoluteOffset.value
- val endValue =
- anchors
- // fighting rounding error once again, anchor should be as close as 0.5
- // pixels
- .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
- .values
- .firstOrNull()
- ?: currentValue
- currentValue = endValue
- }
- }
- }
-
- /**
- * Perform fling with settling to one of the anchors which is determined by the given
- * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
- * since it will settle at the anchor.
- *
- * In general cases, [swipeable] flings by itself when being swiped. This method is to be used
- * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
- * trigger settling fling when the child scroll container reaches the bound.
- *
- * @param velocity velocity to fling and settle with
- * @return the reason fling ended
- */
- suspend fun performFling(velocity: Float) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- val lastAnchor = anchors.getOffset(currentValue)!!
- val targetValue =
- computeTarget(
- offset = offset.value,
- lastValue = lastAnchor,
- anchors = anchors.keys,
- thresholds = thresholds,
- velocity = velocity,
- velocityThreshold = velocityThreshold
- )
- val targetState = anchors[targetValue]
- if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
- // If the user vetoed the state change, rollback to the previous state.
- else animateInternalToOffset(lastAnchor, animationSpec)
- }
- }
-
- /**
- * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
- * gesture flow.
- *
- * Note: This method performs generic drag and it won't settle to any particular anchor, *
- * leaving swipeable in between anchors. When done dragging, [performFling] must be called as
- * well to ensure swipeable will settle at the anchor.
- *
- * In general cases, [swipeable] drags by itself when being swiped. This method is to be used
- * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
- * force drag when the child scroll container reaches the bound.
- *
- * @param delta delta in pixels to drag by
- * @return the amount of [delta] consumed
- */
- fun performDrag(delta: Float): Float {
- val potentiallyConsumed = absoluteOffset.value + delta
- val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
- val deltaToConsume = clamped - absoluteOffset.value
- if (abs(deltaToConsume) > 0) {
- draggableState.dispatchRawDelta(deltaToConsume)
- }
- return deltaToConsume
- }
-
- companion object {
- /** The default [Saver] implementation for [SwipeableState]. */
- fun <T : Any> Saver(
- animationSpec: AnimationSpec<Float>,
- confirmStateChange: (T) -> Boolean
- ) =
- Saver<SwipeableState<T>, T>(
- save = { it.currentValue },
- restore = { SwipeableState(it, animationSpec, confirmStateChange) }
- )
- }
-}
-
-/**
- * Collects information about the ongoing swipe or animation in [swipeable].
- *
- * To access this information, use [SwipeableState.progress].
- *
- * @param from The state corresponding to the anchor we are moving away from.
- * @param to The state corresponding to the anchor we are moving towards.
- * @param fraction The fraction that the current position represents between [from] and [to]. Must
- * be between `0` and `1`.
- */
-@Immutable
-class SwipeProgress<T>(
- val from: T,
- val to: T,
- /*@FloatRange(from = 0.0, to = 1.0)*/
- val fraction: Float
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is SwipeProgress<*>) return false
-
- if (from != other.from) return false
- if (to != other.to) return false
- if (fraction != other.fraction) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = from?.hashCode() ?: 0
- result = 31 * result + (to?.hashCode() ?: 0)
- result = 31 * result + fraction.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
- }
-}
-
-/**
- * Create and [remember] a [SwipeableState] with the default animation clock.
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- */
-@Composable
-fun <T : Any> rememberSwipeableState(
- initialValue: T,
- animationSpec: AnimationSpec<Float> = AnimationSpec,
- confirmStateChange: (newValue: T) -> Boolean = { true }
-): SwipeableState<T> {
- return rememberSaveable(
- saver =
- SwipeableState.Saver(
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- ) {
- SwipeableState(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- }
-}
-
-/**
- * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
- * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
- * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
- * [value] will be notified to update their state to the new value of the [SwipeableState] by
- * invoking [onValueChange]. If the owner does not update their state to the provided value for
- * some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
- */
-@Composable
-internal fun <T : Any> rememberSwipeableStateFor(
- value: T,
- onValueChange: (T) -> Unit,
- animationSpec: AnimationSpec<Float> = AnimationSpec
-): SwipeableState<T> {
- val swipeableState = remember {
- SwipeableState(
- initialValue = value,
- animationSpec = animationSpec,
- confirmStateChange = { true }
- )
- }
- val forceAnimationCheck = remember { mutableStateOf(false) }
- LaunchedEffect(value, forceAnimationCheck.value) {
- if (value != swipeableState.currentValue) {
- swipeableState.animateTo(value)
- }
- }
- DisposableEffect(swipeableState.currentValue) {
- if (value != swipeableState.currentValue) {
- onValueChange(swipeableState.currentValue)
- forceAnimationCheck.value = !forceAnimationCheck.value
- }
- onDispose {}
- }
- return swipeableState
-}
-
-/**
- * Enable swipe gestures between a set of predefined states.
- *
- * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that
- * this map cannot be empty and cannot have two anchors mapped to the same state.
- *
- * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe
- * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`).
- * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableState] will also be updated to the state corresponding to the
- * new anchor. The target anchor is calculated based on the provided positional [thresholds].
- *
- * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
- * past these bounds, a resistance effect will be applied by default. The amount of resistance at
- * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
- *
- * For an example of a [swipeable] with three states, see:
- *
- * @param T The type of the state.
- * @param state The state of the [swipeable].
- * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
- * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
- * used to determine which state to animate to when swiping stops. This is represented as a lambda
- * that takes two states and returns the threshold between them in the form of a
- * [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction.
- * @param orientation The orientation in which the [swipeable] can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
- * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe
- * will behave like bottom to top, and a left to right swipe will behave like right to left.
- * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
- * [Modifier.draggable].
- * @param resistance Controls how much resistance will be applied when swiping past the bounds.
- * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in
- * order to animate to the next state, even if the positional [thresholds] have not been reached.
- * @sample androidx.compose.material.samples.SwipeableSample
- */
-fun <T> Modifier.swipeable(
- state: SwipeableState<T>,
- anchors: Map<Float, T>,
- orientation: Orientation,
- enabled: Boolean = true,
- reverseDirection: Boolean = false,
- interactionSource: MutableInteractionSource? = null,
- thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
- resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
- velocityThreshold: Dp = VelocityThreshold
-) =
- composed(
- inspectorInfo =
- debugInspectorInfo {
- name = "swipeable"
- properties["state"] = state
- properties["anchors"] = anchors
- properties["orientation"] = orientation
- properties["enabled"] = enabled
- properties["reverseDirection"] = reverseDirection
- properties["interactionSource"] = interactionSource
- properties["thresholds"] = thresholds
- properties["resistance"] = resistance
- properties["velocityThreshold"] = velocityThreshold
- }
- ) {
- require(anchors.isNotEmpty()) { "You must have at least one anchor." }
- require(anchors.values.distinct().count() == anchors.size) {
- "You cannot have two anchors mapped to the same state."
- }
- val density = LocalDensity.current
- state.ensureInit(anchors)
- LaunchedEffect(anchors, state) {
- val oldAnchors = state.anchors
- state.anchors = anchors
- state.resistance = resistance
- state.thresholds = { a, b ->
- val from = anchors.getValue(a)
- val to = anchors.getValue(b)
- with(thresholds(from, to)) { density.computeThreshold(a, b) }
- }
- with(density) { state.velocityThreshold = velocityThreshold.toPx() }
- state.processNewAnchors(oldAnchors, anchors)
- }
-
- Modifier.draggable(
- orientation = orientation,
- enabled = enabled,
- reverseDirection = reverseDirection,
- interactionSource = interactionSource,
- startDragImmediately = state.isAnimationRunning,
- onDragStopped = { velocity -> launch { state.performFling(velocity) } },
- state = state.draggableState
- )
- }
-
-/**
- * Interface to compute a threshold between two anchors/states in a [swipeable].
- *
- * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
- */
-@Stable
-interface ThresholdConfig {
- /** Compute the value of the threshold (in pixels), once the values of the anchors are known. */
- fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
-}
-
-/**
- * A fixed threshold will be at an [offset] away from the first anchor.
- *
- * @param offset The offset (in dp) that the threshold will be at.
- */
-@Immutable
-data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
- override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
- return fromValue + offset.toPx() * sign(toValue - fromValue)
- }
-}
-
-/**
- * A fractional threshold will be at a [fraction] of the way between the two anchors.
- *
- * @param fraction The fraction (between 0 and 1) that the threshold will be at.
- */
-@Immutable
-data class FractionalThreshold(
- /*@FloatRange(from = 0.0, to = 1.0)*/
- private val fraction: Float
-) : ThresholdConfig {
- override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
- return lerp(fromValue, toValue, fraction)
- }
-}
-
-/**
- * Specifies how resistance is calculated in [swipeable].
- *
- * There are two things needed to calculate resistance: the resistance basis determines how much
- * overflow will be consumed to achieve maximum resistance, and the resistance factor determines the
- * amount of resistance (the larger the resistance factor, the stronger the resistance).
- *
- * The resistance basis is usually either the size of the component which [swipeable] is applied to,
- * or the distance between the minimum and maximum anchors. For a constructor in which the
- * resistance basis defaults to the latter, consider using [resistanceConfig].
- *
- * You may specify different resistance factors for each bound. Consider using one of the default
- * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has
- * run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this
- * right now. Also, you can set either factor to 0 to disable resistance at that bound.
- *
- * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
- * @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be
- * negative.
- * @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be
- * negative.
- */
-@Immutable
-class ResistanceConfig(
- /*@FloatRange(from = 0.0, fromInclusive = false)*/
- val basis: Float,
- /*@FloatRange(from = 0.0)*/
- val factorAtMin: Float = StandardResistanceFactor,
- /*@FloatRange(from = 0.0)*/
- val factorAtMax: Float = StandardResistanceFactor
-) {
- fun computeResistance(overflow: Float): Float {
- val factor = if (overflow < 0) factorAtMin else factorAtMax
- if (factor == 0f) return 0f
- val progress = (overflow / basis).coerceIn(-1f, 1f)
- return basis / factor * sin(progress * PI.toFloat() / 2)
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ResistanceConfig) return false
-
- if (basis != other.basis) return false
- if (factorAtMin != other.factorAtMin) return false
- if (factorAtMax != other.factorAtMax) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = basis.hashCode()
- result = 31 * result + factorAtMin.hashCode()
- result = 31 * result + factorAtMax.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
- }
-}
-
-/**
- * Given an offset x and a set of anchors, return a list of anchors:
- * 1. [ ] if the set of anchors is empty,
- * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is
- * x rounded to the exact value of the matching anchor,
- * 3. [ min ] if min is the minimum anchor and x < min,
- * 4. [ max ] if max is the maximum anchor and x > max, or
- * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
- */
-private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> {
- // Find the anchors the target lies between with a little bit of rounding error.
- val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
- val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
-
- return when {
- a == null ->
- // case 1 or 3
- listOfNotNull(b)
- b == null ->
- // case 4
- listOf(a)
- a == b ->
- // case 2
- // Can't return offset itself here since it might not be exactly equal
- // to the anchor, despite being considered an exact match.
- listOf(a)
- else ->
- // case 5
- listOf(a, b)
- }
-}
-
-private fun computeTarget(
- offset: Float,
- lastValue: Float,
- anchors: Set<Float>,
- thresholds: (Float, Float) -> Float,
- velocity: Float,
- velocityThreshold: Float
-): Float {
- val bounds = findBounds(offset, anchors)
- return when (bounds.size) {
- 0 -> lastValue
- 1 -> bounds[0]
- else -> {
- val lower = bounds[0]
- val upper = bounds[1]
- if (lastValue <= offset) {
- // Swiping from lower to upper (positive).
- if (velocity >= velocityThreshold) {
- return upper
- } else {
- val threshold = thresholds(lower, upper)
- if (offset < threshold) lower else upper
- }
- } else {
- // Swiping from upper to lower (negative).
- if (velocity <= -velocityThreshold) {
- return lower
- } else {
- val threshold = thresholds(upper, lower)
- if (offset > threshold) upper else lower
- }
- }
- }
- }
-}
-
-private fun <T> Map<Float, T>.getOffset(state: T): Float? {
- return entries.firstOrNull { it.value == state }?.key
-}
-
-/** Contains useful defaults for [swipeable] and [SwipeableState]. */
-object SwipeableDefaults {
- /** The default animation used by [SwipeableState]. */
- val AnimationSpec = SpringSpec<Float>()
-
- /** The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. */
- val VelocityThreshold = 125.dp
-
- /** A stiff resistance factor which indicates that swiping isn't available right now. */
- const val StiffResistanceFactor = 20f
-
- /** A standard resistance factor which indicates that the user has run out of things to see. */
- const val StandardResistanceFactor = 10f
-
- /**
- * The default resistance config used by [swipeable].
- *
- * This returns `null` if there is one anchor. If there are at least two anchors, it returns a
- * [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
- */
- fun resistanceConfig(
- anchors: Set<Float>,
- factorAtMin: Float = StandardResistanceFactor,
- factorAtMax: Float = StandardResistanceFactor
- ): ResistanceConfig? {
- return if (anchors.size <= 1) {
- null
- } else {
- val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
- ResistanceConfig(basis, factorAtMin, factorAtMax)
- }
- }
-}
-
-// temp default nested scroll connection for swipeables which desire as an opt in
-// revisit in b/174756744 as all types will have their own specific connection probably
-internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
- get() =
- object : NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
- performDrag(delta).toOffset()
- } else {
- Offset.Zero
- }
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- return if (source == NestedScrollSource.Drag) {
- performDrag(available.toFloat()).toOffset()
- } else {
- Offset.Zero
- }
- }
-
- override suspend fun onPreFling(available: Velocity): Velocity {
- val toFling = Offset(available.x, available.y).toFloat()
- return if (toFling < 0 && offset.value > minBound) {
- performFling(velocity = toFling)
- // since we go to the anchor with tween settling, consume all for the best UX
- available
- } else {
- Velocity.Zero
- }
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- performFling(velocity = Offset(available.x, available.y).toFloat())
- return available
- }
-
- private fun Float.toOffset(): Offset = Offset(0f, this)
-
- private fun Offset.toFloat(): Float = this.y
- }
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 82fe3f265384..609ea90e9159 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
@@ -53,14 +51,6 @@ object ComposeFacade : BaseComposeFacade {
throwComposeUnavailableError()
}
- override fun createMultiShadeView(
- context: Context,
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- ): View {
- throwComposeUnavailableError()
- }
-
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 7926f9224347..0ee88b90bcc4 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -23,8 +23,6 @@ import androidx.activity.compose.setContent
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
-import com.android.systemui.multishade.ui.composable.MultiShade
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -34,7 +32,6 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
@@ -60,23 +57,6 @@ object ComposeFacade : BaseComposeFacade {
}
}
- override fun createMultiShadeView(
- context: Context,
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- ): View {
- return ComposeView(context).apply {
- setContent {
- PlatformTheme {
- MultiShade(
- viewModel = viewModel,
- clock = clock,
- )
- }
- }
- }
- }
-
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 63a3eca4695d..64227b8c5f2a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -228,43 +228,45 @@ internal fun PatternBouncer(
}
}
) {
- // Draw lines between dots.
- selectedDots.forEachIndexed { index, dot ->
- if (index > 0) {
- val previousDot = selectedDots[index - 1]
- val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
- val startLerp = 1 - lineFadeOutAnimationProgress
- val from = pixelOffset(previousDot, spacing, verticalOffset)
- val to = pixelOffset(dot, spacing, verticalOffset)
- val lerpedFrom =
- Offset(
- x = from.x + (to.x - from.x) * startLerp,
- y = from.y + (to.y - from.y) * startLerp,
+ if (isAnimationEnabled) {
+ // Draw lines between dots.
+ selectedDots.forEachIndexed { index, dot ->
+ if (index > 0) {
+ val previousDot = selectedDots[index - 1]
+ val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
+ val startLerp = 1 - lineFadeOutAnimationProgress
+ val from = pixelOffset(previousDot, spacing, verticalOffset)
+ val to = pixelOffset(dot, spacing, verticalOffset)
+ val lerpedFrom =
+ Offset(
+ x = from.x + (to.x - from.x) * startLerp,
+ y = from.y + (to.y - from.y) * startLerp,
+ )
+ drawLine(
+ start = lerpedFrom,
+ end = to,
+ cap = StrokeCap.Round,
+ alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
)
- drawLine(
- start = lerpedFrom,
- end = to,
- cap = StrokeCap.Round,
- alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ }
}
- }
- // Draw the line between the most recently-selected dot and the input pointer position.
- inputPosition?.let { lineEnd ->
- currentDot?.let { dot ->
- val from = pixelOffset(dot, spacing, verticalOffset)
- val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
- drawLine(
- start = from,
- end = lineEnd,
- cap = StrokeCap.Round,
- alpha = lineAlpha(spacing, lineLength),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ // Draw the line between the most recently-selected dot and the input pointer position.
+ inputPosition?.let { lineEnd ->
+ currentDot?.let { dot ->
+ val from = pixelOffset(dot, spacing, verticalOffset)
+ val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
+ drawLine(
+ start = from,
+ end = lineEnd,
+ cap = StrokeCap.Round,
+ alpha = lineAlpha(spacing, lineLength),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index bef0b3df36c2..ec6e5eda264e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -14,63 +14,40 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalAnimationApi::class, ExperimentalAnimationGraphicsApi::class)
-
package com.android.systemui.bouncer.ui.composable
import android.view.HapticFeedbackConstants
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.Transition
-import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
-import androidx.compose.animation.graphics.res.animatedVectorResource
-import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
-import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
@@ -78,7 +55,6 @@ import com.android.compose.grid.VerticalGrid
import com.android.compose.modifiers.thenIf
import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
-import com.android.systemui.bouncer.ui.viewmodel.EnteredKey
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
@@ -109,147 +85,6 @@ internal fun PinBouncer(
}
@Composable
-private fun PinInputDisplay(viewModel: PinBouncerViewModel) {
- val currentPinEntries: List<EnteredKey> by viewModel.pinEntries.collectAsState()
-
- // visiblePinEntries keeps pins removed from currentPinEntries in the composition until their
- // disappear-animation completed. The list is sorted by the natural ordering of EnteredKey,
- // which is guaranteed to produce the original edit order, since the model only modifies entries
- // at the end.
- val visiblePinEntries = remember { SnapshotStateList<EnteredKey>() }
- currentPinEntries.forEach {
- val index = visiblePinEntries.binarySearch(it)
- if (index < 0) {
- val insertionPoint = -(index + 1)
- visiblePinEntries.add(insertionPoint, it)
- }
- }
-
- Row(
- modifier =
- Modifier.heightIn(min = entryShapeSize)
- // Pins overflowing horizontally should still be shown as scrolling.
- .wrapContentSize(unbounded = true),
- ) {
- visiblePinEntries.forEachIndexed { index, entry ->
- key(entry) {
- val visibility = remember {
- MutableTransitionState<EntryVisibility>(EntryVisibility.Hidden)
- }
- visibility.targetState =
- when {
- currentPinEntries.isEmpty() && visiblePinEntries.size > 1 ->
- EntryVisibility.BulkHidden(index, visiblePinEntries.size)
- currentPinEntries.contains(entry) -> EntryVisibility.Shown
- else -> EntryVisibility.Hidden
- }
-
- val shape = viewModel.pinShapes.getShape(entry.sequenceNumber)
- PinInputEntry(shape, updateTransition(visibility, label = "Pin Entry $entry"))
-
- LaunchedEffect(entry) {
- // Remove entry from visiblePinEntries once the hide transition completed.
- snapshotFlow {
- visibility.currentState == visibility.targetState &&
- visibility.targetState != EntryVisibility.Shown
- }
- .collect { isRemoved ->
- if (isRemoved) {
- visiblePinEntries.remove(entry)
- }
- }
- }
- }
- }
- }
-}
-
-private sealed class EntryVisibility {
- object Shown : EntryVisibility()
-
- object Hidden : EntryVisibility()
-
- /**
- * Same as [Hidden], but applies when multiple entries are hidden simultaneously, without
- * collapsing during the hide.
- */
- data class BulkHidden(val staggerIndex: Int, val totalEntryCount: Int) : EntryVisibility()
-}
-
-@Composable
-private fun PinInputEntry(shapeResourceId: Int, transition: Transition<EntryVisibility>) {
- // spec: http://shortn/_DEhE3Xl2bi
- val dismissStaggerDelayMs = 33
- val dismissDurationMs = 450
- val expansionDurationMs = 250
- val shapeCollapseDurationMs = 200
-
- val animatedEntryWidth by
- transition.animateDp(
- transitionSpec = {
- when (val target = targetState) {
- is EntryVisibility.BulkHidden ->
- // only collapse horizontal space once all entries are removed
- snap(dismissDurationMs + dismissStaggerDelayMs * target.totalEntryCount)
- else -> tween(expansionDurationMs, easing = Easings.Standard)
- }
- },
- label = "entry space"
- ) { state ->
- if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
- }
-
- val animatedShapeSize by
- transition.animateDp(
- transitionSpec = {
- when {
- EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> {
- // The AVD contains the entry transition.
- snap()
- }
- targetState is EntryVisibility.BulkHidden -> {
- val target = targetState as EntryVisibility.BulkHidden
- tween(
- dismissDurationMs,
- delayMillis = target.staggerIndex * dismissStaggerDelayMs,
- easing = Easings.Legacy,
- )
- }
- else -> tween(shapeCollapseDurationMs, easing = Easings.StandardDecelerate)
- }
- },
- label = "shape size"
- ) { state ->
- if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
- }
-
- val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
- Layout(
- content = {
- val image = AnimatedImageVector.animatedVectorResource(shapeResourceId)
- var atEnd by remember { mutableStateOf(false) }
- Image(
- painter = rememberAnimatedVectorPainter(image, atEnd),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- colorFilter = ColorFilter.tint(dotColor),
- )
- LaunchedEffect(Unit) { atEnd = true }
- }
- ) { measurables, _ ->
- val shapeSizePx = animatedShapeSize.roundToPx()
- val placeable = measurables.single().measure(Constraints.fixed(shapeSizePx, shapeSizePx))
-
- layout(animatedEntryWidth.roundToPx(), entryShapeSize.roundToPx()) {
- placeable.place(
- ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
- ((entryShapeSize - animatedShapeSize) / 2f).roundToPx()
- )
- }
- }
-}
-
-@Composable
private fun PinPad(viewModel: PinBouncerViewModel) {
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
@@ -511,8 +346,6 @@ private suspend fun showFailureAnimation(
}
}
-private val entryShapeSize = 30.dp
-
private val pinButtonSize = 84.dp
private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize
private const val pinButtonErrorShrinkMs = 50
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
new file mode 100644
index 000000000000..77065cfdeb76
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalAnimationGraphicsApi::class)
+
+package com.android.systemui.bouncer.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.Easings
+import com.android.keyguard.PinShapeAdapter
+import com.android.systemui.R
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+
+@Composable
+fun PinInputDisplay(viewModel: PinBouncerViewModel) {
+ val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsState()
+ val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes)
+
+ // The display comes in two different flavors:
+ // 1) hinting: shows a circle (◦) per expected pin input, and dot (●) per entered digit.
+ // This has a fixed width, and uses two distinct types of AVDs to animate the addition and
+ // removal of digits.
+ // 2) regular, shows a dot (●) per entered digit.
+ // This grows/shrinks as digits are added deleted. Uses the same type of AVDs to animate the
+ // addition of digits, but simply center-shrinks the dot (●) shape to zero to animate the
+ // removal.
+ // Because of all these differences, there are two separate implementations, rather than
+ // unifying into a single, more complex implementation.
+
+ when (val length = hintedPinLength) {
+ null -> RegularPinInputDisplay(viewModel, shapeAnimations)
+ else -> HintingPinInputDisplay(viewModel, shapeAnimations, length)
+ }
+}
+
+/**
+ * A pin input display that shows a placeholder circle (◦) for every digit in the pin not yet
+ * entered.
+ *
+ * Used for auto-confirmed pins of a specific length, see design: http://shortn/_jS8kPzQ7QV
+ */
+@Composable
+private fun HintingPinInputDisplay(
+ viewModel: PinBouncerViewModel,
+ shapeAnimations: ShapeAnimations,
+ hintedPinLength: Int,
+) {
+ val pinInput: PinInputViewModel by viewModel.pinInput.collectAsState()
+ // [ClearAll] marker pointing at the beginning of the current pin input.
+ // When a new [ClearAll] token is added to the [pinInput], the clear-all animation is played
+ // and the marker is advanced manually to the most recent marker. See LaunchedEffect below.
+ var currentClearAll by remember { mutableStateOf(pinInput.mostRecentClearAll()) }
+ // The length of the pin currently entered by the user.
+ val currentPinLength = pinInput.getDigits(currentClearAll).size
+
+ // The animated vector drawables for each of the [hintedPinLength] slots.
+ // The first [currentPinLength] drawables end in a dot (●) shape, the remaining drawables up to
+ // [hintedPinLength] end in the circle (◦) shape.
+ // This list is re-generated upon each pin entry, it is modelled as a [MutableStateList] to
+ // allow the clear-all animation to replace the shapes asynchronously, see LaunchedEffect below.
+ // Note that when a [ClearAll] token is added to the input (and the clear-all animation plays)
+ // the [currentPinLength] does not change; the [pinEntryDrawable] is remembered until the
+ // clear-all animation finishes and the [currentClearAll] state is manually advanced.
+ val pinEntryDrawable =
+ remember(currentPinLength) {
+ buildList {
+ repeat(currentPinLength) { add(shapeAnimations.getShapeToDot(it)) }
+ repeat(hintedPinLength - currentPinLength) { add(shapeAnimations.dotToCircle) }
+ }
+ .toMutableStateList()
+ }
+
+ val mostRecentClearAll = pinInput.mostRecentClearAll()
+ // Whenever a new [ClearAll] marker is added to the input, the clear-all animation needs to
+ // be played.
+ LaunchedEffect(mostRecentClearAll) {
+ if (currentClearAll == mostRecentClearAll) {
+ // Except during the initial composition.
+ return@LaunchedEffect
+ }
+
+ // Staggered replace of all dot (●) shapes with an animation from dot (●) to circle (◦).
+ for (index in 0 until hintedPinLength) {
+ if (!shapeAnimations.isDotShape(pinEntryDrawable[index])) break
+
+ pinEntryDrawable[index] = shapeAnimations.dotToCircle
+ delay(shapeAnimations.dismissStaggerDelay)
+ }
+
+ // Once the animation is done, start processing the next pin input again.
+ currentClearAll = mostRecentClearAll
+ }
+
+ // During the initial composition, do not play the [pinEntryDrawable] animations. This prevents
+ // the dot (●) to circle (◦) animation when the empty display becomes first visible, and a
+ // superfluous shape to dot (●) animation after for example device rotation.
+ var playAnimation by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { playAnimation = true }
+
+ val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+ Row(modifier = Modifier.heightIn(min = shapeAnimations.shapeSize)) {
+ pinEntryDrawable.forEachIndexed { index, drawable ->
+ // Key the loop by [index] and [drawable], so that updating a shape drawable at the same
+ // index will play the new animation (by remembering a new [atEnd]).
+ key(index, drawable) {
+ // [rememberAnimatedVectorPainter] requires a `atEnd` boolean to switch from `false`
+ // to `true` for the animation to play. This animation is suppressed when
+ // playAnimation is false, always rendering the end-state of the animation.
+ var atEnd by remember { mutableStateOf(!playAnimation) }
+ LaunchedEffect(Unit) { atEnd = true }
+
+ Image(
+ painter = rememberAnimatedVectorPainter(drawable, atEnd),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ colorFilter = ColorFilter.tint(dotColor),
+ )
+ }
+ }
+ }
+}
+
+/**
+ * A pin input that shows a dot (●) for each entered pin, horizontally centered and growing /
+ * shrinking as more digits are entered and deleted.
+ *
+ * Used for pin input when the pin length is not hinted, see design http://shortn/_wNP7SrBD78
+ */
+@Composable
+private fun RegularPinInputDisplay(
+ viewModel: PinBouncerViewModel,
+ shapeAnimations: ShapeAnimations,
+) {
+ // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from
+ // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove
+ // animation, thus the composable to be removed has to remain in the composition until fully
+ // disappeared (see `prune` launched effect below)
+ val pinInputRow = remember(shapeAnimations) { PinInputRow(shapeAnimations) }
+
+ // Processed `viewModel.pinInput` updates and applies them to [pinDigitShapes]
+ LaunchedEffect(viewModel.pinInput, pinInputRow) {
+ // Initial setup: capture the most recent [ClearAll] marker and create the visuals for the
+ // existing digits (if any) without animation..
+ var currentClearAll =
+ with(viewModel.pinInput.value) {
+ val initialClearAll = mostRecentClearAll()
+ pinInputRow.setDigits(getDigits(initialClearAll))
+ initialClearAll
+ }
+
+ viewModel.pinInput.collect { input ->
+ // Process additions and removals of pins within the current input block.
+ pinInputRow.updateDigits(input.getDigits(currentClearAll), scope = this@LaunchedEffect)
+
+ val mostRecentClearAll = input.mostRecentClearAll()
+ if (currentClearAll != mostRecentClearAll) {
+ // A new [ClearAll] token is added to the [input], play the clear-all animation
+ pinInputRow.playClearAllAnimation()
+
+ // Animation finished, advance manually to the next marker.
+ currentClearAll = mostRecentClearAll
+ }
+ }
+ }
+
+ LaunchedEffect(pinInputRow) {
+ // Prunes unused VisiblePinEntries once they are no longer visible.
+ snapshotFlow { pinInputRow.hasUnusedEntries() }
+ .collect { hasUnusedEntries ->
+ if (hasUnusedEntries) {
+ pinInputRow.prune()
+ }
+ }
+ }
+
+ pinInputRow.Content()
+}
+
+private class PinInputRow(
+ val shapeAnimations: ShapeAnimations,
+) {
+ private val entries = mutableStateListOf<PinInputEntry>()
+
+ @Composable
+ fun Content() {
+ Row(
+ modifier =
+ Modifier.heightIn(min = shapeAnimations.shapeSize)
+ // Pins overflowing horizontally should still be shown as scrolling.
+ .wrapContentSize(unbounded = true),
+ ) {
+ entries.forEach { entry -> key(entry.digit) { entry.Content() } }
+ }
+ }
+
+ /**
+ * Replaces all current [PinInputEntry] composables with new instances for each digit.
+ *
+ * Does not play the entry expansion animation.
+ */
+ fun setDigits(digits: List<Digit>) {
+ entries.clear()
+ entries.addAll(digits.map { PinInputEntry(it, shapeAnimations) })
+ }
+
+ /**
+ * Adds [PinInputEntry] composables for new digits and plays an entry animation, and starts the
+ * exit animation for digits not in [updated] anymore.
+ *
+ * The function return immediately, playing the animations in the background.
+ *
+ * Removed entries have to be [prune]d once the exit animation completes, [hasUnusedEntries] can
+ * be used in a [SnapshotFlow] to discover when its time to do so.
+ */
+ fun updateDigits(updated: List<Digit>, scope: CoroutineScope) {
+ val incoming = updated.minus(entries.map { it.digit }.toSet()).toList()
+ val outgoing = entries.filterNot { entry -> updated.any { entry.digit == it } }.toList()
+
+ entries.addAll(
+ incoming.map {
+ PinInputEntry(it, shapeAnimations).apply { scope.launch { animateAppearance() } }
+ }
+ )
+
+ outgoing.forEach { entry -> scope.launch { entry.animateRemoval() } }
+
+ entries.sortWith(compareBy { it.digit })
+ }
+
+ /**
+ * Plays a staggered remove animation, and upon completion removes the [PinInputEntry]
+ * composables.
+ *
+ * This function returns once the animation finished playing and the entries are removed.
+ */
+ suspend fun playClearAllAnimation() = coroutineScope {
+ val entriesToRemove = entries.toList()
+ entriesToRemove
+ .mapIndexed { index, entry ->
+ launch {
+ delay(shapeAnimations.dismissStaggerDelay * index)
+ entry.animateClearAllCollapse()
+ }
+ }
+ .joinAll()
+
+ // Remove all [PinInputEntry] composables for which the staggered remove animation was
+ // played. Note that up to now, each PinInputEntry still occupied the full width.
+ entries.removeAll(entriesToRemove)
+ }
+
+ /**
+ * Whether there are [PinInputEntry] that can be removed from the composition since they were
+ * fully animated out.
+ */
+ fun hasUnusedEntries(): Boolean {
+ return entries.any { it.isUnused }
+ }
+
+ /** Remove all no longer visible [PinInputEntry]s from the composition. */
+ fun prune() {
+ entries.removeAll { it.isUnused }
+ }
+}
+
+private class PinInputEntry(
+ val digit: Digit,
+ val shapeAnimations: ShapeAnimations,
+) {
+ private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber)
+ // horizontal space occupied, used to shift contents as individual digits are animated in/out
+ private val entryWidth =
+ Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Width of pin ($digit)")
+ // intrinsic width and height of the shape, used to collapse the shape during exit animations.
+ private val shapeSize =
+ Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Size of pin ($digit)")
+
+ /**
+ * Whether the is fully animated out. When `true`, removing this from the composable won't have
+ * visual effects.
+ */
+ val isUnused: Boolean
+ get() {
+ return entryWidth.targetValue == 0.dp && !entryWidth.isRunning
+ }
+
+ /** Animate the shape appearance by growing the entry width from 0.dp to the intrinsic width. */
+ suspend fun animateAppearance() = coroutineScope {
+ entryWidth.snapTo(0.dp)
+ entryWidth.animateTo(shapeAnimations.shapeSize, shapeAnimations.inputShiftAnimationSpec)
+ }
+
+ /**
+ * Animates shape disappearance by collapsing the shape and occupied horizontal space.
+ *
+ * Once complete, [isUnused] will return `true`.
+ */
+ suspend fun animateRemoval() = coroutineScope {
+ awaitAll(
+ async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) },
+ async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) }
+ )
+ }
+
+ /** Collapses the shape in place, while still holding on to the horizontal space. */
+ suspend fun animateClearAllCollapse() = coroutineScope {
+ shapeSize.animateTo(0.dp, shapeAnimations.clearAllShapeSizeAnimationSpec)
+ }
+
+ @Composable
+ fun Content() {
+ val animatedShapeSize by shapeSize.asState()
+ val animatedEntryWidth by entryWidth.asState()
+
+ val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+ val shapeHeight = shapeAnimations.shapeSize
+ var atEnd by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { atEnd = true }
+ Image(
+ painter = rememberAnimatedVectorPainter(shape, atEnd),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ colorFilter = ColorFilter.tint(dotColor),
+ modifier =
+ Modifier.layout { measurable, _ ->
+ val shapeSizePx = animatedShapeSize.roundToPx()
+ val placeable = measurable.measure(Constraints.fixed(shapeSizePx, shapeSizePx))
+
+ layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) {
+ placeable.place(
+ ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
+ ((shapeHeight - animatedShapeSize) / 2f).roundToPx()
+ )
+ }
+ },
+ )
+ }
+}
+
+/** Animated Vector Drawables used to render the pin input. */
+private class ShapeAnimations(
+ /** Width and height for all the animation images listed here. */
+ val shapeSize: Dp,
+ /** Transitions from the dot (●) to the circle (◦). Used for the hinting pin input only. */
+ val dotToCircle: AnimatedImageVector,
+ /** Each of the animations transition from nothing via a shape to the dot (●). */
+ private val shapesToDot: List<AnimatedImageVector>,
+) {
+ /**
+ * Returns a transition from nothing via shape to the dot (●)., specific to the input position.
+ */
+ fun getShapeToDot(position: Int): AnimatedImageVector {
+ return shapesToDot[position.mod(shapesToDot.size)]
+ }
+
+ /**
+ * Whether the [shapeAnimation] is a image returned by [getShapeToDot], and thus is ending in
+ * the dot (●) shape.
+ *
+ * `false` if the shape's end state is the circle (◦).
+ */
+ fun isDotShape(shapeAnimation: AnimatedImageVector): Boolean {
+ return shapeAnimation != dotToCircle
+ }
+
+ // spec: http://shortn/_DEhE3Xl2bi
+ val dismissStaggerDelay = 33.milliseconds
+ val inputShiftAnimationSpec = tween<Dp>(durationMillis = 250, easing = Easings.Standard)
+ val deleteShapeSizeAnimationSpec =
+ tween<Dp>(durationMillis = 200, easing = Easings.StandardDecelerate)
+ val clearAllShapeSizeAnimationSpec = tween<Dp>(durationMillis = 450, easing = Easings.Legacy)
+}
+
+@Composable
+private fun rememberShapeAnimations(pinShapes: PinShapeAdapter): ShapeAnimations {
+ // NOTE: `animatedVectorResource` does remember the returned AnimatedImageVector.
+ val dotToCircle = AnimatedImageVector.animatedVectorResource(R.drawable.pin_dot_delete_avd)
+ val shapesToDot = pinShapes.shapes.map { AnimatedImageVector.animatedVectorResource(it) }
+ val shapeSize = dimensionResource(R.dimen.password_shape_size)
+
+ return remember(dotToCircle, shapesToDot, shapeSize) {
+ ShapeAnimations(shapeSize, dotToCircle, shapesToDot)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
deleted file mode 100644
index 99fe26ce1f3b..000000000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.composable
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.gestures.detectVerticalDragGestures
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.unit.IntSize
-import com.android.systemui.R
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
-import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
-import com.android.systemui.statusbar.ui.composable.StatusBar
-import com.android.systemui.util.time.SystemClock
-
-@Composable
-fun MultiShade(
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- modifier: Modifier = Modifier,
-) {
- val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState()
- val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState()
-
- // TODO(b/273298030): find a different way to get the height constraint from its parent.
- BoxWithConstraints(modifier = modifier) {
- val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() }
-
- Scrim(
- modifier = Modifier.fillMaxSize(),
- remoteTouch = viewModel::onScrimTouched,
- alpha = { scrimAlpha },
- isScrimEnabled = isScrimEnabled,
- )
- Shade(
- viewModel = viewModel.leftShade,
- currentTimeMillis = clock::elapsedRealtime,
- containerHeightPx = maxHeightPx,
- modifier = Modifier.align(Alignment.TopStart),
- ) {
- Column {
- StatusBar()
- Notifications()
- }
- }
- Shade(
- viewModel = viewModel.rightShade,
- currentTimeMillis = clock::elapsedRealtime,
- containerHeightPx = maxHeightPx,
- modifier = Modifier.align(Alignment.TopEnd),
- ) {
- Column {
- StatusBar()
- QuickSettings()
- }
- }
- Shade(
- viewModel = viewModel.singleShade,
- currentTimeMillis = clock::elapsedRealtime,
- containerHeightPx = maxHeightPx,
- modifier = Modifier,
- ) {
- Column {
- StatusBar()
- Notifications()
- QuickSettings()
- }
- }
- }
-}
-
-@Composable
-private fun Scrim(
- remoteTouch: (ProxiedInputModel) -> Unit,
- alpha: () -> Float,
- isScrimEnabled: Boolean,
- modifier: Modifier = Modifier,
-) {
- var size by remember { mutableStateOf(IntSize.Zero) }
-
- Box(
- modifier =
- modifier
- .graphicsLayer { this.alpha = alpha() }
- .background(colorResource(R.color.opaque_scrim))
- .fillMaxSize()
- .onSizeChanged { size = it }
- .then(
- if (isScrimEnabled) {
- Modifier.pointerInput(Unit) {
- detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) })
- }
- .pointerInput(Unit) {
- detectVerticalDragGestures(
- onVerticalDrag = { change, dragAmount ->
- remoteTouch(
- ProxiedInputModel.OnDrag(
- xFraction = change.position.x / size.width,
- yDragAmountPx = dragAmount,
- )
- )
- },
- onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) },
- onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) }
- )
- }
- } else {
- Modifier
- }
- )
- )
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
deleted file mode 100644
index cfcc2fb251fd..000000000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.composable
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.android.compose.modifiers.height
-import com.android.compose.modifiers.padding
-import com.android.compose.swipeable.FixedThreshold
-import com.android.compose.swipeable.SwipeableState
-import com.android.compose.swipeable.ThresholdConfig
-import com.android.compose.swipeable.rememberSwipeableState
-import com.android.compose.swipeable.swipeable
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel
-import kotlin.math.min
-import kotlin.math.roundToInt
-import kotlinx.coroutines.launch
-
-/**
- * Renders a shade (container and content).
- *
- * This should be allowed to grow to fill the width and height of its container.
- *
- * @param viewModel The view-model for this shade.
- * @param currentTimeMillis A provider for the current time, in milliseconds.
- * @param containerHeightPx The height of the container that this shade is being shown in, in
- * pixels.
- * @param modifier The Modifier.
- * @param content The content of the shade.
- */
-@Composable
-fun Shade(
- viewModel: ShadeViewModel,
- currentTimeMillis: () -> Long,
- containerHeightPx: Float,
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit = {},
-) {
- val isVisible: Boolean by viewModel.isVisible.collectAsState()
- if (!isVisible) {
- return
- }
-
- val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
- ReportNonProxiedInput(viewModel, interactionSource)
-
- val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed)
- HandleForcedCollapse(viewModel, swipeableState)
- HandleProxiedInput(viewModel, swipeableState, currentTimeMillis)
- ReportShadeExpansion(viewModel, swipeableState, containerHeightPx)
-
- val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState()
- val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState()
- val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState()
-
- val width: ShadeViewModel.Size by viewModel.width.collectAsState()
- val density = LocalDensity.current
-
- val anchors: Map<Float, ShadeState> =
- remember(containerHeightPx) { swipeableAnchors(containerHeightPx) }
-
- ShadeContent(
- shadeHeightPx = { swipeableState.offset.value },
- overstretch = { swipeableState.overflow.value / containerHeightPx },
- isSwipingEnabled = isSwipingEnabled,
- swipeableState = swipeableState,
- interactionSource = interactionSource,
- anchors = anchors,
- thresholds = { _, to ->
- swipeableThresholds(
- to = to,
- swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx),
- swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx),
- )
- },
- modifier = modifier.shadeWidth(width, density),
- content = content,
- )
-}
-
-/**
- * Draws the content of the shade.
- *
- * @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is
- * fully collapsed.
- * @param overstretch Provider for the current amount of vertical "overstretch" that the shade
- * should be rendered with. This is `0` or a positive number that is a percentage of the total
- * height of the shade when fully expanded. A value of `0` means that the shade is not stretched
- * at all.
- * @param isSwipingEnabled Whether swiping inside the shade is enabled or not.
- * @param swipeableState The state to use for the [swipeable] modifier, allowing external control in
- * addition to direct control (proxied user input in addition to non-proxied/direct user input).
- * @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state
- * occurs; this is used to configure the [swipeable] modifier.
- * @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to
- * another. This controls how the [swipeable] decides which [ShadeState] to animate to once the
- * user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded.
- * @param content The content to render inside the shade.
- * @param modifier The [Modifier].
- */
-@Composable
-private fun ShadeContent(
- shadeHeightPx: () -> Float,
- overstretch: () -> Float,
- isSwipingEnabled: Boolean,
- swipeableState: SwipeableState<ShadeState>,
- interactionSource: MutableInteractionSource,
- anchors: Map<Float, ShadeState>,
- thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig,
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit = {},
-) {
- /**
- * Returns a function that takes in [Density] and returns the current padding around the shade
- * content.
- */
- fun padding(
- shadeHeightPx: () -> Float,
- ): Density.() -> Int {
- return {
- min(
- 12.dp.toPx().roundToInt(),
- shadeHeightPx().roundToInt(),
- )
- }
- }
-
- Surface(
- shape = RoundedCornerShape(32.dp),
- modifier =
- modifier
- .fillMaxWidth()
- .height { shadeHeightPx().roundToInt() }
- .padding(
- horizontal = padding(shadeHeightPx),
- vertical = padding(shadeHeightPx),
- )
- .graphicsLayer {
- // Applies the vertical over-stretching of the shade content that may happen if
- // the user keep dragging down when the shade is already fully-expanded.
- transformOrigin = transformOrigin.copy(pivotFractionY = 0f)
- this.scaleY = 1 + overstretch().coerceAtLeast(0f)
- }
- .swipeable(
- enabled = isSwipingEnabled,
- state = swipeableState,
- interactionSource = interactionSource,
- anchors = anchors,
- thresholds = thresholds,
- orientation = Orientation.Vertical,
- ),
- content = content,
- )
-}
-
-/** Funnels current shade expansion values into the view-model. */
-@Composable
-private fun ReportShadeExpansion(
- viewModel: ShadeViewModel,
- swipeableState: SwipeableState<ShadeState>,
- containerHeightPx: Float,
-) {
- LaunchedEffect(swipeableState.offset, containerHeightPx) {
- snapshotFlow { swipeableState.offset.value / containerHeightPx }
- .collect { expansion -> viewModel.onExpansionChanged(expansion) }
- }
-}
-
-/** Funnels drag gesture start and end events into the view-model. */
-@Composable
-private fun ReportNonProxiedInput(
- viewModel: ShadeViewModel,
- interactionSource: InteractionSource,
-) {
- LaunchedEffect(interactionSource) {
- interactionSource.interactions.collect {
- when (it) {
- is DragInteraction.Start -> {
- viewModel.onDragStarted()
- }
- is DragInteraction.Stop -> {
- viewModel.onDragEnded()
- }
- }
- }
- }
-}
-
-/** When told to force collapse, collapses the shade. */
-@Composable
-private fun HandleForcedCollapse(
- viewModel: ShadeViewModel,
- swipeableState: SwipeableState<ShadeState>,
-) {
- LaunchedEffect(viewModel) {
- viewModel.isForceCollapsed.collect {
- launch { swipeableState.animateTo(ShadeState.FullyCollapsed) }
- }
- }
-}
-
-/**
- * Handles proxied input (input originating outside of the UI of the shade) by driving the
- * [SwipeableState] accordingly.
- */
-@Composable
-private fun HandleProxiedInput(
- viewModel: ShadeViewModel,
- swipeableState: SwipeableState<ShadeState>,
- currentTimeMillis: () -> Long,
-) {
- val velocityTracker: VelocityTracker = remember { VelocityTracker() }
- LaunchedEffect(viewModel) {
- viewModel.proxiedInput.collect {
- when (it) {
- is ProxiedInputModel.OnDrag -> {
- velocityTracker.addPosition(
- timeMillis = currentTimeMillis.invoke(),
- position = Offset(0f, it.yDragAmountPx),
- )
- swipeableState.performDrag(it.yDragAmountPx)
- }
- is ProxiedInputModel.OnDragEnd -> {
- launch {
- val velocity = velocityTracker.calculateVelocity().y
- velocityTracker.resetTracking()
- // We use a VelocityTracker to keep a record of how fast the pointer was
- // moving such that we know how far to fling the shade when the gesture
- // ends. Flinging the SwipeableState using performFling is required after
- // one or more calls to performDrag such that the swipeable settles into one
- // of the states. Without doing that, the shade would remain unmoving in an
- // in-between state on the screen.
- swipeableState.performFling(velocity)
- }
- }
- is ProxiedInputModel.OnDragCancel -> {
- launch {
- velocityTracker.resetTracking()
- swipeableState.animateTo(swipeableState.progress.from)
- }
- }
- else -> Unit
- }
- }
- }
-}
-
-/**
- * Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp.
- *
- * @param density The [Density] of the display.
- * @param wholePx The whole amount that the given [Float] is a fraction of.
- * @return The dp size that's a fraction of the whole amount.
- */
-private fun Float.fractionToDp(density: Density, wholePx: Float): Dp {
- return with(density) { (this@fractionToDp * wholePx).toDp() }
-}
-
-private fun Modifier.shadeWidth(
- size: ShadeViewModel.Size,
- density: Density,
-): Modifier {
- return then(
- when (size) {
- is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction)
- is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() })
- }
- )
-}
-
-/** Returns the pixel positions for each of the supported shade states. */
-private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> {
- return mapOf(
- 0f to ShadeState.FullyCollapsed,
- containerHeightPx to ShadeState.FullyExpanded,
- )
-}
-
-/**
- * Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it
- * actually completes the expansion or collapse after the user lifts their pointer.
- */
-private fun swipeableThresholds(
- to: ShadeState,
- swipeExpandThreshold: Dp,
- swipeCollapseThreshold: Dp,
-): ThresholdConfig {
- return FixedThreshold(
- when (to) {
- ShadeState.FullyExpanded -> swipeExpandThreshold
- ShadeState.FullyCollapsed -> swipeCollapseThreshold
- }
- )
-}
-
-/** Enumerates the shade UI states for [SwipeableState]. */
-private enum class ShadeState {
- FullyCollapsed,
- FullyExpanded,
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 5e0761063af2..32986649388d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -92,6 +92,14 @@ private fun Scene(
onSceneChanged: (SceneModel) -> Unit,
modifier: Modifier = Modifier,
) {
+ val destinationScenes: Map<UserAction, SceneModel> by
+ scene.destinationScenes(containerName).collectAsState()
+ val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
+ val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
+ val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
+ val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
+ val backDestinationScene = destinationScenes[UserAction.Back]
+
// TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress.
Box(modifier) {
Column(
@@ -103,14 +111,6 @@ private fun Scene(
modifier = Modifier,
)
- val destinationScenes: Map<UserAction, SceneModel> by
- scene.destinationScenes(containerName).collectAsState()
- val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
- val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
- val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
- val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
- val backDestinationScene = destinationScenes[UserAction.Back]
-
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index 95a9ce960dcd..d43276c00f87 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -187,6 +187,9 @@ object CustomizationProviderContract {
/** Flag denoting transit clock are enabled in wallpaper picker. */
const val FLAG_NAME_TRANSIT_CLOCK = "lockscreen_custom_transit_clock"
+ /** Flag denoting transit clock are enabled in wallpaper picker. */
+ const val FLAG_NAME_PAGE_TRANSITIONS = "wallpaper_picker_page_transitions"
+
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
new file mode 100644
index 000000000000..40bab5ed08f2
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorPrimary"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorPrimary"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml
new file mode 100644
index 000000000000..e76ad991a92c
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorOnPrimary"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorOnPrimary"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml
new file mode 100644
index 000000000000..16076b17a6e5
--- /dev/null
+++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml
new file mode 100644
index 000000000000..309770ddd76d
--- /dev/null
+++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
index 54bdf18e3076..bc1775ee64ae 100644
--- a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
+++ b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
@@ -13,30 +14,22 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="56dp"
- android:height="24dp"
- android:viewportWidth="56"
- android:viewportHeight="24">
- <path
- android:pathData="M12,0L44,0A12,12 0,0 1,56 12L56,12A12,12 0,0 1,44 24L12,24A12,12 0,0 1,0 12L0,12A12,12 0,0 1,12 0z"
- android:fillColor="#ffffff"/>
- <group
- android:scaleX="0.8"
- android:scaleY="0.8"
- android:translateY="2"
- android:translateX="18">
- <path
- android:pathData="M21.5,9C22.3284,9 23,8.3284 23,7.5C23,6.6716 22.3284,6 21.5,6C20.6716,6 20,6.6716 20,7.5C20,8.3284 20.6716,9 21.5,9Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M17,14C18.6569,14 20,12.6569 20,11C20,9.3432 18.6569,8 17,8C15.3431,8 14,9.3432 14,11C14,12.6569 15.3431,14 17,14Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M17,22C18.933,22 20.5,20.433 20.5,18.5C20.5,16.567 18.933,15 17,15C15.067,15 13.5,16.567 13.5,18.5C13.5,20.433 15.067,22 17,22Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M7,14C10.3137,14 13,11.3137 13,8C13,4.6863 10.3137,2 7,2C3.6863,2 1,4.6863 1,8C1,11.3137 3.6863,14 7,14Z"
- android:fillColor="#000000"/>
- </group>
-</vector> \ No newline at end of file
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@id/background"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <size
+ android:height="24px"
+ android:width="24px"
+ />
+ <solid android:color="#FFFFFFFF" />
+ </shape>
+ </item>
+ <item android:id="@id/icon"
+ android:gravity="center"
+ android:width="20px"
+ android:height="20px"
+ android:drawable="@drawable/ic_person_outline"
+ />
+</layer-list> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_person_outline.xml b/packages/SystemUI/res/drawable/ic_person_outline.xml
new file mode 100644
index 000000000000..d94714e0d51a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_person_outline.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720L480,720Z"/>
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
index 4029702ec6b4..32e88ab22b91 100644
--- a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
+++ b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
- <solid android:color="@android:color/white" />
+ <solid android:color="?android:attr/colorBackground" />
<size
android:height="56dp"
diff --git a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
index e3c7d0ce89aa..12c3e23bf0a0 100644
--- a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
+++ b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
- <solid android:color="#80ffffff" />
+ <solid android:color="?android:attr/colorBackground" />
<size
android:height="76dp"
diff --git a/packages/SystemUI/res/drawable/media_output_icon_volume.xml b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
index fce4e0022c7a..85d608fa736f 100644
--- a/packages/SystemUI/res/drawable/media_output_icon_volume.xml
+++ b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
@@ -3,7 +3,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
- android:tint="?attr/colorControlNormal">
+ android:tint="?attr/colorControlNormal"
+ android:autoMirrored="true">
<path
android:fillColor="@color/media_dialog_item_main_content"
android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.125,8.475 15.812,9.575Q16.5,10.675 16.5,12Q16.5,13.325 15.812,14.4Q15.125,15.475 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/>
diff --git a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
index b93793773179..a8779002c1b3 100644
--- a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
+++ b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
@@ -17,9 +17,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
- android:bottomLeftRadius="28dp"
- android:topLeftRadius="28dp"
- android:bottomRightRadius="0dp"
- android:topRightRadius="0dp"/>
+ android:bottomLeftRadius="@dimen/media_output_dialog_icon_left_radius"
+ android:topLeftRadius="@dimen/media_output_dialog_icon_left_radius"
+ android:bottomRightRadius="@dimen/media_output_dialog_icon_right_radius"
+ android:topRightRadius="@dimen/media_output_dialog_icon_right_radius"/>
<solid android:color="@color/media_dialog_item_background" />
</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml
new file mode 100644
index 000000000000..f63c2ffbfdad
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/background">
+ <shape
+ android:shape="oval"
+ android:id="@id/background"
+ android:gravity="center">
+ <size
+ android:height="24dp"
+ android:width="24dp"/>
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml
new file mode 100644
index 000000000000..5d5529ff1a50
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml
new file mode 100644
index 000000000000..310b0becf10c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml
new file mode 100644
index 000000000000..e89bdd31ca17
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml
new file mode 100644
index 000000000000..fcf0b1c5091f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
new file mode 100644
index 000000000000..b9f5d60abd8b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9.55,18l-5.7,-5.7 1.425,-1.425L9.55,15.15l9.175,-9.175L20.15,7.4z"/>
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml
new file mode 100644
index 000000000000..ea8f9c2839ec
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M26.0,14.0l-4.0,0.0l0.0,4.0l4.0,0.0l0.0,-4.0zm0.0,8.0l-4.0,0.0l0.0,12.0l4.0,0.0L26.0,22.0zm8.0,-19.98L14.0,2.0c-2.21,0.0 -4.0,1.79 -4.0,4.0l0.0,36.0c0.0,2.21 1.79,4.0 4.0,4.0l20.0,0.0c2.21,0.0 4.0,-1.79 4.0,-4.0L38.0,6.0c0.0,-2.21 -1.79,-3.98 -4.0,-3.98zM34.0,38.0L14.0,38.0L14.0,10.0l20.0,0.0l0.0,28.0z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml
new file mode 100644
index 000000000000..f8b99f4a0ee4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml
new file mode 100644
index 000000000000..ae60d517ceb4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
index c4e45bf2c223..9bc8b53b308e 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
@@ -15,7 +15,6 @@
~ limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:insetTop="@dimen/dialog_button_vertical_inset"
android:insetBottom="@dimen/dialog_button_vertical_inset">
<ripple android:color="?android:attr/colorControlHighlight">
@@ -28,7 +27,7 @@
<item>
<shape android:shape="rectangle">
<corners android:radius="?android:attr/buttonCornerRadius"/>
- <solid android:color="?androidprv:attr/materialColorPrimary"/>
+ <solid android:color="@color/qs_dialog_btn_filled_background"/>
<padding android:left="@dimen/dialog_button_horizontal_padding"
android:top="@dimen/dialog_button_vertical_padding"
android:right="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index bb32022a0b5f..82410703c9e6 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -110,7 +110,7 @@
<ImageView
android:id="@+id/dream_overlay_assistant_attention_indicator"
- android:layout_width="@dimen/dream_overlay_grey_chip_width"
+ android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
android:src="@drawable/dream_overlay_assistant_attention_indicator"
diff --git a/packages/SystemUI/res/layout/immersive_mode_cling.xml b/packages/SystemUI/res/layout/immersive_mode_cling.xml
index bfb8184ee044..e6529b9aa9a1 100644
--- a/packages/SystemUI/res/layout/immersive_mode_cling.xml
+++ b/packages/SystemUI/res/layout/immersive_mode_cling.xml
@@ -58,7 +58,7 @@
android:paddingStart="48dp"
android:paddingTop="40dp"
android:text="@string/immersive_cling_title"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="24sp" />
<TextView
@@ -70,7 +70,7 @@
android:paddingStart="48dp"
android:paddingTop="12.6dp"
android:text="@string/immersive_cling_description"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="16sp" />
<Button
@@ -85,7 +85,7 @@
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:text="@string/immersive_cling_positive"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="14sp" />
</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/privacy_dialog_card_button.xml b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml
new file mode 100644
index 000000000000..e297b939e2b8
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_marginBottom="4dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"
+ style="@style/Widget.Dialog.Button.BorderButton"/> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
new file mode 100644
index 000000000000..b84f3a9794be
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
@@ -0,0 +1,89 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/privacy_dialog_item_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:foreground="?android:attr/selectableItemBackground"
+ app:cardCornerRadius="28dp"
+ app:cardElevation="0dp"
+ app:cardBackgroundColor="?androidprv:attr/materialColorSurfaceBright">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/privacy_dialog_item_header"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="20dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp">
+ <ImageView
+ android:id="@+id/privacy_dialog_item_header_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:importantForAccessibility="no" />
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_centerVertical="true">
+ <TextView
+ android:id="@+id/privacy_dialog_item_header_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:hyphenationFrequency="normalFast"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Title" />
+ <TextView
+ android:id="@+id/privacy_dialog_item_header_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Summary" />
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/privacy_dialog_item_header_expand_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:visibility="gone" />
+ </LinearLayout>
+ <LinearLayout
+ android:id="@+id/privacy_dialog_item_header_expanded_layout"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:visibility="gone">
+ </LinearLayout>
+ </LinearLayout>
+</androidx.cardview.widget.CardView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_v2.xml
new file mode 100644
index 000000000000..843dad03bca4
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_v2.xml
@@ -0,0 +1,109 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.core.widget.NestedScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="@dimen/large_dialog_width"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:orientation="vertical">
+
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:fontFamily="@*android:string/config_headlineFontFamily"
+ android:text="@string/privacy_dialog_title"
+ android:layout_marginBottom="12dp"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_summary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ android:gravity="center"
+ android:layout_marginBottom="20dp"/>
+ </LinearLayout>
+
+ <!-- Items -->
+ <LinearLayout
+ android:id="@+id/privacy_dialog_items_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:orientation="vertical"
+ />
+
+ <!-- Buttons -->
+ <LinearLayout
+ android:id="@+id/button_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="18dp"
+ android:clickable="false"
+ android:focusable="false">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="start|center_vertical"
+ android:orientation="vertical">
+ <Button
+ android:id="@+id/privacy_dialog_more_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_more_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|center_vertical">
+ <Button
+ android:id="@+id/privacy_dialog_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_done_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"/>
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+</androidx.core.widget.NestedScrollView> \ No newline at end of file
diff --git a/packages/SystemUI/res/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml
new file mode 100644
index 000000000000..0d99b617819b
--- /dev/null
+++ b/packages/SystemUI/res/values-ldrtl/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <dimen name="media_output_dialog_icon_left_radius">0dp</dimen>
+ <dimen name="media_output_dialog_icon_right_radius">28dp</dimen>
+</resources> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index db7eb7a049e7..ab754985e11d 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -16,7 +16,8 @@
* limitations under the License.
*/
-->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<drawable name="notification_number_text_color">#ffffffff</drawable>
<drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
<color name="system_bar_background_opaque">#ff000000</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3366f4f6d443..5a15dcec5223 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1097,6 +1097,9 @@
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
+ <dimen name="privacy_dialog_background_radius_large">12dp</dimen>
+ <dimen name="privacy_dialog_background_radius_small">4dp</dimen>
+
<!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_padding">16dp</dimen>
<dimen name="qs_media_album_radius">14dp</dimen>
@@ -1346,6 +1349,8 @@
<dimen name="media_output_dialog_default_margin_end">16dp</dimen>
<dimen name="media_output_dialog_selectable_margin_end">80dp</dimen>
<dimen name="media_output_dialog_list_padding_top">8dp</dimen>
+ <dimen name="media_output_dialog_icon_left_radius">28dp</dimen>
+ <dimen name="media_output_dialog_icon_right_radius">0dp</dimen>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
like "expand". -->
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 3a2177a0045c..15ca9d48c62a 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -214,4 +214,8 @@
<item type="id" name="nssl_guideline" />
<item type="id" name="lock_icon" />
<item type="id" name="lock_icon_bg" />
+
+ <!-- Privacy dialog -->
+ <item type="id" name="privacy_dialog_close_app_button" />
+ <item type="id" name="privacy_dialog_manage_app_button" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f8c13b008fd9..983b09f957ae 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -369,6 +369,8 @@
<string name="biometric_dialog_tap_confirm_with_face_alt_3">Face recognized. Press the unlock icon to continue.</string>
<!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
<string name="biometric_dialog_authenticated">Authenticated</string>
+ <!-- Talkback string when a canceling authentication [CHAR LIMIT=NONE] -->
+ <string name="biometric_dialog_cancel_authentication">Cancel Authentication</string>
<!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] -->
<string name="biometric_dialog_use_pin">Use PIN</string>
@@ -953,6 +955,8 @@
<!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
<string name="keyguard_retry">Swipe up to try again</string>
+ <!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
+ <string name="accesssibility_keyguard_retry">Swipe up to try Face Unlock again</string>
<!-- Message shown when notifying user to unlock in order to use NFC. [CHAR LIMIT=60] -->
<string name="require_unlock_for_nfc">Unlock to use NFC</string>
@@ -2582,6 +2586,9 @@
<!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] -->
<string name="controls_structure_tooltip">Swipe to see more</string>
+ <!-- Accessibility action informing the user how they can retry face authentication [CHAR LIMIT=NONE] -->
+ <string name="retry_face">Retry face authentication</string>
+
<!-- Message to tell the user to wait while systemui attempts to load a set of
recommended controls [CHAR_LIMIT=60] -->
<string name="controls_seeding_in_progress">Loading recommendations</string>
@@ -3167,10 +3174,43 @@
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
- <!--
- Label for a button that, when clicked, sends the user to the app store to install an app.
-
- [CHAR LIMIT=64].
- -->
+ <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
<string name="install_app">Install app</string>
+
+ <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] -->
+ <string name="privacy_dialog_title">Microphone &amp; Camera</string>
+ <!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_summary">Recent app use</string>
+ <!-- Label of the secondary button of the privacy dialog, used to check recent app usage of phone sensors [CHAR LIMIT=30] -->
+ <string name="privacy_dialog_more_button">See recent access</string>
+ <!-- Label of the primary button to dismiss the privacy dialog [CHAR LIMIT=20] -->
+ <string name="privacy_dialog_done_button">Done</string>
+ <!-- Description for expanding a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_expand_action">Expand and show options</string>
+ <!-- Description for collapsing a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_collapse_action">Collapse</string>
+ <!-- Label of a button of the privacy dialog to close an app actively using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_close_app_button">Close this app</string>
+ <!-- Message shown in the privacy dialog when an app actively using a phone sensor is closed [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_close_app_message"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> closed</string>
+ <!-- Label of a button of the privacy dialog to learn more of a service actively or recently using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_manage_service">Manage service</string>
+ <!-- Label of a button of the privacy dialog to manage permissions of an app actively or recently using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_manage_permissions">Manage access</string>
+ <!-- Label for active usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_call_usage">In use by phone call</string>
+ <!-- Label for recent usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_call_usage">Recently used in phone call</string>
+ <!-- Label for active app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
+ <!-- Label for recent app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
+ <!-- Label for active app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage_1">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
+ <!-- Label for recent app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage_1">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
+ <!-- Label for active app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+ <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
</resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 31f40e99e91c..d520670ec012 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1116,7 +1116,7 @@
<style name="Widget.Dialog.Button">
<item name="android:buttonCornerRadius">28dp</item>
<item name="android:background">@drawable/qs_dialog_btn_filled</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
+ <item name="android:textColor">@color/qs_dialog_btn_filled_text_color</item>
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20sp</item>
<item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
@@ -1439,4 +1439,22 @@
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
</style>
+
+ <style name="TextAppearance.PrivacyDialog.Item.Title"
+ parent="@android:style/TextAppearance.DeviceDefault.Medium">
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ </style>
+
+ <style name="TextAppearance.PrivacyDialog.Item.Summary"
+ parent="@android:style/TextAppearance.DeviceDefault.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+ </style>
+
+ <style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
+ <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index d8085b9f9f2e..22cdb30376d0 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -20,6 +20,7 @@ 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.ACCESSIBILITY_ACTION
import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED
import com.android.keyguard.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED
import com.android.keyguard.FaceAuthApiRequestReason.Companion.QS_EXPANDED
@@ -71,6 +72,7 @@ import com.android.keyguard.InternalFaceAuthReasons.USER_SWITCHING
NOTIFICATION_PANEL_CLICKED,
QS_EXPANDED,
PICK_UP_GESTURE_TRIGGERED,
+ ACCESSIBILITY_ACTION,
)
annotation class FaceAuthApiRequestReason {
companion object {
@@ -80,6 +82,7 @@ annotation class FaceAuthApiRequestReason {
const val QS_EXPANDED = "Face auth due to QS expansion."
const val PICK_UP_GESTURE_TRIGGERED =
"Face auth due to pickup gesture triggered when the device is awake and not from AOD."
+ const val ACCESSIBILITY_ACTION = "Face auth due to an accessibility action."
}
}
@@ -217,7 +220,8 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) :
@UiEvent(doc = STRONG_AUTH_ALLOWED_CHANGED)
FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED(1255, STRONG_AUTH_ALLOWED_CHANGED),
@UiEvent(doc = NON_STRONG_BIOMETRIC_ALLOWED_CHANGED)
- FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED);
+ FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED),
+ @UiEvent(doc = ACCESSIBILITY_ACTION) FACE_AUTH_ACCESSIBILITY_ACTION(1454, ACCESSIBILITY_ACTION);
override fun getId(): Int = this.id
@@ -233,6 +237,8 @@ private val apiRequestReasonToUiEvent =
FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED,
QS_EXPANDED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED,
PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED,
+ PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED,
+ ACCESSIBILITY_ACTION to FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION,
)
/** Converts the [reason] to the corresponding [FaceAuthUiEvent]. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index f9523370adb1..bc24249b23c9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -68,6 +68,7 @@ import com.android.keyguard.dagger.KeyguardBouncerScope;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.R;
+import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate;
import com.android.systemui.biometrics.SideFpsController;
import com.android.systemui.biometrics.SideFpsUiRequestSource;
import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
@@ -417,9 +418,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
BouncerMessageInteractor bouncerMessageInteractor,
Provider<JavaAdapter> javaAdapter,
UserInteractor userInteractor,
+ FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
Provider<SceneInteractor> sceneInteractor
) {
super(view);
+ view.setAccessibilityDelegate(faceAuthAccessibilityDelegate);
mLockPatternUtils = lockPatternUtils;
mUpdateMonitor = keyguardUpdateMonitor;
mSecurityModel = keyguardSecurityModel;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f5022ab64add..f1cb37c0ae76 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3158,6 +3158,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
return false;
}
+ if (isFaceAuthInteractorEnabled()) {
+ return mFaceAuthInteractor.canFaceAuthRun();
+ }
+
final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive
&& !statusBarShadeLocked;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 2f6a68c3ff8d..03ad132c452c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -54,6 +54,7 @@ import com.android.systemui.recents.Recents;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -188,6 +189,7 @@ public class SystemActions implements CoreStartable {
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
private final NotificationShadeWindowController mNotificationShadeController;
private final ShadeController mShadeController;
+ private final Lazy<ShadeViewController> mShadeViewController;
private final StatusBarWindowCallback mNotificationShadeCallback;
private boolean mDismissNotificationShadeActionRegistered;
@@ -196,12 +198,14 @@ public class SystemActions implements CoreStartable {
UserTracker userTracker,
NotificationShadeWindowController notificationShadeController,
ShadeController shadeController,
+ Lazy<ShadeViewController> shadeViewController,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
Optional<Recents> recentsOptional,
DisplayTracker displayTracker) {
mContext = context;
mUserTracker = userTracker;
mShadeController = shadeController;
+ mShadeViewController = shadeViewController;
mRecentsOptional = recentsOptional;
mDisplayTracker = displayTracker;
mReceiver = new SystemActionsBroadcastReceiver();
@@ -330,8 +334,7 @@ public class SystemActions implements CoreStartable {
final Optional<CentralSurfaces> centralSurfacesOptional =
mCentralSurfacesOptionalLazy.get();
if (centralSurfacesOptional.isPresent()
- && centralSurfacesOptional.get().getShadeViewController() != null
- && centralSurfacesOptional.get().getShadeViewController().isPanelExpanded()
+ && mShadeViewController.get().isPanelExpanded()
&& !centralSurfacesOptional.get().isKeyguardShowing()) {
if (!mDismissNotificationShadeActionRegistered) {
mA11yManager.registerSystemAction(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
index 783460c325fa..0ef256d41157 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
@@ -145,6 +145,8 @@ class FontScalingDialog(
*/
@MainThread
fun updateFontScaleDelayed(delayMsFromSource: Long) {
+ doneButton.isEnabled = false
+
var delayMs = delayMsFromSource
if (systemClock.elapsedRealtime() - lastUpdateTime < MIN_UPDATE_INTERVAL_MS) {
delayMs += MIN_UPDATE_INTERVAL_MS
@@ -197,17 +199,22 @@ class FontScalingDialog(
title.post {
title.setTextAppearance(R.style.TextAppearance_Dialog_Title)
doneButton.setTextAppearance(R.style.Widget_Dialog_Button)
+ doneButton.isEnabled = true
}
}
}
@WorkerThread
fun updateFontScale() {
- systemSettings.putStringForUser(
- Settings.System.FONT_SCALE,
- strEntryValues[lastProgress.get()],
- userTracker.userId
- )
+ if (
+ !systemSettings.putStringForUser(
+ Settings.System.FONT_SCALE,
+ strEntryValues[lastProgress.get()],
+ userTracker.userId
+ )
+ ) {
+ title.post { doneButton.isEnabled = true }
+ }
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index a9779663cc7c..deb3d035d753 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
package com.android.systemui.authentication.data.repository
import com.android.internal.widget.LockPatternChecker
@@ -29,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.time.SystemClock
import dagger.Binds
import dagger.Module
@@ -38,16 +37,14 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** Defines interface for classes that can access authentication-related application state. */
@@ -156,32 +153,18 @@ constructor(
}
override val isAutoConfirmEnabled: StateFlow<Boolean> =
- userRepository.selectedUserInfo
- .map { it.id }
- .flatMapLatest { userId ->
- flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) }
- .flowOn(backgroundDispatcher)
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
+ refreshingFlow(
+ initialValue = false,
+ getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled,
+ )
override val hintedPinLength: Int = 6
override val isPatternVisible: StateFlow<Boolean> =
- userRepository.selectedUserInfo
- .map { it.id }
- .flatMapLatest { userId ->
- flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) }
- .flowOn(backgroundDispatcher)
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = true,
- )
+ refreshingFlow(
+ initialValue = true,
+ getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
+ )
private val _throttling = MutableStateFlow(AuthenticationThrottlingModel())
override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow()
@@ -276,6 +259,48 @@ constructor(
)
}
}
+
+ /**
+ * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is
+ * invoked on a background thread every time the selected user is changed and every time a new
+ * downstream subscriber is added to the flow.
+ *
+ * Initially, the flow will emit [initialValue] while it refreshes itself in the background by
+ * invoking the [getFreshValue] function and emitting the fresh value when that's done.
+ *
+ * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the
+ * new value.
+ *
+ * Every time a new downstream subscriber is added to the flow it first receives the latest
+ * cached value that's either the [initialValue] or the latest previously fetched value. In
+ * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a
+ * subsequent emission of that newest value.
+ */
+ private fun <T> refreshingFlow(
+ initialValue: T,
+ getFreshValue: suspend (selectedUserId: Int) -> T,
+ ): StateFlow<T> {
+ val flow = MutableStateFlow(initialValue)
+ applicationScope.launch {
+ combine(
+ // Emits a value initially and every time the selected user is changed.
+ userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
+ // Emits a value only when the number of downstream subscribers of this flow
+ // increases.
+ flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current)
+ ->
+ current > previous
+ },
+ ) { selectedUserId, _ ->
+ selectedUserId
+ }
+ .collect { selectedUserId ->
+ flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) }
+ }
+ }
+
+ return flow.asStateFlow()
+ }
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index b482977bde67..d4371bf30e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -104,7 +104,9 @@ constructor(
}
.stateIn(
scope = applicationScope,
- started = SharingStarted.Eagerly,
+ // Make sure this is kept as WhileSubscribed or we can run into a bug where the
+ // downstream continues to receive old/stale/cached values.
+ started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 9df56fcce430..58adfa1d882c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -58,6 +58,10 @@ import android.widget.ScrollView;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
@@ -333,6 +337,20 @@ public class AuthContainerView extends LinearLayout
addView(mFrameLayout);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
+ ViewCompat.setAccessibilityDelegate(mBackgroundView, new AccessibilityDelegateCompat() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ mContext.getString(R.string.biometric_dialog_cancel_authentication)
+ )
+ );
+ }
+ });
+
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
new file mode 100644
index 000000000000..b9fa24022ad5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.content.res.Resources
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import com.android.keyguard.FaceAuthApiRequestReason
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import javax.inject.Inject
+
+/**
+ * Accessibility delegate that will add a click accessibility action to a view when face auth can
+ * run. When the click a11y action is triggered, face auth will retry.
+ */
+@SysUISingleton
+class FaceAuthAccessibilityDelegate
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val faceAuthInteractor: KeyguardFaceAuthInteractor,
+) : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ if (keyguardUpdateMonitor.shouldListenForFace()) {
+ val clickActionToRetryFace =
+ AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+ resources.getString(R.string.retry_face)
+ )
+ info.addAction(clickActionToRetryFace)
+ }
+ }
+
+ override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+ return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) {
+ keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION)
+ faceAuthInteractor.onAccessibilityAction()
+ true
+ } else super.performAccessibilityAction(host, action, args)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 083e21fbdfba..37ce44488346 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -17,7 +17,12 @@ package com.android.systemui.biometrics
import android.app.ActivityTaskManager
import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -28,23 +33,27 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController
import android.os.Handler
import android.util.Log
+import android.util.RotationUtils
import android.view.Display
import android.view.DisplayInfo
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
+import android.view.View.AccessibilityDelegate
import android.view.ViewPropertyAnimator
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.RawRes
import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -55,7 +64,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.traceSection
import java.io.PrintWriter
import javax.inject.Inject
-import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -78,7 +86,6 @@ constructor(
@Main private val mainExecutor: DelayableExecutor,
@Main private val handler: Handler,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
- private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
@Application private val scope: CoroutineScope,
dumpManager: DumpManager
) : Dumpable {
@@ -243,15 +250,105 @@ constructor(
private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
overlayView = view
- SideFpsOverlayViewBinder.bind(
- view = view,
- viewModel = sideFpsOverlayViewModelFactory.get(),
- overlayViewParams = overlayViewParams,
- reason = reason,
- context = context,
+ val display = context.display!!
+ // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
+ display.getDisplayInfo(displayInfo)
+ val offsets =
+ sensorProps.getLocation(display.uniqueId).let { location ->
+ if (location == null) {
+ Log.w(TAG, "No location specified for display: ${display.uniqueId}")
+ }
+ location ?: sensorProps.location
+ }
+ overlayOffsets = offsets
+
+ val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+ view.rotation =
+ display.asSideFpsAnimationRotation(
+ offsets.isYAligned(),
+ getRotationFromDefault(displayInfo.rotation)
+ )
+ lottie.setAnimation(
+ display.asSideFpsAnimation(
+ offsets.isYAligned(),
+ getRotationFromDefault(displayInfo.rotation)
+ )
)
+ lottie.addLottieOnCompositionLoadedListener {
+ // Check that view is not stale, and that overlayView has not been hidden/removed
+ if (overlayView != null && overlayView == view) {
+ updateOverlayParams(display, it.bounds)
+ }
+ }
orientationReasonListener.reason = reason
+ lottie.addOverlayDynamicColor(context, reason)
+
+ /**
+ * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
+ * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
+ * in focus
+ */
+ view.setAccessibilityDelegate(
+ object : AccessibilityDelegate() {
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent
+ ): Boolean {
+ return if (
+ event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ ) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
+ )
}
+
+ @VisibleForTesting
+ fun updateOverlayParams(display: Display, bounds: Rect) {
+ val isNaturalOrientation = display.isNaturalOrientation()
+ val isDefaultOrientation =
+ if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
+ val size = windowManager.maximumWindowMetrics.bounds
+
+ val displayWidth = if (isDefaultOrientation) size.width() else size.height()
+ val displayHeight = if (isDefaultOrientation) size.height() else size.width()
+ val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
+ val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
+
+ val sensorBounds =
+ if (overlayOffsets.isYAligned()) {
+ Rect(
+ displayWidth - boundsWidth,
+ overlayOffsets.sensorLocationY,
+ displayWidth,
+ overlayOffsets.sensorLocationY + boundsHeight
+ )
+ } else {
+ Rect(
+ overlayOffsets.sensorLocationX,
+ 0,
+ overlayOffsets.sensorLocationX + boundsWidth,
+ boundsHeight
+ )
+ }
+
+ RotationUtils.rotateBounds(
+ sensorBounds,
+ Rect(0, 0, displayWidth, displayHeight),
+ getRotationFromDefault(display.rotation)
+ )
+
+ overlayViewParams.x = sensorBounds.left
+ overlayViewParams.y = sensorBounds.top
+
+ windowManager.updateViewLayout(overlayView, overlayViewParams)
+ }
+
+ private fun getRotationFromDefault(rotation: Int): Int =
+ if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
}
private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
@@ -276,12 +373,89 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo
private fun ActivityTaskManager.topClass(): String =
getTasks(1).firstOrNull()?.topActivity?.className ?: ""
+@RawRes
+private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
+ when (rotationFromDefault) {
+ Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+ Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+ else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
+ }
+
+private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
+ when (rotationFromDefault) {
+ Surface.ROTATION_90 -> if (yAligned) 0f else 180f
+ Surface.ROTATION_180 -> 180f
+ Surface.ROTATION_270 -> if (yAligned) 180f else 0f
+ else -> 0f
+ }
+
private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
private fun Display.isNaturalOrientation(): Boolean =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-public class OrientationReasonListener(
+private fun LottieAnimationView.addOverlayDynamicColor(
+ context: Context,
+ @BiometricOverlayConstants.ShowReason reason: Int
+) {
+ fun update() {
+ val isKeyguard = reason == REASON_AUTH_KEYGUARD
+ if (isKeyguard) {
+ val color =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorPrimaryFixed
+ )
+ val outerRimColor =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorPrimaryFixedDim
+ )
+ val chevronFill =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorOnPrimaryFixed
+ )
+ addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
+ }
+ addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
+ }
+ addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
+ }
+ } else {
+ if (!isDarkMode(context)) {
+ addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
+ }
+ }
+ for (key in listOf(".blue600", ".blue400")) {
+ addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(
+ context.getColor(R.color.settingslib_color_blue400),
+ PorterDuff.Mode.SRC_ATOP
+ )
+ }
+ }
+ }
+ }
+
+ if (composition != null) {
+ update()
+ } else {
+ addLottieOnCompositionLoadedListener { update() }
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ return darkMode == Configuration.UI_MODE_NIGHT_YES
+}
+
+@VisibleForTesting
+class OrientationReasonListener(
context: Context,
displayManager: DisplayManager,
handler: Handler,
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 a5e846ad61ca..53dc0e3d4846 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -17,6 +17,8 @@
package com.android.systemui.biometrics.dagger
import com.android.settingslib.udfps.UdfpsUtils
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.data.repository.FacePropertyRepositoryImpl
import com.android.systemui.biometrics.data.repository.FaceSettingsRepository
import com.android.systemui.biometrics.data.repository.FaceSettingsRepositoryImpl
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -53,6 +55,10 @@ interface BiometricsModule {
@Binds
@SysUISingleton
+ fun faceSensors(impl: FacePropertyRepositoryImpl): FacePropertyRepository
+
+ @Binds
+ @SysUISingleton
fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
new file mode 100644
index 000000000000..d2cb84945252
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.repository
+
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorStrength
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/** A repository for the global state of Face sensor. */
+interface FacePropertyRepository {
+ /** Face sensor information, null if it is not available. */
+ val sensorInfo: Flow<FaceSensorInfo?>
+}
+
+/** Describes a biometric sensor */
+data class FaceSensorInfo(val id: Int, val strength: SensorStrength)
+
+private const val TAG = "FaceSensorPropertyRepositoryImpl"
+
+@SysUISingleton
+class FacePropertyRepositoryImpl
+@Inject
+constructor(@Application private val applicationScope: CoroutineScope, faceManager: FaceManager?) :
+ FacePropertyRepository {
+
+ private val sensorProps: Flow<List<FaceSensorPropertiesInternal>> =
+ faceManager?.let {
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ val callback =
+ object : IFaceAuthenticatorsRegisteredCallback.Stub() {
+ override fun onAllAuthenticatorsRegistered(
+ sensors: List<FaceSensorPropertiesInternal>
+ ) {
+ trySendWithFailureLogging(
+ sensors,
+ TAG,
+ "onAllAuthenticatorsRegistered"
+ )
+ }
+ }
+ it.addAuthenticatorsRegisteredCallback(callback)
+ awaitClose {}
+ }
+ .shareIn(applicationScope, SharingStarted.Eagerly)
+ }
+ ?: flowOf(emptyList())
+
+ override val sensorInfo: Flow<FaceSensorInfo?> =
+ sensorProps
+ .map { it.firstOrNull() }
+ .map { it?.let { FaceSensorInfo(it.sensorId, it.sensorStrength.toSensorStrength()) } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index efbde4c5985b..daff5feb0123 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -16,15 +16,13 @@
package com.android.systemui.biometrics.data.repository
-import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintManager
-import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorStrength
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -33,8 +31,10 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn
/**
@@ -44,17 +44,22 @@ import kotlinx.coroutines.flow.shareIn
*/
interface FingerprintPropertyRepository {
+ /**
+ * If the repository is initialized or not. Other properties are defaults until this is true.
+ */
+ val isInitialized: Flow<Boolean>
+
/** The id of fingerprint sensor. */
- val sensorId: Flow<Int>
+ val sensorId: StateFlow<Int>
/** The security strength of sensor (convenience, weak, strong). */
- val strength: Flow<SensorStrength>
+ val strength: StateFlow<SensorStrength>
/** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
- val sensorType: Flow<FingerprintSensorType>
+ val sensorType: StateFlow<FingerprintSensorType>
/** The sensor location relative to each physical display. */
- val sensorLocations: Flow<Map<String, SensorLocationInternal>>
+ val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
}
@SysUISingleton
@@ -62,10 +67,10 @@ class FingerprintPropertyRepositoryImpl
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val fingerprintManager: FingerprintManager?
+ private val fingerprintManager: FingerprintManager?,
) : FingerprintPropertyRepository {
- private val props: Flow<FingerprintSensorPropertiesInternal> =
+ override val isInitialized: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -73,56 +78,45 @@ constructor(
sensors: List<FingerprintSensorPropertiesInternal>
) {
if (sensors.isNotEmpty()) {
- trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
- } else {
- trySendWithFailureLogging(
- DEFAULT_PROPS,
- TAG,
- "initialize with default properties"
- )
+ setProperties(sensors[0])
+ trySendWithFailureLogging(true, TAG, "initialize properties")
}
}
}
fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
- trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
+ trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
awaitClose {}
}
.shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
- override val sensorId: Flow<Int> = props.map { it.sensorId }
- override val strength: Flow<SensorStrength> =
- props.map { sensorStrengthIntToObject(it.sensorStrength) }
- override val sensorType: Flow<FingerprintSensorType> =
- props.map { sensorTypeIntToObject(it.sensorType) }
- override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
- props.map {
- it.allLocations.associateBy { sensorLocationInternal ->
+ private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
+ override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
+
+ private val _strength: MutableStateFlow<SensorStrength> =
+ MutableStateFlow(SensorStrength.CONVENIENCE)
+ override val strength = _strength.asStateFlow()
+
+ private val _sensorType: MutableStateFlow<FingerprintSensorType> =
+ MutableStateFlow(FingerprintSensorType.UNKNOWN)
+ override val sensorType = _sensorType.asStateFlow()
+
+ private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
+ MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
+ override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+ _sensorLocations.asStateFlow()
+
+ private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
+ _sensorId.value = prop.sensorId
+ _strength.value = prop.sensorStrength.toSensorStrength()
+ _sensorType.value = sensorTypeIntToObject(prop.sensorType)
+ _sensorLocations.value =
+ prop.allLocations.associateBy { sensorLocationInternal ->
sensorLocationInternal.displayId
}
- }
+ }
companion object {
private const val TAG = "FingerprintPropertyRepositoryImpl"
- private val DEFAULT_PROPS =
- FingerprintSensorPropertiesInternal(
- -1 /* sensorId */,
- SensorProperties.STRENGTH_CONVENIENCE,
- 0 /* maxEnrollmentsPerUser */,
- listOf<ComponentInfoInternal>(),
- FingerprintSensorProperties.TYPE_UNKNOWN,
- false /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
- )
- }
-}
-
-private fun sensorStrengthIntToObject(value: Int): SensorStrength {
- return when (value) {
- 0 -> SensorStrength.CONVENIENCE
- 1 -> SensorStrength.WEAK
- 2 -> SensorStrength.STRONG
- else -> throw IllegalArgumentException("Invalid SensorStrength value: $value")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
index 37f39cb5fe0e..aa85e5f3b21a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
@@ -17,24 +17,16 @@
package com.android.systemui.biometrics.domain.interactor
import android.hardware.biometrics.SensorLocationInternal
+import android.util.Log
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
/** Business logic for SideFps overlay offsets. */
interface SideFpsOverlayInteractor {
- /** The displayId of the current display. */
- val displayId: Flow<String>
- /** The corresponding offsets based on different displayId. */
- val overlayOffsets: Flow<SensorLocationInternal>
-
- /** Update the displayId. */
- fun changeDisplay(displayId: String?)
+ /** Get the corresponding offsets based on different displayId. */
+ fun getOverlayOffsets(displayId: String): SensorLocationInternal
}
@SysUISingleton
@@ -43,16 +35,14 @@ class SideFpsOverlayInteractorImpl
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
SideFpsOverlayInteractor {
- private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
- override val displayId: Flow<String> = _displayId.asStateFlow()
-
- override val overlayOffsets: Flow<SensorLocationInternal> =
- combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
- offsets[displayId] ?: SensorLocationInternal.DEFAULT
+ override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
+ val offsets = fingerprintPropertyRepository.sensorLocations.value
+ return if (offsets.containsKey(displayId)) {
+ offsets[displayId]!!
+ } else {
+ Log.w(TAG, "No location specified for display: $displayId")
+ offsets[""]!!
}
-
- override fun changeDisplay(displayId: String?) {
- _displayId.value = displayId ?: ""
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
index 2982d0be3764..30e865eff8b8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
@@ -18,9 +18,18 @@ package com.android.systemui.biometrics.shared.model
import android.hardware.biometrics.SensorProperties
-/** Fingerprint sensor security strength. Represents [SensorProperties.Strength]. */
+/** Sensor security strength. Represents [SensorProperties.Strength]. */
enum class SensorStrength {
CONVENIENCE,
WEAK,
STRONG,
}
+
+/** Convert [this] to corresponding [SensorStrength] */
+fun Int.toSensorStrength(): SensorStrength =
+ when (this) {
+ 0 -> SensorStrength.CONVENIENCE
+ 1 -> SensorStrength.WEAK
+ 2 -> SensorStrength.STRONG
+ else -> throw IllegalArgumentException("Invalid SensorStrength value: $this")
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 7ae1443203bb..9bbf1ef04481 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -305,6 +305,10 @@ object BiometricViewBinder {
.collect { onClick ->
iconViewOverlay.setOnClickListener(onClick)
iconView.setOnClickListener(onClick)
+ if (onClick == null) {
+ iconViewOverlay.isClickable = false
+ iconView.isClickable = false
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
deleted file mode 100644
index 0409519c9816..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.ui.binder
-
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Color
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import android.hardware.biometrics.BiometricOverlayConstants
-import android.view.View
-import android.view.WindowManager
-import android.view.accessibility.AccessibilityEvent
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.model.KeyPath
-import com.android.systemui.R
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.launch
-
-/** Sub-binder for SideFpsOverlayView. */
-object SideFpsOverlayViewBinder {
-
- /** Bind the view. */
- @JvmStatic
- fun bind(
- view: View,
- viewModel: SideFpsOverlayViewModel,
- overlayViewParams: WindowManager.LayoutParams,
- @BiometricOverlayConstants.ShowReason reason: Int,
- @Application context: Context
- ) {
- val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
- val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
-
- viewModel.changeDisplay()
-
- view.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
- viewModel.sideFpsAnimationRotation.collect { rotation ->
- view.rotation = rotation
- }
- }
-
- launch {
- // TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation
- // in order to add scuba tests in the future.
- viewModel.sideFpsAnimation.collect { animation ->
- lottie.setAnimation(animation)
- }
- }
-
- launch {
- viewModel.sensorBounds.collect { sensorBounds ->
- overlayViewParams.x = sensorBounds.left
- overlayViewParams.y = sensorBounds.top
-
- windowManager.updateViewLayout(view, overlayViewParams)
- }
- }
-
- launch {
- viewModel.overlayOffsets.collect { overlayOffsets ->
- lottie.addLottieOnCompositionLoadedListener {
- viewModel.updateSensorBounds(
- it.bounds,
- windowManager.maximumWindowMetrics.bounds,
- overlayOffsets
- )
- }
- }
- }
- }
- }
-
- lottie.addOverlayDynamicColor(context, reason)
-
- /**
- * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
- * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
- * in focus
- */
- view.accessibilityDelegate =
- object : View.AccessibilityDelegate() {
- override fun dispatchPopulateAccessibilityEvent(
- host: View,
- event: AccessibilityEvent
- ): Boolean {
- return if (
- event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- ) {
- true
- } else {
- super.dispatchPopulateAccessibilityEvent(host, event)
- }
- }
- }
- }
-}
-
-private fun LottieAnimationView.addOverlayDynamicColor(
- context: Context,
- @BiometricOverlayConstants.ShowReason reason: Int
-) {
- fun update() {
- val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD
- if (isKeyguard) {
- val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color
- val chevronFill =
- com.android.settingslib.Utils.getColorAttrDefaultColor(
- context,
- android.R.attr.textColorPrimaryInverse
- )
- for (key in listOf(".blue600", ".blue400")) {
- addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
- }
- }
- addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
- }
- } else if (!isDarkMode(context)) {
- addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
- }
- } else if (isDarkMode(context)) {
- for (key in listOf(".blue600", ".blue400")) {
- addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(
- context.getColor(R.color.settingslib_color_blue400),
- PorterDuff.Mode.SRC_ATOP
- )
- }
- }
- }
- }
-
- if (composition != null) {
- update()
- } else {
- addLottieOnCompositionLoadedListener { update() }
- }
-}
-
-private fun isDarkMode(context: Context): Boolean {
- val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
- return darkMode == Configuration.UI_MODE_NIGHT_YES
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
deleted file mode 100644
index e938b4efb68c..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.ui.viewmodel
-
-import android.content.Context
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.util.RotationUtils
-import android.view.Display
-import android.view.DisplayInfo
-import android.view.Surface
-import androidx.annotation.RawRes
-import com.android.systemui.R
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.dagger.qualifiers.Application
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.map
-
-/** View-model for SideFpsOverlayView. */
-class SideFpsOverlayViewModel
-@Inject
-constructor(
- @Application private val context: Context,
- private val sideFpsOverlayInteractor: SideFpsOverlayInteractor,
-) {
-
- private val isReverseDefaultRotation =
- context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
-
- private val _sensorBounds: MutableStateFlow<Rect> = MutableStateFlow(Rect())
- val sensorBounds = _sensorBounds.asStateFlow()
-
- val overlayOffsets: Flow<SensorLocationInternal> = sideFpsOverlayInteractor.overlayOffsets
-
- /** Update the displayId. */
- fun changeDisplay() {
- sideFpsOverlayInteractor.changeDisplay(context.display!!.uniqueId)
- }
-
- /** Determine the rotation of the sideFps animation given the overlay offsets. */
- val sideFpsAnimationRotation: Flow<Float> =
- overlayOffsets.map { overlayOffsets ->
- val display = context.display!!
- val displayInfo: DisplayInfo = DisplayInfo()
- // b/284098873 `context.display.rotation` may not up-to-date, we use
- // displayInfo.rotation
- display.getDisplayInfo(displayInfo)
- val yAligned: Boolean = overlayOffsets.isYAligned()
- when (getRotationFromDefault(displayInfo.rotation)) {
- Surface.ROTATION_90 -> if (yAligned) 0f else 180f
- Surface.ROTATION_180 -> 180f
- Surface.ROTATION_270 -> if (yAligned) 180f else 0f
- else -> 0f
- }
- }
-
- /** Populate the sideFps animation from the overlay offsets. */
- @RawRes
- val sideFpsAnimation: Flow<Int> =
- overlayOffsets.map { overlayOffsets ->
- val display = context.display!!
- val displayInfo: DisplayInfo = DisplayInfo()
- // b/284098873 `context.display.rotation` may not up-to-date, we use
- // displayInfo.rotation
- display.getDisplayInfo(displayInfo)
- val yAligned: Boolean = overlayOffsets.isYAligned()
- when (getRotationFromDefault(displayInfo.rotation)) {
- Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
- Surface.ROTATION_180 ->
- if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
- else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
- }
- }
-
- /**
- * Calculate and update the bounds of the sensor based on the bounds of the overlay view, the
- * maximum bounds of the window, and the offsets of the sensor location.
- */
- fun updateSensorBounds(
- bounds: Rect,
- maximumWindowBounds: Rect,
- offsets: SensorLocationInternal
- ) {
- val isNaturalOrientation = context.display!!.isNaturalOrientation()
- val isDefaultOrientation =
- if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
-
- val displayWidth =
- if (isDefaultOrientation) maximumWindowBounds.width() else maximumWindowBounds.height()
- val displayHeight =
- if (isDefaultOrientation) maximumWindowBounds.height() else maximumWindowBounds.width()
- val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
- val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
-
- val sensorBounds =
- if (offsets.isYAligned()) {
- Rect(
- displayWidth - boundsWidth,
- offsets.sensorLocationY,
- displayWidth,
- offsets.sensorLocationY + boundsHeight
- )
- } else {
- Rect(
- offsets.sensorLocationX,
- 0,
- offsets.sensorLocationX + boundsWidth,
- boundsHeight
- )
- }
-
- val displayInfo: DisplayInfo = DisplayInfo()
- context.display!!.getDisplayInfo(displayInfo)
-
- RotationUtils.rotateBounds(
- sensorBounds,
- Rect(0, 0, displayWidth, displayHeight),
- getRotationFromDefault(displayInfo.rotation)
- )
-
- _sensorBounds.value = sensorBounds
- }
-
- private fun getRotationFromDefault(rotation: Int): Int =
- if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
-}
-
-private fun Display.isNaturalOrientation(): Boolean =
- rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-
-private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 1b14acc7fabc..844cf024ef71 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -40,9 +40,10 @@ class PinBouncerViewModel(
) {
val pinShapes = PinShapeAdapter(applicationContext)
+ private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
- private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList())
- val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries
+ /** Currently entered pin keys. */
+ val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
/** The length of the PIN for which we should show a hint. */
val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength
@@ -50,17 +51,19 @@ class PinBouncerViewModel(
/** Appearance of the backspace button. */
val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
combine(
- mutablePinEntries,
+ mutablePinInput,
interactor.isAutoConfirmEnabled,
) { mutablePinEntries, isAutoConfirmEnabled ->
computeBackspaceButtonAppearance(
- enteredPin = mutablePinEntries,
+ pinInput = mutablePinEntries,
isAutoConfirmEnabled = isAutoConfirmEnabled,
)
}
.stateIn(
scope = applicationScope,
- started = SharingStarted.Eagerly,
+ // Make sure this is kept as WhileSubscribed or we can run into a bug where the
+ // downstream continues to receive old/stale/cached values.
+ started = SharingStarted.WhileSubscribed(),
initialValue = ActionButtonAppearance.Hidden,
)
@@ -87,26 +90,23 @@ class PinBouncerViewModel(
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
- if (mutablePinEntries.value.isEmpty()) {
+ val pinInput = mutablePinInput.value
+ if (pinInput.isEmpty()) {
interactor.clearMessage()
}
- mutablePinEntries.value += EnteredKey(input)
-
+ mutablePinInput.value = pinInput.append(input)
tryAuthenticate(useAutoConfirm = true)
}
/** Notifies that the user clicked the backspace button. */
fun onBackspaceButtonClicked() {
- if (mutablePinEntries.value.isEmpty()) {
- return
- }
- mutablePinEntries.value = mutablePinEntries.value.toMutableList().apply { removeLast() }
+ mutablePinInput.value = mutablePinInput.value.deleteLast()
}
/** Notifies that the user long-pressed the backspace button. */
fun onBackspaceButtonLongPressed() {
- mutablePinEntries.value = emptyList()
+ mutablePinInput.value = mutablePinInput.value.clearAll()
}
/** Notifies that the user clicked the "enter" button. */
@@ -115,7 +115,7 @@ class PinBouncerViewModel(
}
private fun tryAuthenticate(useAutoConfirm: Boolean) {
- val pinCode = mutablePinEntries.value.map { it.input }
+ val pinCode = mutablePinInput.value.getPin()
applicationScope.launch {
val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch
@@ -124,15 +124,17 @@ class PinBouncerViewModel(
showFailureAnimation()
}
- mutablePinEntries.value = emptyList()
+ // TODO(b/291528545): this should not be cleared on success (at least until the view
+ // is animated away).
+ mutablePinInput.value = mutablePinInput.value.clearAll()
}
}
private fun computeBackspaceButtonAppearance(
- enteredPin: List<EnteredKey>,
+ pinInput: PinInputViewModel,
isAutoConfirmEnabled: Boolean,
): ActionButtonAppearance {
- val isEmpty = enteredPin.isEmpty()
+ val isEmpty = pinInput.isEmpty()
return when {
isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden
@@ -151,19 +153,3 @@ enum class ActionButtonAppearance {
/** Button is shown. */
Shown,
}
-
-private var nextSequenceNumber = 1
-
-/**
- * The pin bouncer [input] as digits 0-9, together with a [sequenceNumber] to indicate the ordering.
- *
- * Since the model only allows appending/removing [EnteredKey]s from the end, the [sequenceNumber]
- * is strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at
- * a specific number.
- */
-data class EnteredKey
-internal constructor(val input: Int, val sequenceNumber: Int = nextSequenceNumber++) :
- Comparable<EnteredKey> {
- override fun compareTo(other: EnteredKey): Int =
- compareValuesBy(this, other, EnteredKey::sequenceNumber)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt
new file mode 100644
index 000000000000..4efc21b41e6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.bouncer.ui.viewmodel
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+
+/**
+ * Immutable pin input state.
+ *
+ * The input is a hybrid of state ([Digit]) and event ([ClearAll]) tokens. The [ClearAll] token can
+ * be interpreted as a watermark, indicating that the current input up to that point is deleted
+ * (after a auth failure or when long-pressing the delete button). Therefore, [Digit]s following a
+ * [ClearAll] make up the next pin input entry. Up to two complete pin inputs are memoized.
+ *
+ * This is required when auto-confirm rejects the input, and the last digit will be animated-in at
+ * the end of the input, concurrently with the staggered clear-all animation starting to play at the
+ * beginning of the input.
+ *
+ * The input is guaranteed to always contain a initial [ClearAll] token as a sentinel, thus clients
+ * can always assume there is a 'ClearAll' watermark available.
+ */
+data class PinInputViewModel
+internal constructor(
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val input: List<EntryToken>
+) {
+ init {
+ require(input.firstOrNull() is ClearAll) { "input does not begin with a ClearAll token" }
+ require(input.zipWithNext().all { it.first < it.second }) {
+ "EntryTokens are not sorted by their sequenceNumber"
+ }
+ }
+ /**
+ * [PinInputViewModel] with [previousInput] and appended [newToken].
+ *
+ * [previousInput] is trimmed so that the new [PinBouncerViewModel] contains at most two pin
+ * inputs.
+ */
+ private constructor(
+ previousInput: List<EntryToken>,
+ newToken: EntryToken
+ ) : this(
+ buildList {
+ addAll(
+ previousInput.subList(previousInput.indexOfLastClearAllToKeep(), previousInput.size)
+ )
+ add(newToken)
+ }
+ )
+
+ fun append(digit: Int): PinInputViewModel {
+ return PinInputViewModel(input, Digit(digit))
+ }
+
+ /**
+ * Delete last digit.
+ *
+ * This removes the last digit from the input. Returns `this` if the last token is [ClearAll].
+ */
+ fun deleteLast(): PinInputViewModel {
+ if (isEmpty()) return this
+ return PinInputViewModel(input.take(input.size - 1))
+ }
+
+ /**
+ * Appends a [ClearAll] watermark, completing the current pin.
+ *
+ * Returns `this` if the last token is [ClearAll].
+ */
+ fun clearAll(): PinInputViewModel {
+ if (isEmpty()) return this
+ return PinInputViewModel(input, ClearAll())
+ }
+
+ /** Whether the current pin is empty. */
+ fun isEmpty(): Boolean {
+ return input.last() is ClearAll
+ }
+
+ /** The current pin, or an empty list if [isEmpty]. */
+ fun getPin(): List<Int> {
+ return getDigits(mostRecentClearAll()).map { it.input }
+ }
+
+ /**
+ * The digits following the specified [ClearAll] marker, up to the next marker or the end of the
+ * input.
+ *
+ * Returns an empty list if the [ClearAll] is not in the input.
+ */
+ fun getDigits(clearAllMarker: ClearAll): List<Digit> {
+ val startIndex = input.indexOf(clearAllMarker) + 1
+ if (startIndex == 0 || startIndex == input.size) return emptyList()
+
+ return input.subList(startIndex, input.size).takeWhile { it is Digit }.map { it as Digit }
+ }
+
+ /** The most recent [ClearAll] marker. */
+ fun mostRecentClearAll(): ClearAll {
+ return input.last { it is ClearAll } as ClearAll
+ }
+
+ companion object {
+ fun empty() = PinInputViewModel(listOf(ClearAll()))
+ }
+}
+
+/**
+ * Pin bouncer entry token with a [sequenceNumber] to indicate input event ordering.
+ *
+ * Since the model only allows appending/removing [Digit]s from the end, the [sequenceNumber] is
+ * strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at a
+ * specific number.
+ */
+sealed interface EntryToken : Comparable<EntryToken> {
+ val sequenceNumber: Int
+
+ /** The pin bouncer [input] as digits 0-9. */
+ data class Digit
+ internal constructor(val input: Int, override val sequenceNumber: Int = nextSequenceNumber++) :
+ EntryToken {
+ init {
+ check(input in 0..9)
+ }
+ }
+
+ /**
+ * Marker to indicate the input is completely cleared, and subsequent [EntryToken]s mark a new
+ * pin entry.
+ */
+ data class ClearAll
+ internal constructor(override val sequenceNumber: Int = nextSequenceNumber++) : EntryToken
+
+ override fun compareTo(other: EntryToken): Int =
+ compareValuesBy(this, other, EntryToken::sequenceNumber)
+
+ companion object {
+ private var nextSequenceNumber = 1
+ }
+}
+
+/**
+ * Index of the last [ClearAll] token to keep for a new [PinInputViewModel], so that after appending
+ * another [EntryToken], there are at most two pin inputs in the [PinInputViewModel].
+ */
+private fun List<EntryToken>.indexOfLastClearAllToKeep(): Int {
+ require(isNotEmpty() && first() is ClearAll)
+
+ var seenClearAll = 0
+ for (i in size - 1 downTo 0) {
+ if (get(i) is ClearAll) {
+ seenClearAll++
+ if (seenClearAll == 2) {
+ return i
+ }
+ }
+ }
+
+ // The first element is guaranteed to be a ClearAll marker.
+ return 0
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
index e342ac2f320d..566a74ae3e07 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
@@ -17,6 +17,7 @@
package com.android.systemui.clipboardoverlay;
import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -41,10 +42,16 @@ class IntentCreator {
// From the ACTION_SEND docs:
// "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the
// MIME type of the data in EXTRA_STREAM"
- if (clipData.getItemAt(0).getUri() != null) {
- shareIntent.setDataAndType(
- clipData.getItemAt(0).getUri(), clipData.getDescription().getMimeType(0));
- shareIntent.putExtra(Intent.EXTRA_STREAM, clipData.getItemAt(0).getUri());
+ Uri uri = clipData.getItemAt(0).getUri();
+ if (uri != null) {
+ // We don't use setData here because some apps interpret this as "to:".
+ shareIntent.setType(clipData.getDescription().getMimeType(0));
+ // Include URI in ClipData also, so that grantPermission picks it up.
+ shareIntent.setClipData(new ClipData(
+ new ClipDescription(
+ "content", new String[]{clipData.getDescription().getMimeType(0)}),
+ new ClipData.Item(uri)));
+ shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
shareIntent.putExtra(
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index b15c60e62ead..85f31e5e6b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
/**
* A facade to interact with Compose, when it is available.
@@ -64,13 +62,6 @@ interface BaseComposeFacade {
): View
/** Create a [View] to represent [viewModel] on screen. */
- fun createMultiShadeView(
- context: Context,
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- ): View
-
- /** Create a [View] to represent [viewModel] on screen. */
fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index a560accfff68..d9665c5b5047 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -48,6 +48,7 @@ import com.android.systemui.reardisplay.RearDisplayDialogController
import com.android.systemui.recents.Recents
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
+import com.android.systemui.statusbar.ImmersiveModeConfirmation
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.LockscreenWallpaper
@@ -162,6 +163,12 @@ abstract class SystemUICoreStartableModule {
@ClassKey(Recents::class)
abstract fun bindRecents(sysui: Recents): CoreStartable
+ /** Inject into ImmersiveModeConfirmation. */
+ @Binds
+ @IntoMap
+ @ClassKey(ImmersiveModeConfirmation::class)
+ abstract fun bindImmersiveModeConfirmation(sysui: ImmersiveModeConfirmation): CoreStartable
+
/** Inject into RingtonePlayer. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
index 99451f2ee1b9..6f05e83b22ba 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
@@ -37,12 +37,15 @@ import javax.inject.Named;
*/
public class ShadeTouchHandler implements DreamTouchHandler {
private final Optional<CentralSurfaces> mSurfaces;
+ private final ShadeViewController mShadeViewController;
private final int mInitiationHeight;
@Inject
ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces,
+ ShadeViewController shadeViewController,
@Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
mSurfaces = centralSurfaces;
+ mShadeViewController = shadeViewController;
mInitiationHeight = initiationHeight;
}
@@ -54,12 +57,7 @@ public class ShadeTouchHandler implements DreamTouchHandler {
}
session.registerInputListener(ev -> {
- final ShadeViewController viewController =
- mSurfaces.map(CentralSurfaces::getShadeViewController).orElse(null);
-
- if (viewController != null) {
- viewController.handleExternalTouch((MotionEvent) ev);
- }
+ mShadeViewController.handleExternalTouch((MotionEvent) ev);
if (ev instanceof MotionEvent) {
if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index efa5981ef7b2..66813f90bac4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -77,7 +77,7 @@ object Flags {
// TODO(b/278873737): Tracking Bug
@JvmField
val LOAD_NOTIFICATIONS_BEFORE_THE_USER_SWITCH_IS_COMPLETE =
- releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
+ releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
// TODO(b/277338665): Tracking Bug
@JvmField
@@ -92,11 +92,7 @@ object Flags {
// TODO(b/288326013): Tracking Bug
@JvmField
val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
- unreleasedFlag(
- 288326013,
- "notification_async_hybrid_view_inflation",
- teamfood = false
- )
+ unreleasedFlag(288326013, "notification_async_hybrid_view_inflation", teamfood = false)
@JvmField
val ANIMATED_NOTIFICATION_SHADE_INSETS =
@@ -104,18 +100,23 @@ object Flags {
// TODO(b/268005230): Tracking Bug
@JvmField
- val SENSITIVE_REVEAL_ANIM =
- unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
+ val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
// TODO(b/280783617): Tracking Bug
@Keep
@JvmField
val BUILDER_EXTRAS_OVERRIDE =
- sysPropBooleanFlag(
- 128,
- "persist.sysui.notification.builder_extras_override",
- default = false
- )
+ sysPropBooleanFlag(
+ 128,
+ "persist.sysui.notification.builder_extras_override",
+ default = true
+ )
+
+ /** Only notify group expansion listeners when a change happens. */
+ // TODO(b/292213543): Tracking Bug
+ @JvmField
+ val NOTIFICATION_GROUP_EXPANSION_CHANGE =
+ unreleasedFlag(292213543, "notification_group_expansion_change", teamfood = false)
// 200 - keyguard/lockscreen
// ** Flag retired **
@@ -133,16 +134,17 @@ object Flags {
// TODO(b/254512676): Tracking Bug
@JvmField
- val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag(
- 207,
- R.bool.config_enableLockScreenCustomClocks,
- "lockscreen_custom_clocks"
- )
+ val LOCKSCREEN_CUSTOM_CLOCKS =
+ resourceBooleanFlag(
+ 207,
+ R.bool.config_enableLockScreenCustomClocks,
+ "lockscreen_custom_clocks"
+ )
// TODO(b/275694445): Tracking Bug
@JvmField
- val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = releasedFlag(208,
- "lockscreen_without_secure_lock_when_dreaming")
+ val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING =
+ releasedFlag(208, "lockscreen_without_secure_lock_when_dreaming")
// TODO(b/286092087): Tracking Bug
@JvmField
@@ -156,8 +158,7 @@ object Flags {
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
* the digits when the clock moves.
*/
- @JvmField
- val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
+ @JvmField val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
/**
* Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
@@ -183,13 +184,11 @@ object Flags {
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
// TODO(b/262780002): Tracking Bug
- @JvmField
- val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
+ @JvmField val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
// flag for controlling auto pin confirmation and material u shapes in bouncer
@JvmField
- val AUTO_PIN_CONFIRMATION =
- releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
+ val AUTO_PIN_CONFIRMATION = releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
// TODO(b/262859270): Tracking Bug
@JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
@@ -206,20 +205,11 @@ object Flags {
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@JvmField
- val LOCK_SCREEN_LONG_PRESS_ENABLED =
- releasedFlag(
- 228,
- "lock_screen_long_press_enabled"
- )
+ val LOCK_SCREEN_LONG_PRESS_ENABLED = releasedFlag(228, "lock_screen_long_press_enabled")
/** Enables UI updates for AI wallpapers in the wallpaper picker. */
// TODO(b/267722622): Tracking Bug
- @JvmField
- val WALLPAPER_PICKER_UI_FOR_AIWP =
- releasedFlag(
- 229,
- "wallpaper_picker_ui_for_aiwp"
- )
+ @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag(229, "wallpaper_picker_ui_for_aiwp")
/** Whether to use a new data source for intents to run on keyguard dismissal. */
// TODO(b/275069969): Tracking bug.
@@ -232,6 +222,12 @@ object Flags {
val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP =
unreleasedFlag(232, "lock_screen_long_press_directly_opens_wallpaper_picker")
+ /** Whether page transition animations in the wallpaper picker are enabled */
+ // TODO(b/291710220): Tracking bug.
+ @JvmField
+ val WALLPAPER_PICKER_PAGE_TRANSITIONS =
+ unreleasedFlag(291710220, "wallpaper_picker_page_transitions")
+
/** Whether to run the new udfps keyguard refactor code. */
// TODO(b/279440316): Tracking bug.
@JvmField
@@ -239,27 +235,21 @@ object Flags {
/** Provide new auth messages on the bouncer. */
// TODO(b/277961132): Tracking bug.
- @JvmField
- val REVAMPED_BOUNCER_MESSAGES =
- unreleasedFlag(234, "revamped_bouncer_messages")
+ @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages")
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
// TODO(b/279794160): Tracking bug.
- @JvmField
- val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
-
+ @JvmField val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
/** Keyguard Migration */
/** Migrate the indication area to the new keyguard root view. */
// TODO(b/280067944): Tracking bug.
- @JvmField
- val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
+ @JvmField val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
/**
- * Migrate the bottom area to the new keyguard root view.
- * Because there is no such thing as a "bottom area" after this, this also breaks it up into
- * many smaller, modular pieces.
+ * Migrate the bottom area to the new keyguard root view. Because there is no such thing as a
+ * "bottom area" after this, this also breaks it up into many smaller, modular pieces.
*/
// TODO(b/290652751): Tracking bug.
@JvmField
@@ -268,36 +258,29 @@ object Flags {
/** Whether to listen for fingerprint authentication over keyguard occluding activities. */
// TODO(b/283260512): Tracking bug.
- @JvmField
- val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
+ @JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
/** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
// TODO(b/286563884): Tracking bug
- @JvmField
- val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
+ @JvmField val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
// TODO(b/287268101): Tracking bug.
- @JvmField
- val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
+ @JvmField val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
/** Migrate the lock icon view to the new keyguard root view. */
// TODO(b/286552209): Tracking bug.
- @JvmField
- val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
+ @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
// TODO(b/288276738): Tracking bug.
- @JvmField
- val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
+ @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
/** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
// TODO(b/288074305): Tracking bug.
- @JvmField
- val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
+ @JvmField val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
/** Migrate the status view from the notification panel to keyguard root view. */
// TODO(b/291767565): Tracking bug.
- @JvmField
- val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
+ @JvmField val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@@ -316,8 +299,7 @@ object Flags {
// TODO(b/270223352): Tracking Bug
@JvmField
- val HIDE_SMARTSPACE_ON_DREAM_OVERLAY =
- releasedFlag(404, "hide_smartspace_on_dream_overlay")
+ val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag(404, "hide_smartspace_on_dream_overlay")
// TODO(b/271460958): Tracking Bug
@JvmField
@@ -357,8 +339,7 @@ object Flags {
/** Enables Font Scaling Quick Settings tile */
// TODO(b/269341316): Tracking Bug
- @JvmField
- val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
+ @JvmField val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
/** Enables new QS Edit Mode visual refresh */
// TODO(b/269787742): Tracking Bug
@@ -367,13 +348,11 @@ object Flags {
// 600- status bar
-
// TODO(b/265892345): Tracking Bug
val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip")
// TODO(b/280426085): Tracking Bug
- @JvmField
- val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
+ @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
@@ -441,8 +420,8 @@ object Flags {
// TODO(b/273509374): Tracking Bug
@JvmField
- val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006,
- "always_show_home_controls_on_dreams")
+ val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
+ releasedFlag(1006, "always_show_home_controls_on_dreams")
// 1100 - windowing
@Keep
@@ -490,11 +469,7 @@ object Flags {
@Keep
@JvmField
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
- sysPropBooleanFlag(
- 1110,
- "persist.wm.debug.enable_pip_keep_clear_algorithm",
- default = true
- )
+ sysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", default = true)
// TODO(b/256873975): Tracking Bug
@JvmField
@@ -532,7 +507,8 @@ object Flags {
// TODO(b/273443374): Tracking Bug
@Keep
- @JvmField val LOCKSCREEN_LIVE_WALLPAPER =
+ @JvmField
+ val LOCKSCREEN_LIVE_WALLPAPER =
sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
// TODO(b/281648899): Tracking bug
@@ -547,7 +523,6 @@ object Flags {
val ENABLE_PIP2_IMPLEMENTATION =
sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
-
// 1200 - predictive back
@Keep
@JvmField
@@ -573,8 +548,7 @@ object Flags {
unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
// TODO(b/270987164): Tracking Bug
- @JvmField
- val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
+ @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
// TODO(b/263826204): Tracking Bug
@JvmField
@@ -597,8 +571,7 @@ object Flags {
unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
// TODO(b/273800936): Tracking Bug
- @JvmField
- val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
+ @JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
// 1300 - screenshots
// TODO(b/264916608): Tracking Bug
@@ -623,16 +596,12 @@ object Flags {
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// TODO(b/278714186) Tracking Bug
- @JvmField val CLIPBOARD_IMAGE_TIMEOUT =
- unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
+ @JvmField
+ val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
// TODO(b/279405451): Tracking Bug
@JvmField
val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions")
- // 1800 - shade container
- // TODO(b/265944639): Tracking Bug
- @JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade")
-
// TODO(b/283300105): Tracking Bug
@JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
@@ -642,19 +611,15 @@ object Flags {
// 2000 - device controls
@Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels")
- @JvmField
- val APP_PANELS_ALL_APPS_ALLOWED =
- releasedFlag(2001, "app_panels_all_apps_allowed")
+ @JvmField val APP_PANELS_ALL_APPS_ALLOWED = releasedFlag(2001, "app_panels_all_apps_allowed")
@JvmField
- val CONTROLS_MANAGEMENT_NEW_FLOWS =
- releasedFlag(2002, "controls_management_new_flows")
+ val CONTROLS_MANAGEMENT_NEW_FLOWS = releasedFlag(2002, "controls_management_new_flows")
// Enables removing app from Home control panel as a part of a new flow
// TODO(b/269132640): Tracking Bug
@JvmField
- val APP_PANELS_REMOVE_APPS_ALLOWED =
- releasedFlag(2003, "app_panels_remove_apps_allowed")
+ val APP_PANELS_REMOVE_APPS_ALLOWED = releasedFlag(2003, "app_panels_remove_apps_allowed")
// 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.)
// TODO(b/259264861): Tracking Bug
@@ -665,11 +630,9 @@ object Flags {
// 2300 - stylus
@JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used")
+ @JvmField val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
- val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
- @JvmField
- val ENABLE_USI_BATTERY_NOTIFICATIONS =
- releasedFlag(2302, "enable_usi_battery_notifications")
+ val ENABLE_USI_BATTERY_NOTIFICATIONS = releasedFlag(2302, "enable_usi_battery_notifications")
@JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
// 2400 - performance tools and debugging info
@@ -681,11 +644,10 @@ object Flags {
// TODO(b/283071711): Tracking bug
@JvmField
val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
- unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
+ unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
// TODO:(b/283203305): Tracking bug
- @JvmField
- val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
+ @JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
// 2700 - unfold transitions
// TODO(b/265764985): Tracking Bug
@@ -708,27 +670,21 @@ object Flags {
@JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout")
// TODO(b/259428678): Tracking Bug
- @JvmField
- val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
+ @JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
// TODO(b/277192623): Tracking Bug
- @JvmField
- val KEYBOARD_EDUCATION =
- unreleasedFlag(2603, "keyboard_education", teamfood = false)
+ @JvmField val KEYBOARD_EDUCATION = unreleasedFlag(2603, "keyboard_education", teamfood = false)
// TODO(b/277201412): Tracking Bug
@JvmField
- val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
- releasedFlag(2805, "split_shade_subpixel_optimization")
+ val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = releasedFlag(2805, "split_shade_subpixel_optimization")
// TODO(b/288868056): Tracking Bug
@JvmField
- val PARTIAL_SCREEN_SHARING_TASK_SWITCHER =
- unreleasedFlag(288868056, "pss_task_switcher")
+ val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag(288868056, "pss_task_switcher")
// TODO(b/278761837): Tracking Bug
- @JvmField
- val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
+ @JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
// 2900 - Zero Jank fixes. Naming convention is: zj_<bug number>_<cuj name>
@@ -744,23 +700,20 @@ object Flags {
unreleasedFlag(3000, name = "enable_lockscreen_wallpaper_dream")
// TODO(b/283084712): Tracking Bug
- @JvmField
- val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
+ @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
// TODO(b/283447257): Tracking bug
@JvmField
val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
- unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
+ unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
// TODO(b/283740863): Tracking Bug
@JvmField
val ENABLE_NEW_PRIVACY_DIALOG =
- unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
+ unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = true)
// TODO(b/289573946): Tracking Bug
- @JvmField
- val PRECOMPUTED_TEXT =
- unreleasedFlag(289573946, "precomputed_text")
+ @JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text")
// 2900 - CentralSurfaces-related flags
@@ -773,6 +726,5 @@ object Flags {
// TODO(b/290213663): Tracking Bug
@JvmField
- val ONE_WAY_HAPTICS_API_MIGRATION =
- unreleasedFlag(3100, "oneway_haptics_api_migration")
+ val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag(3100, "oneway_haptics_api_migration")
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 80a92340d3f3..5d7ea1cc5819 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -444,6 +444,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private final LockPatternUtils mLockPatternUtils;
private final BroadcastDispatcher mBroadcastDispatcher;
private boolean mKeyguardDonePending = false;
+ private boolean mUnlockingAndWakingFromDream = false;
private boolean mHideAnimationRun = false;
private boolean mHideAnimationRunning = false;
@@ -802,6 +803,25 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
mKeyguardDisplayManager.hide();
mUpdateMonitor.startBiometricWatchdog();
+
+ // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
+ // dreaming. It's time to wake up.
+ if (mUnlockingAndWakingFromDream) {
+ Log.d(TAG, "waking from dream after unlock");
+ mUnlockingAndWakingFromDream = false;
+
+ if (mKeyguardStateController.isShowing()) {
+ Log.d(TAG, "keyguard showing after keyguardGone, dismiss");
+ mKeyguardViewControllerLazy.get()
+ .notifyKeyguardAuthenticated(!mWakeAndUnlocking);
+ } else {
+ Log.d(TAG, "keyguard gone, waking up from dream");
+ mPM.wakeUp(mSystemClock.uptimeMillis(),
+ mWakeAndUnlocking ? PowerManager.WAKE_REASON_BIOMETRIC
+ : PowerManager.WAKE_REASON_GESTURE,
+ "com.android.systemui:UNLOCK_DREAMING");
+ }
+ }
Trace.endSection();
}
@@ -2665,6 +2685,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mKeyguardExitAnimationRunner = null;
mWakeAndUnlocking = false;
+ mUnlockingAndWakingFromDream = false;
setPendingLock(false);
// Force if we we're showing in the middle of hiding, to ensure we end up in the correct
@@ -2789,7 +2810,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mHiding = true;
- if (mShowing && !mOccluded) {
+ mUnlockingAndWakingFromDream = mStatusBarStateController.isDreaming()
+ && !mStatusBarStateController.isDozing();
+
+ if ((mShowing && !mOccluded) || mUnlockingAndWakingFromDream) {
+ if (mUnlockingAndWakingFromDream) {
+ Log.d(TAG, "hiding keyguard before waking from dream");
+ }
mKeyguardGoingAwayRunnable.run();
} else {
// TODO(bc-unlock): Fill parameters
@@ -2800,13 +2827,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
null /* nonApps */, null /* finishedCallback */);
});
}
-
- // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
- // dreaming. It's time to wake up.
- if (mDreamOverlayShowing || mUpdateMonitor.isDreaming()) {
- mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
- "com.android.systemui:UNLOCK_DREAMING");
- }
}
Trace.endSection();
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index 1978b3d048b7..039460d8fdae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -61,28 +61,23 @@ constructor(
override fun start() {
Log.d(LOG_TAG, "Resource trimmer registered.")
- if (
- !(featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK) ||
- featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK))
- ) {
- return
- }
-
- applicationScope.launch(bgDispatcher) {
- // We need to wait for the AoD transition (and animation) to complete.
- // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
- // signal. This is to make sure we don't clear font caches during animation which
- // would jank and leave stale data in memory.
- val isDozingFully =
- keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
- combine(
- keyguardInteractor.wakefulnessModel.map { it.state },
- keyguardInteractor.isDreaming,
- isDozingFully,
- ::Triple
- )
- .distinctUntilChanged()
- .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
+ if (featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
+ applicationScope.launch(bgDispatcher) {
+ // We need to wait for the AoD transition (and animation) to complete.
+ // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
+ // signal. This is to make sure we don't clear font caches during animation which
+ // would jank and leave stale data in memory.
+ val isDozingFully =
+ keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
+ combine(
+ keyguardInteractor.wakefulnessModel.map { it.state },
+ keyguardInteractor.isDreaming,
+ isDozingFully,
+ ::Triple
+ )
+ .distinctUntilChanged()
+ .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
+ }
}
applicationScope.launch(bgDispatcher) {
@@ -97,17 +92,16 @@ constructor(
@WorkerThread
private fun onKeyguardGone() {
- if (!featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
- return
- }
-
- if (DEBUG) {
- Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
- }
// We want to clear temporary caches we've created while rendering and animating
// lockscreen elements, especially clocks.
+ Log.d(LOG_TAG, "Sending TRIM_MEMORY_UI_HIDDEN.")
globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
- globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ if (featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
+ }
+ globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ }
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index d1f011ea4f9b..6fd3e21b25a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -26,6 +26,8 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.FaceAuthUiEvent
import com.android.systemui.Dumpable
import com.android.systemui.R
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -58,6 +60,7 @@ import java.util.stream.Collectors
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@@ -68,6 +71,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -102,6 +106,9 @@ interface DeviceEntryFaceAuthRepository {
/** Whether bypass is currently enabled */
val isBypassEnabled: Flow<Boolean>
+ /** Set whether face authentication should be locked out or not */
+ fun lockoutFaceAuth()
+
/**
* Trigger face authentication.
*
@@ -117,6 +124,7 @@ interface DeviceEntryFaceAuthRepository {
fun cancel()
}
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class DeviceEntryFaceAuthRepositoryImpl
@Inject
@@ -140,7 +148,8 @@ constructor(
@FaceDetectTableLog private val faceDetectLog: TableLogBuffer,
@FaceAuthTableLog private val faceAuthLog: TableLogBuffer,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
- private val featureFlags: FeatureFlags,
+ featureFlags: FeatureFlags,
+ facePropertyRepository: FacePropertyRepository,
dumpManager: DumpManager,
) : DeviceEntryFaceAuthRepository, Dumpable {
private var authCancellationSignal: CancellationSignal? = null
@@ -160,6 +169,13 @@ constructor(
override val detectionStatus: Flow<FaceDetectionStatus>
get() = _detectionStatus.filterNotNull()
+ private val isFaceBiometricsAllowed: Flow<Boolean> =
+ facePropertyRepository.sensorInfo.flatMapLatest {
+ if (it?.strength == SensorStrength.STRONG)
+ biometricSettingsRepository.isStrongBiometricAllowed
+ else biometricSettingsRepository.isNonStrongBiometricAllowed
+ }
+
private val _isLockedOut = MutableStateFlow(false)
override val isLockedOut: StateFlow<Boolean> = _isLockedOut
@@ -199,6 +215,10 @@ constructor(
}
?: flowOf(false)
+ override fun lockoutFaceAuth() {
+ _isLockedOut.value = true
+ }
+
private val faceLockoutResetCallback =
object : FaceManager.LockoutResetCallback() {
override fun onLockoutReset(sensorId: Int) {
@@ -267,10 +287,8 @@ constructor(
canFaceAuthOrDetectRun(faceDetectLog),
logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
logAndObserve(
- biometricSettingsRepository.isNonStrongBiometricAllowed
- .isFalse()
- .or(trustRepository.isCurrentUserTrusted),
- "nonStrongBiometricIsNotAllowedOrCurrentUserIsTrusted",
+ isFaceBiometricsAllowed.isFalse().or(trustRepository.isCurrentUserTrusted),
+ "biometricIsNotAllowedOrCurrentUserIsTrusted",
faceDetectLog
),
// We don't want to run face detect if fingerprint can be used to unlock the device
@@ -362,20 +380,11 @@ constructor(
canFaceAuthOrDetectRun(faceAuthLog),
logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog),
logAndObserve(
- deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(),
- "fpIsNotLockedOut",
- faceAuthLog
- ),
- logAndObserve(
trustRepository.isCurrentUserTrusted.isFalse(),
"currentUserIsNotTrusted",
faceAuthLog
),
- logAndObserve(
- biometricSettingsRepository.isNonStrongBiometricAllowed,
- "nonStrongBiometricIsAllowed",
- faceAuthLog
- ),
+ logAndObserve(isFaceBiometricsAllowed, "isFaceBiometricsAllowed", faceAuthLog),
logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog),
)
.reduce(::and)
@@ -396,7 +405,7 @@ constructor(
private val faceAuthCallback =
object : FaceManager.AuthenticationCallback() {
override fun onAuthenticationFailed() {
- _authenticationStatus.value = FailedFaceAuthenticationStatus
+ _authenticationStatus.value = FailedFaceAuthenticationStatus()
_isAuthenticated.value = false
faceAuthLogger.authenticationFailed()
onFaceAuthRequestCompleted()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index 482e9a3d09d7..6a2511fdb90e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -20,10 +20,13 @@ package com.android.systemui.keyguard.data.repository
import android.content.Context
import android.graphics.Point
+import androidx.core.animation.Animator
+import androidx.core.animation.ValueAnimator
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LiftReveal
import com.android.systemui.statusbar.LightRevealEffect
@@ -31,9 +34,12 @@ import com.android.systemui.statusbar.PowerButtonReveal
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -52,6 +58,10 @@ interface LightRevealScrimRepository {
* at the current screen position of the appropriate sensor.
*/
val revealEffect: Flow<LightRevealEffect>
+
+ val revealAmount: Flow<Float>
+
+ fun startRevealAmountAnimator(reveal: Boolean)
}
@SysUISingleton
@@ -108,13 +118,30 @@ constructor(
/** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */
private val nonBiometricRevealEffect: Flow<LightRevealEffect?> =
- keyguardRepository.wakefulness.flatMapLatest { wakefulnessModel ->
- when {
- wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
- wakefulnessModel.isAwakeFromTap() -> tapRevealEffect
- else -> flowOf(LiftReveal)
+ keyguardRepository.wakefulness
+ .filter { it.isStartingToWake() || it.isStartingToSleep() }
+ .flatMapLatest { wakefulnessModel ->
+ when {
+ wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
+ wakefulnessModel.isWakingFrom(TAP) -> tapRevealEffect
+ else -> flowOf(LiftReveal)
+ }
}
- }
+
+ private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 500 }
+
+ override val revealAmount: Flow<Float> = callbackFlow {
+ val updateListener =
+ Animator.AnimatorUpdateListener {
+ trySend((it as ValueAnimator).animatedValue as Float)
+ }
+ revealAmountAnimator.addUpdateListener(updateListener)
+ awaitClose { revealAmountAnimator.removeUpdateListener(updateListener) }
+ }
+
+ override fun startRevealAmountAnimator(reveal: Boolean) {
+ if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
+ }
override val revealEffect =
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
index 27e3a749a6c0..5ef9a9e0482c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
@@ -55,6 +55,8 @@ class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceA
override val isBypassEnabled: Flow<Boolean>
get() = emptyFlow()
+ override fun lockoutFaceAuth() = Unit
+
/**
* Trigger face authentication.
*
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
index 141b13055889..e57c919a5b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
@@ -60,6 +60,7 @@ interface KeyguardFaceAuthInteractor {
fun onNotificationPanelClicked()
fun onSwipeUpOnBouncer()
fun onPrimaryBouncerUserInput()
+ fun onAccessibilityAction()
}
/**
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 324d443d974d..40e0604ae1b3 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
@@ -409,6 +409,10 @@ constructor(
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_TRANSIT_CLOCK,
value = featureFlags.isEnabled(Flags.TRANSIT_CLOCK)
+ ),
+ KeyguardPickerFlag(
+ name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS,
+ value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS)
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 833eda77108d..4244e5565a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -17,28 +17,44 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
@SysUISingleton
class LightRevealScrimInteractor
@Inject
constructor(
- transitionRepository: KeyguardTransitionRepository,
- transitionInteractor: KeyguardTransitionInteractor,
- lightRevealScrimRepository: LightRevealScrimRepository,
+ private val transitionInteractor: KeyguardTransitionInteractor,
+ private val lightRevealScrimRepository: LightRevealScrimRepository,
+ @Application private val scope: CoroutineScope,
) {
+ init {
+ listenForStartedKeyguardTransitionStep()
+ }
+
+ private fun listenForStartedKeyguardTransitionStep() {
+ scope.launch {
+ transitionInteractor.startedKeyguardTransitionStep.collect {
+ if (willTransitionChangeEndState(it)) {
+ lightRevealScrimRepository.startRevealAmountAnimator(
+ willBeRevealedInState(it.to)
+ )
+ }
+ }
+ }
+ }
+
/**
* Whenever a keyguard transition starts, sample the latest reveal effect from the repository
* and use that for the starting transition.
@@ -54,17 +70,7 @@ constructor(
lightRevealScrimRepository.revealEffect
)
- /**
- * The reveal amount to use for the light reveal scrim, which is derived from the keyguard
- * transition steps.
- */
- val revealAmount: Flow<Float> =
- transitionRepository.transitions
- // Only listen to transitions that change the reveal amount.
- .filter { willTransitionAffectRevealAmount(it) }
- // Use the transition amount as the reveal amount, inverting it if we're transitioning
- // to a non-revealed (hidden) state.
- .map { step -> if (willBeRevealedInState(step.to)) step.value else 1f - step.value }
+ val revealAmount = lightRevealScrimRepository.revealAmount
companion object {
@@ -72,7 +78,7 @@ constructor(
* Whether the transition requires a change in the reveal amount of the light reveal scrim.
* If not, we don't care about the transition and don't need to listen to it.
*/
- fun willTransitionAffectRevealAmount(transition: TransitionStep): Boolean {
+ fun willTransitionChangeEndState(transition: TransitionStep): Boolean {
return willBeRevealedInState(transition.from) != willBeRevealedInState(transition.to)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
index 10dd900e437e..596a1c01ca42 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
@@ -60,4 +60,5 @@ class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInt
override fun onSwipeUpOnBouncer() {}
override fun onPrimaryBouncerUserInput() {}
+ override fun onAccessibilityAction() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index 6e7a20b092f4..8f4776fa2ed3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -30,6 +30,7 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionState
@@ -67,6 +68,7 @@ constructor(
private val featureFlags: FeatureFlags,
private val faceAuthenticationLogger: FaceAuthenticationLogger,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
) : CoreStartable, KeyguardFaceAuthInteractor {
private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
@@ -117,6 +119,15 @@ constructor(
)
}
.launchIn(applicationScope)
+
+ deviceEntryFingerprintAuthRepository.isLockedOut
+ .onEach {
+ if (it) {
+ faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
+ repository.lockoutFaceAuth()
+ }
+ }
+ .launchIn(applicationScope)
}
override fun onSwipeUpOnBouncer() {
@@ -143,6 +154,10 @@ constructor(
runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)
}
+ override fun onAccessibilityAction() {
+ runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false)
+ }
+
override fun registerListener(listener: FaceAuthenticationListener) {
listeners.add(listener)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index d9792cf704c8..3de3666fdc3c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -23,29 +23,40 @@ import android.os.SystemClock.elapsedRealtime
* Authentication status provided by
* [com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository]
*/
-sealed class FaceAuthenticationStatus(
- // present to break equality check if the same error occurs repeatedly.
- val createdAt: Long = elapsedRealtime()
-)
+sealed class FaceAuthenticationStatus
/** Success authentication status. */
-data class SuccessFaceAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
- FaceAuthenticationStatus()
+data class SuccessFaceAuthenticationStatus(
+ val successResult: FaceManager.AuthenticationResult,
+ // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face authentication help message. */
-data class HelpFaceAuthenticationStatus(val msgId: Int, val msg: String?) :
- FaceAuthenticationStatus()
+data class HelpFaceAuthenticationStatus(
+ val msgId: Int,
+ val msg: String?, // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face acquired message. */
-data class AcquiredFaceAuthenticationStatus(val acquiredInfo: Int) : FaceAuthenticationStatus()
+data class AcquiredFaceAuthenticationStatus(
+ val acquiredInfo: Int, // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face authentication failed message. */
-object FailedFaceAuthenticationStatus : FaceAuthenticationStatus()
+data class FailedFaceAuthenticationStatus(
+ // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face authentication error message */
data class ErrorFaceAuthenticationStatus(
val msgId: Int,
val msg: String? = null,
+ // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
) : FaceAuthenticationStatus() {
/**
* Method that checks if [msgId] is a lockout error. A lockout error means that face
@@ -80,5 +91,5 @@ data class FaceDetectionStatus(
val userId: Int,
val isStrongBiometric: Boolean,
// present to break equality check if the same error occurs repeatedly.
- val createdAt: Long = elapsedRealtime()
+ @JvmField val createdAt: Long = elapsedRealtime()
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
index cfd9e0866c06..62f43ed82a96 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
@@ -16,6 +16,13 @@
package com.android.systemui.keyguard.shared.model
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.GESTURE
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.POWER_BUTTON
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.ASLEEP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.AWAKE
+import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_SLEEP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_WAKE
/** Model device wakefulness states. */
data class WakefulnessModel(
@@ -23,33 +30,31 @@ data class WakefulnessModel(
val lastWakeReason: WakeSleepReason,
val lastSleepReason: WakeSleepReason,
) {
- fun isStartingToWake() = state == WakefulnessState.STARTING_TO_WAKE
+ fun isStartingToWake() = state == STARTING_TO_WAKE
- fun isStartingToSleep() = state == WakefulnessState.STARTING_TO_SLEEP
+ fun isStartingToSleep() = state == STARTING_TO_SLEEP
- private fun isAsleep() = state == WakefulnessState.ASLEEP
+ private fun isAsleep() = state == ASLEEP
+
+ private fun isAwake() = state == AWAKE
+
+ fun isStartingToWakeOrAwake() = isStartingToWake() || isAwake()
fun isStartingToSleepOrAsleep() = isStartingToSleep() || isAsleep()
fun isDeviceInteractive() = !isAsleep()
- fun isStartingToWakeOrAwake() = isStartingToWake() || state == WakefulnessState.AWAKE
+ fun isWakingFrom(wakeSleepReason: WakeSleepReason) =
+ isStartingToWake() && lastWakeReason == wakeSleepReason
- fun isStartingToSleepFromPowerButton() =
- isStartingToSleep() && lastWakeReason == WakeSleepReason.POWER_BUTTON
-
- fun isWakingFromPowerButton() =
- isStartingToWake() && lastWakeReason == WakeSleepReason.POWER_BUTTON
+ fun isStartingToSleepFrom(wakeSleepReason: WakeSleepReason) =
+ isStartingToSleep() && lastSleepReason == wakeSleepReason
fun isTransitioningFromPowerButton() =
- isStartingToSleepFromPowerButton() || isWakingFromPowerButton()
-
- fun isAwakeFromTap() =
- state == WakefulnessState.STARTING_TO_WAKE && lastWakeReason == WakeSleepReason.TAP
+ isStartingToSleepFrom(POWER_BUTTON) || isWakingFrom(POWER_BUTTON)
fun isDeviceInteractiveFromTapOrGesture(): Boolean {
- return isDeviceInteractive() &&
- (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
+ return isDeviceInteractive() && (lastWakeReason == TAP || lastWakeReason == GESTURE)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index 389cf76c47ac..f1ceaaa391f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -20,20 +20,18 @@ import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** View-model for the keyguard indication area view */
-@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardIndicationAreaViewModel
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
- private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
- private val keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
+ bottomAreaInteractor: KeyguardBottomAreaInteractor,
+ keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
index a46d441613ac..82f40bf3a16a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
@@ -19,12 +19,14 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.keyguard.domain.interactor.LightRevealScrimInteractor
import com.android.systemui.statusbar.LightRevealEffect
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
/**
* Models UI state for the light reveal scrim, which is used during screen on and off animations to
* draw a gradient that reveals/hides the contents of the screen.
*/
+@OptIn(ExperimentalCoroutinesApi::class)
class LightRevealScrimViewModel @Inject constructor(interactor: LightRevealScrimInteractor) {
val lightRevealEffect: Flow<LightRevealEffect> = interactor.lightRevealEffect
val revealAmount: Flow<Float> = interactor.revealAmount
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index 373f70582612..66067b11a18c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -8,6 +8,7 @@ import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.dagger.FaceAuthLog
+import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
private const val TAG = "DeviceEntryFaceAuthRepositoryLog"
@@ -264,4 +265,8 @@ constructor(
fun watchdogScheduled() {
logBuffer.log(TAG, DEBUG, "FaceManager Biometric watchdog scheduled.")
}
+
+ fun faceLockedOut(@CompileTimeConstant reason: String) {
+ logBuffer.log(TAG, DEBUG, "Face auth has been locked out: $reason")
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index f6a2f3704283..72352e397806 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -330,6 +330,8 @@ public class MediaProjectionPermissionActivity extends Activity
// Don't send cancel if the user has moved on to the next activity.
if (!mUserSelectingTask) {
finish(RECORD_CANCEL, /* projection= */ null);
+ } else {
+ super.finish();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
new file mode 100644
index 000000000000..c74a71c52260
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.model
+
+import com.android.systemui.dagger.qualifiers.DisplayId
+
+/**
+ * In-bulk updates multiple flag values and commits the update.
+ *
+ * Example:
+ * ```
+ * sysuiState.updateFlags(
+ * displayId,
+ * SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ * SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ * SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ * SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ * SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == SceneKey.Lockscreen),
+ * )
+ * ```
+ *
+ * You can inject [displayId] by injecting it using:
+ * ```
+ * @DisplayId private val displayId: Int`,
+ * ```
+ */
+fun SysUiState.updateFlags(
+ @DisplayId displayId: Int,
+ vararg flagValuePairs: Pair<Int, Boolean>,
+) {
+ flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
+ commitUpdate(displayId)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
deleted file mode 100644
index c48028c31cf0..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.data.model
-
-import com.android.systemui.multishade.shared.model.ShadeId
-
-/** Models the current interaction with one of the shades. */
-data class MultiShadeInteractionModel(
- /** The ID of the shade that the user is currently interacting with. */
- val shadeId: ShadeId,
- /** Whether the interaction is proxied (as in: coming from an external app or different UI). */
- val isProxied: Boolean,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
deleted file mode 100644
index 86f0c0d15b55..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.data.remoteproxy
-
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-
-/**
- * Acts as a hub for routing proxied user input into the multi shade system.
- *
- * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
- * In other words: it's not user input that's occurring directly on the shade UI itself. This class
- * is that proxy.
- */
-@Singleton
-class MultiShadeInputProxy @Inject constructor() {
- private val _proxiedTouch =
- MutableSharedFlow<ProxiedInputModel>(
- replay = 1,
- onBufferOverflow = BufferOverflow.DROP_OLDEST,
- )
- val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow()
-
- fun onProxiedInput(proxiedInput: ProxiedInputModel) {
- _proxiedTouch.tryEmit(proxiedInput)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
deleted file mode 100644
index 117203012757..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.data.repository
-
-import android.content.Context
-import androidx.annotation.FloatRange
-import com.android.systemui.R
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.multishade.shared.model.ShadeModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/** Encapsulates application state for all shades. */
-@SysUISingleton
-class MultiShadeRepository
-@Inject
-constructor(
- @Application private val applicationContext: Context,
- inputProxy: MultiShadeInputProxy,
-) {
- /**
- * Remote input coming from sources outside of system UI (for example, swiping down on the
- * Launcher or from the status bar).
- */
- val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput
-
- /** Width of the left-hand side shade, in pixels. */
- private val leftShadeWidthPx =
- applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)
-
- /** Width of the right-hand side shade, in pixels. */
- private val rightShadeWidthPx =
- applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)
-
- /**
- * The amount that the user must swipe up when the shade is fully expanded to automatically
- * collapse once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully expanded once the user stops swiping.
- *
- * This is a fraction between `0` and `1`.
- */
- private val swipeCollapseThreshold =
- checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))
-
- /**
- * The amount that the user must swipe down when the shade is fully collapsed to automatically
- * expand once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully collapsed once the user stops swiping.
- *
- * This is a fraction between `0` and `1`.
- */
- private val swipeExpandThreshold =
- checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))
-
- /**
- * Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
- *
- * This is a fraction between `0` and `1`.
- */
- private val dualShadeScrimAlpha =
- checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))
-
- /** The current configuration of the shade system. */
- val shadeConfig: StateFlow<ShadeConfig> =
- MutableStateFlow(
- if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
- ShadeConfig.DualShadeConfig(
- leftShadeWidthPx = leftShadeWidthPx,
- rightShadeWidthPx = rightShadeWidthPx,
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- splitFraction =
- applicationContext.resources.getFloat(
- R.dimen.dual_shade_split_fraction
- ),
- scrimAlpha = dualShadeScrimAlpha,
- )
- } else {
- ShadeConfig.SingleShadeConfig(
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- )
- }
- )
- .asStateFlow()
-
- private val _forceCollapseAll = MutableStateFlow(false)
- /** Whether all shades should be collapsed. */
- val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow()
-
- private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null)
- /** The current shade interaction or `null` if no shade is interacted with currently. */
- val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow()
-
- private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>()
-
- /** The model for the shade with the given ID. */
- fun getShade(
- shadeId: ShadeId,
- ): StateFlow<ShadeModel> {
- return getMutableShade(shadeId).asStateFlow()
- }
-
- /** Sets the expansion amount for the shade with the given ID. */
- fun setExpansion(
- shadeId: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) expansion: Float,
- ) {
- getMutableShade(shadeId).let { mutableState ->
- mutableState.value = mutableState.value.copy(expansion = expansion)
- }
- }
-
- /** Sets whether all shades should be immediately forced to collapse. */
- fun setForceCollapseAll(isForced: Boolean) {
- _forceCollapseAll.value = isForced
- }
-
- /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
- fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
- _shadeInteraction.value = shadeInteraction
- }
-
- private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> {
- return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
- }
-
- /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
- private fun checkInBounds(float: Float): Float {
- check(float in 0f..1f) { "$float isn't between 0 and 1." }
- return float
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
deleted file mode 100644
index ebb8639b8922..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.domain.interactor
-
-import androidx.annotation.FloatRange
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.shared.math.isZero
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.multishade.shared.model.ShadeModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.yield
-
-/** Encapsulates business logic related to interactions with the multi-shade system. */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MultiShadeInteractor
-@Inject
-constructor(
- @Application private val applicationScope: CoroutineScope,
- private val repository: MultiShadeRepository,
- private val inputProxy: MultiShadeInputProxy,
-) {
- /** The current configuration of the shade system. */
- val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig
-
- /** The expansion of the shade that's most expanded. */
- val maxShadeExpansion: Flow<Float> =
- repository.shadeConfig.flatMapLatest { shadeConfig ->
- combine(allShades(shadeConfig)) { shadeModels ->
- shadeModels.maxOfOrNull { it.expansion } ?: 0f
- }
- }
-
- /** Whether any shade is expanded, even a little bit. */
- val isAnyShadeExpanded: Flow<Boolean> =
- maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged()
-
- /**
- * A _processed_ version of the proxied input flow.
- *
- * All internal dependencies on the proxied input flow *must* use this one for two reasons:
- * 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we
- * actually have.
- * 2. It actually does some preprocessing as the proxied input events stream through, handling
- * common things like recording the current state of the system based on incoming input
- * events.
- */
- private val processedProxiedInput: SharedFlow<ProxiedInputModel> =
- combine(
- repository.shadeConfig,
- repository.proxiedInput.distinctUntilChanged(),
- ::Pair,
- )
- .map { (shadeConfig, proxiedInput) ->
- if (proxiedInput !is ProxiedInputModel.OnTap) {
- // If the user is interacting with any other gesture type (for instance,
- // dragging),
- // we no longer want to force collapse all shades.
- repository.setForceCollapseAll(false)
- }
-
- when (proxiedInput) {
- is ProxiedInputModel.OnDrag -> {
- val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction)
- // This might be the start of a new drag gesture, let's update our
- // application
- // state to record that fact.
- onUserInteractionStarted(
- shadeId = affectedShadeId,
- isProxied = true,
- )
- }
- is ProxiedInputModel.OnTap -> {
- // Tapping outside any shade collapses all shades. This code path is not hit
- // for
- // taps that happen _inside_ a shade as that input event is directly applied
- // through the UI and is, hence, not a proxied input.
- collapseAll()
- }
- else -> Unit
- }
-
- proxiedInput
- }
- .shareIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- replay = 1,
- )
-
- /** Whether the shade with the given ID should be visible. */
- fun isVisible(shadeId: ShadeId): Flow<Boolean> {
- return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) }
- }
-
- /** Whether direct user input is allowed on the shade with the given ID. */
- fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> {
- return combine(
- isForceCollapsed(shadeId),
- repository.shadeInteraction,
- ::Pair,
- )
- .map { (isForceCollapsed, shadeInteraction) ->
- !isForceCollapsed && shadeInteraction?.isProxied != true
- }
- }
-
- /** Whether the shade with the given ID is forced to collapse. */
- fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> {
- return combine(
- repository.forceCollapseAll,
- repository.shadeInteraction.map { it?.shadeId },
- ::Pair,
- )
- .map { (collapseAll, userInteractedShadeIdOrNull) ->
- val counterpartShadeIdOrNull =
- when (shadeId) {
- ShadeId.SINGLE -> null
- ShadeId.LEFT -> ShadeId.RIGHT
- ShadeId.RIGHT -> ShadeId.LEFT
- }
-
- when {
- // If all shades have been told to collapse (by a tap outside, for example),
- // then this shade is collapsed.
- collapseAll -> true
- // A shade that doesn't have a counterpart shade cannot be force-collapsed by
- // interactions on the counterpart shade.
- counterpartShadeIdOrNull == null -> false
- // If the current user interaction is on the counterpart shade, then this shade
- // should be force-collapsed.
- else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull
- }
- }
- }
-
- /**
- * Proxied input affecting the shade with the given ID. This is input coming from sources
- * outside of system UI (for example, swiping down on the Launcher or from the status bar) or
- * outside the UI of any shade (for example, the scrim that's shown behind the shades).
- */
- fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> {
- return combine(
- processedProxiedInput,
- isForceCollapsed(shadeId).distinctUntilChanged(),
- repository.shadeInteraction,
- ::Triple,
- )
- .map { (proxiedInput, isForceCollapsed, shadeInteraction) ->
- when {
- // If the shade is force-collapsed, we ignored proxied input on it.
- isForceCollapsed -> null
- // If the proxied input does not belong to this shade, ignore it.
- shadeInteraction?.shadeId != shadeId -> null
- // If there is ongoing non-proxied user input on any shade, ignore the
- // proxied input.
- !shadeInteraction.isProxied -> null
- // Otherwise, send the proxied input downstream.
- else -> proxiedInput
- }
- }
- .onEach { proxiedInput ->
- // We use yield() to make sure that the following block of code happens _after_
- // downstream collectors had a chance to process the proxied input. Otherwise, we
- // might change our state to clear the current UserInteraction _before_ those
- // downstream collectors get a chance to process the proxied input, which will make
- // them ignore it (since they ignore proxied input when the current user interaction
- // doesn't match their shade).
- yield()
-
- if (
- proxiedInput is ProxiedInputModel.OnDragEnd ||
- proxiedInput is ProxiedInputModel.OnDragCancel
- ) {
- onUserInteractionEnded(shadeId = shadeId, isProxied = true)
- }
- }
- }
-
- /** Sets the expansion amount for the shade with the given ID. */
- fun setExpansion(
- shadeId: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) expansion: Float,
- ) {
- repository.setExpansion(shadeId, expansion)
- }
-
- /** Collapses all shades. */
- fun collapseAll() {
- repository.setForceCollapseAll(true)
- }
-
- /**
- * Notifies that a new non-proxied interaction may have started. Safe to call multiple times for
- * the same interaction as it won't overwrite an existing interaction.
- *
- * Existing interactions can be cleared by calling [onUserInteractionEnded].
- */
- fun onUserInteractionStarted(shadeId: ShadeId) {
- onUserInteractionStarted(
- shadeId = shadeId,
- isProxied = false,
- )
- }
-
- /**
- * Notifies that the current non-proxied interaction has ended.
- *
- * Safe to call multiple times, even if there's no current interaction or even if the current
- * interaction doesn't belong to the given shade or is proxied as the code is a no-op unless
- * there's a match between the parameters and the current interaction.
- */
- fun onUserInteractionEnded(
- shadeId: ShadeId,
- ) {
- onUserInteractionEnded(
- shadeId = shadeId,
- isProxied = false,
- )
- }
-
- fun sendProxiedInput(proxiedInput: ProxiedInputModel) {
- inputProxy.onProxiedInput(proxiedInput)
- }
-
- /**
- * Notifies that a new interaction may have started. Safe to call multiple times for the same
- * interaction as it won't overwrite an existing interaction.
- *
- * Existing interactions can be cleared by calling [onUserInteractionEnded].
- */
- private fun onUserInteractionStarted(
- shadeId: ShadeId,
- isProxied: Boolean,
- ) {
- if (repository.shadeInteraction.value != null) {
- return
- }
-
- repository.setShadeInteraction(
- MultiShadeInteractionModel(
- shadeId = shadeId,
- isProxied = isProxied,
- )
- )
- }
-
- /**
- * Notifies that the current interaction has ended.
- *
- * Safe to call multiple times, even if there's no current interaction or even if the current
- * interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op
- * unless there's a match between the parameters and the current interaction.
- */
- private fun onUserInteractionEnded(
- shadeId: ShadeId,
- isProxied: Boolean,
- ) {
- repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) ->
- if (shadeId == interactionShadeId && isProxied == isInteractionProxied) {
- repository.setShadeInteraction(null)
- }
- }
- }
-
- /**
- * Returns the ID of the shade that's affected by user input at a given coordinate.
- *
- * @param config The shade configuration being used.
- * @param xFraction The horizontal position of the user input as a fraction along the width of
- * its container where `0` is all the way to the left and `1` is all the way to the right.
- */
- private fun affectedShadeId(
- config: ShadeConfig,
- @FloatRange(from = 0.0, to = 1.0) xFraction: Float,
- ): ShadeId {
- return if (config is ShadeConfig.DualShadeConfig) {
- if (xFraction <= config.splitFraction) {
- ShadeId.LEFT
- } else {
- ShadeId.RIGHT
- }
- } else {
- ShadeId.SINGLE
- }
- }
-
- /** Returns the list of flows of all the shades in the given configuration. */
- private fun allShades(
- config: ShadeConfig,
- ): List<Flow<ShadeModel>> {
- return config.shadeIds.map { shadeId -> repository.getShade(shadeId) }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt
deleted file mode 100644
index 1894bc4cfeab..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.domain.interactor
-
-import android.content.Context
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import com.android.systemui.classifier.Classifier
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.multishade.shared.math.isZero
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.shade.ShadeController
-import javax.inject.Inject
-import kotlin.math.abs
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-
-/**
- * Encapsulates business logic to handle [MotionEvent]-based user input.
- *
- * This class is meant purely for the legacy `View`-based system to be able to pass `MotionEvent`s
- * into the newer multi-shade framework for processing.
- */
-@SysUISingleton
-class MultiShadeMotionEventInteractor
-@Inject
-constructor(
- @Application private val applicationContext: Context,
- @Application private val applicationScope: CoroutineScope,
- private val multiShadeInteractor: MultiShadeInteractor,
- featureFlags: FeatureFlags,
- keyguardTransitionInteractor: KeyguardTransitionInteractor,
- private val falsingManager: FalsingManager,
- private val shadeController: ShadeController,
-) {
- init {
- if (featureFlags.isEnabled(Flags.DUAL_SHADE)) {
- applicationScope.launch {
- multiShadeInteractor.isAnyShadeExpanded.collect {
- if (!it && !shadeController.isKeyguard) {
- shadeController.makeExpandedInvisible()
- } else {
- shadeController.makeExpandedVisible(false)
- }
- }
- }
- }
- }
-
- private val isAnyShadeExpanded: StateFlow<Boolean> =
- multiShadeInteractor.isAnyShadeExpanded.stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = false,
- )
-
- private val isBouncerShowing: StateFlow<Boolean> =
- keyguardTransitionInteractor
- .transitionValue(state = KeyguardState.PRIMARY_BOUNCER)
- .map { !it.isZero() }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = false,
- )
-
- private var interactionState: InteractionState? = null
-
- /**
- * Returns `true` if the given [MotionEvent] and the rest of events in this gesture should be
- * passed to this interactor's [onTouchEvent] method.
- *
- * Note: the caller should continue to pass [MotionEvent] instances into this method, even if it
- * returns `false` as the gesture may be intercepted mid-stream.
- */
- fun shouldIntercept(event: MotionEvent): Boolean {
- if (isAnyShadeExpanded.value) {
- // If any shade is expanded, we assume that touch handling outside the shades is handled
- // by the scrim that appears behind the shades. No need to intercept anything here.
- return false
- }
-
- if (isBouncerShowing.value) {
- return false
- }
-
- return when (event.actionMasked) {
- MotionEvent.ACTION_DOWN -> {
- // Record where the pointer was placed and which pointer it was.
- interactionState =
- InteractionState(
- initialX = event.x,
- initialY = event.y,
- currentY = event.y,
- pointerId = event.getPointerId(0),
- isDraggingHorizontally = false,
- isDraggingShade = false,
- )
-
- false
- }
- MotionEvent.ACTION_MOVE -> {
- onMove(event)
-
- // We want to intercept the rest of the gesture if we're dragging the shade.
- isDraggingShade()
- }
- MotionEvent.ACTION_UP,
- MotionEvent.ACTION_CANCEL ->
- // Make sure that we intercept the up or cancel if we're dragging the shade, to
- // handle drag end or cancel.
- isDraggingShade()
- else -> false
- }
- }
-
- /**
- * Notifies that a [MotionEvent] in a series of events of a gesture that was intercepted due to
- * the result of [shouldIntercept] has been received.
- *
- * @param event The [MotionEvent] to handle.
- * @param viewWidthPx The width of the view, in pixels.
- * @return `true` if the event was consumed, `false` otherwise.
- */
- fun onTouchEvent(event: MotionEvent, viewWidthPx: Int): Boolean {
- return when (event.actionMasked) {
- MotionEvent.ACTION_MOVE -> {
- interactionState?.let {
- if (it.isDraggingShade) {
- val pointerIndex = event.findPointerIndex(it.pointerId)
- val previousY = it.currentY
- val currentY = event.getY(pointerIndex)
- interactionState = it.copy(currentY = currentY)
-
- val yDragAmountPx = currentY - previousY
-
- if (yDragAmountPx != 0f) {
- multiShadeInteractor.sendProxiedInput(
- ProxiedInputModel.OnDrag(
- xFraction = event.x / viewWidthPx,
- yDragAmountPx = yDragAmountPx,
- )
- )
- }
- true
- } else {
- onMove(event)
- isDraggingShade()
- }
- }
- ?: false
- }
- MotionEvent.ACTION_UP -> {
- if (isDraggingShade()) {
- // We finished dragging the shade. Record that so the multi-shade framework can
- // issue a fling, if the velocity reached in the drag was high enough, for
- // example.
- multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragEnd)
-
- if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
- multiShadeInteractor.collapseAll()
- }
- }
-
- interactionState = null
- true
- }
- MotionEvent.ACTION_POINTER_UP -> {
- val removedPointerId = event.getPointerId(event.actionIndex)
- if (removedPointerId == interactionState?.pointerId && event.pointerCount > 1) {
- // We removed the original pointer but there must be another pointer because the
- // gesture is still ongoing. Let's switch to that pointer.
- interactionState =
- event.firstUnremovedPointerId(removedPointerId)?.let { replacementPointerId
- ->
- interactionState?.copy(
- pointerId = replacementPointerId,
- // We want to update the currentY of our state so that the
- // transition to the next pointer doesn't report a big jump between
- // the Y coordinate of the removed pointer and the Y coordinate of
- // the replacement pointer.
- currentY = event.getY(replacementPointerId),
- )
- }
- }
- true
- }
- MotionEvent.ACTION_CANCEL -> {
- if (isDraggingShade()) {
- // Our drag gesture was canceled by the system. This happens primarily in one of
- // two occasions: (a) the parent view has decided to intercept the gesture
- // itself and/or route it to a different child view or (b) the pointer has
- // traveled beyond the bounds of our view and/or the touch display. Either way,
- // we pass the cancellation event to the multi-shade framework to record it.
- // Doing that allows the multi-shade framework to know that the gesture ended to
- // allow new gestures to be accepted.
- multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragCancel)
-
- if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
- multiShadeInteractor.collapseAll()
- }
- }
-
- interactionState = null
- true
- }
- else -> false
- }
- }
-
- /**
- * Handles [MotionEvent.ACTION_MOVE] and sets whether or not we are dragging shade in our
- * current interaction
- *
- * @param event The [MotionEvent] to handle.
- */
- private fun onMove(event: MotionEvent) {
- interactionState?.let {
- val pointerIndex = event.findPointerIndex(it.pointerId)
- val currentX = event.getX(pointerIndex)
- val currentY = event.getY(pointerIndex)
- if (!it.isDraggingHorizontally && !it.isDraggingShade) {
- val xDistanceTravelled = currentX - it.initialX
- val yDistanceTravelled = currentY - it.initialY
- val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop
- interactionState =
- when {
- yDistanceTravelled > touchSlop -> it.copy(isDraggingShade = true)
- abs(xDistanceTravelled) > touchSlop ->
- it.copy(isDraggingHorizontally = true)
- else -> interactionState
- }
- }
- }
- }
-
- private data class InteractionState(
- val initialX: Float,
- val initialY: Float,
- val currentY: Float,
- val pointerId: Int,
- /** Whether the current gesture is dragging horizontally. */
- val isDraggingHorizontally: Boolean,
- /** Whether the current gesture is dragging the shade vertically. */
- val isDraggingShade: Boolean,
- )
-
- private fun isDraggingShade(): Boolean {
- return interactionState?.isDraggingShade ?: false
- }
-
- /**
- * Returns the index of the first pointer that is not [removedPointerId] or `null`, if there is
- * no other pointer.
- */
- private fun MotionEvent.firstUnremovedPointerId(removedPointerId: Int): Int? {
- return (0 until pointerCount)
- .firstOrNull { pointerIndex ->
- val pointerId = getPointerId(pointerIndex)
- pointerId != removedPointerId
- }
- ?.let { pointerIndex -> getPointerId(pointerIndex) }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt
deleted file mode 100644
index c2eaf72a841a..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.shared.math
-
-import androidx.annotation.VisibleForTesting
-import kotlin.math.abs
-
-/** Returns `true` if this [Float] is within [epsilon] of `0`. */
-fun Float.isZero(epsilon: Float = EPSILON): Boolean {
- return abs(this) < epsilon
-}
-
-@VisibleForTesting private const val EPSILON = 0.0001f
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
deleted file mode 100644
index ee1dd65b867f..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.shared.model
-
-import androidx.annotation.FloatRange
-
-/**
- * Models a part of an ongoing proxied user input gesture.
- *
- * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
- * In other words: it's not user input that's occurring directly on the shade UI itself.
- */
-sealed class ProxiedInputModel {
- /** The user is dragging their pointer. */
- data class OnDrag(
- /**
- * The relative position of the pointer as a fraction of its container width where `0` is
- * all the way to the left and `1` is all the way to the right.
- */
- @FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
- /** The amount that the pointer was dragged, in pixels. */
- val yDragAmountPx: Float,
- ) : ProxiedInputModel()
-
- /** The user finished dragging by lifting up their pointer. */
- object OnDragEnd : ProxiedInputModel()
-
- /**
- * The drag gesture has been canceled. Usually because the pointer exited the draggable area.
- */
- object OnDragCancel : ProxiedInputModel()
-
- /** The user has tapped (clicked). */
- object OnTap : ProxiedInputModel()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
deleted file mode 100644
index a4cd35c8a11a..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.shared.model
-
-import androidx.annotation.FloatRange
-
-/** Enumerates the various possible configurations of the shade system. */
-sealed class ShadeConfig(
-
- /** IDs of the shade(s) in this configuration. */
- open val shadeIds: List<ShadeId>,
-
- /**
- * The amount that the user must swipe up when the shade is fully expanded to automatically
- * collapse once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully expanded once the user stops swiping.
- */
- @FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float,
-
- /**
- * The amount that the user must swipe down when the shade is fully collapsed to automatically
- * expand once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully collapsed once the user stops swiping.
- */
- @FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float,
-) {
-
- /** There is a single shade. */
- data class SingleShadeConfig(
- @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
- @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
- ) :
- ShadeConfig(
- shadeIds = listOf(ShadeId.SINGLE),
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- )
-
- /** There are two shades arranged side-by-side. */
- data class DualShadeConfig(
- /** Width of the left-hand side shade. */
- val leftShadeWidthPx: Int,
- /** Width of the right-hand side shade. */
- val rightShadeWidthPx: Int,
- @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
- @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
- /**
- * The position of the "split" between interaction areas for each of the shades, as a
- * fraction of the width of the container.
- *
- * Interactions that occur on the start-side (left-hand side in left-to-right languages like
- * English) affect the start-side shade. Interactions that occur on the end-side (right-hand
- * side in left-to-right languages like English) affect the end-side shade.
- */
- @FloatRange(from = 0.0, to = 1.0) val splitFraction: Float,
- /** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */
- @FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float,
- ) :
- ShadeConfig(
- shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT),
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- )
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
deleted file mode 100644
index 9e026576e842..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.shared.model
-
-/** Enumerates all known shade IDs. */
-enum class ShadeId {
- /** ID of the shade on the left in dual shade configurations. */
- LEFT,
- /** ID of the shade on the right in dual shade configurations. */
- RIGHT,
- /** ID of the single shade in single shade configurations. */
- SINGLE,
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
deleted file mode 100644
index aecec39c5c07..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.view
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.compose.ComposeFacade
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.launch
-
-/**
- * View that hosts the multi-shade system and acts as glue between legacy code and the
- * implementation.
- */
-class MultiShadeView(
- context: Context,
- attrs: AttributeSet?,
-) :
- FrameLayout(
- context,
- attrs,
- ) {
-
- fun init(
- interactor: MultiShadeInteractor,
- clock: SystemClock,
- ) {
- repeatWhenAttached {
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- addView(
- ComposeFacade.createMultiShadeView(
- context = context,
- viewModel =
- MultiShadeViewModel(
- viewModelScope = this,
- interactor = interactor,
- ),
- clock = clock,
- )
- )
- }
-
- // Here when destroyed.
- removeAllViews()
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
deleted file mode 100644
index ed92c5469d23..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.viewmodel
-
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Models UI state for UI that supports multi (or single) shade. */
-@OptIn(ExperimentalCoroutinesApi::class)
-class MultiShadeViewModel(
- viewModelScope: CoroutineScope,
- private val interactor: MultiShadeInteractor,
-) {
- /** Models UI state for the single shade. */
- val singleShade =
- ShadeViewModel(
- viewModelScope,
- ShadeId.SINGLE,
- interactor,
- )
-
- /** Models UI state for the shade on the left-hand side. */
- val leftShade =
- ShadeViewModel(
- viewModelScope,
- ShadeId.LEFT,
- interactor,
- )
-
- /** Models UI state for the shade on the right-hand side. */
- val rightShade =
- ShadeViewModel(
- viewModelScope,
- ShadeId.RIGHT,
- interactor,
- )
-
- /** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */
- val scrimAlpha: StateFlow<Float> =
- combine(
- interactor.maxShadeExpansion,
- interactor.shadeConfig
- .map { it as? ShadeConfig.DualShadeConfig }
- .map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f },
- ::Pair,
- )
- .map { (anyShadeExpansion, scrimAlpha) ->
- (anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f)
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = 0f,
- )
-
- /** Whether the scrim should accept touch events. */
- val isScrimEnabled: StateFlow<Boolean> =
- interactor.shadeConfig
- .flatMapLatest { shadeConfig ->
- when (shadeConfig) {
- // In the dual shade configuration, the scrim is enabled when the expansion is
- // greater than zero on any one of the shades.
- is ShadeConfig.DualShadeConfig -> interactor.isAnyShadeExpanded
- // No scrim in the single shade configuration.
- is ShadeConfig.SingleShadeConfig -> flowOf(false)
- }
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
-
- /** Notifies that the scrim has been touched. */
- fun onScrimTouched(proxiedInput: ProxiedInputModel) {
- if (!isScrimEnabled.value) {
- return
- }
-
- interactor.sendProxiedInput(proxiedInput)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
deleted file mode 100644
index e828dbdc6c62..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.viewmodel
-
-import androidx.annotation.FloatRange
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Models UI state for a single shade. */
-class ShadeViewModel(
- viewModelScope: CoroutineScope,
- private val shadeId: ShadeId,
- private val interactor: MultiShadeInteractor,
-) {
- /** Whether the shade is visible. */
- val isVisible: StateFlow<Boolean> =
- interactor
- .isVisible(shadeId)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
-
- /** Whether swiping on the shade UI is currently enabled. */
- val isSwipingEnabled: StateFlow<Boolean> =
- interactor
- .isNonProxiedInputAllowed(shadeId)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
-
- /** Whether the shade must be collapsed immediately. */
- val isForceCollapsed: Flow<Boolean> =
- interactor.isForceCollapsed(shadeId).distinctUntilChanged()
-
- /** The width of the shade. */
- val width: StateFlow<Size> =
- interactor.shadeConfig
- .map { shadeWidth(it) }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = shadeWidth(interactor.shadeConfig.value),
- )
-
- /**
- * The amount that the user must swipe up when the shade is fully expanded to automatically
- * collapse once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully expanded once the user stops swiping.
- */
- val swipeCollapseThreshold: StateFlow<Float> =
- interactor.shadeConfig
- .map { it.swipeCollapseThreshold }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = interactor.shadeConfig.value.swipeCollapseThreshold,
- )
-
- /**
- * The amount that the user must swipe down when the shade is fully collapsed to automatically
- * expand once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully collapsed once the user stops swiping.
- */
- val swipeExpandThreshold: StateFlow<Float> =
- interactor.shadeConfig
- .map { it.swipeExpandThreshold }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = interactor.shadeConfig.value.swipeExpandThreshold,
- )
-
- /**
- * Proxied input affecting the shade. This is input coming from sources outside of system UI
- * (for example, swiping down on the Launcher or from the status bar) or outside the UI of any
- * shade (for example, the scrim that's shown behind the shades).
- */
- val proxiedInput: Flow<ProxiedInputModel?> =
- interactor
- .proxiedInput(shadeId)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null,
- )
-
- /** Notifies that the expansion amount for the shade has changed. */
- fun onExpansionChanged(
- expansion: Float,
- ) {
- interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f))
- }
-
- /** Notifies that a drag gesture has started. */
- fun onDragStarted() {
- interactor.onUserInteractionStarted(shadeId)
- }
-
- /** Notifies that a drag gesture has ended. */
- fun onDragEnded() {
- interactor.onUserInteractionEnded(shadeId = shadeId)
- }
-
- private fun shadeWidth(shadeConfig: ShadeConfig): Size {
- return when (shadeId) {
- ShadeId.LEFT ->
- Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0)
- ShadeId.RIGHT ->
- Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0)
- ShadeId.SINGLE -> Size.Fraction(1f)
- }
- }
-
- sealed class Size {
- data class Fraction(
- @FloatRange(from = 0.0, to = 1.0) val fraction: Float,
- ) : Size()
- data class Pixels(
- val pixels: Int,
- ) : Size()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 682335e0b419..e134f7c10b9b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -133,6 +133,7 @@ import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.rotation.RotationButton;
@@ -199,6 +200,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
private final SysUiState mSysUiFlagsContainer;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
private final ShadeController mShadeController;
+ private final ShadeViewController mShadeViewController;
private final NotificationRemoteInputManager mNotificationRemoteInputManager;
private final OverviewProxyService mOverviewProxyService;
private final NavigationModeController mNavigationModeController;
@@ -523,6 +525,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
@Inject
NavigationBar(
NavigationBarView navigationBarView,
+ ShadeController shadeController,
NavigationBarFrame navigationBarFrame,
@Nullable Bundle savedState,
@DisplayId Context context,
@@ -541,7 +544,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
Optional<Pip> pipOptional,
Optional<Recents> recentsOptional,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
- ShadeController shadeController,
+ ShadeViewController shadeViewController,
NotificationRemoteInputManager notificationRemoteInputManager,
NotificationShadeDepthController notificationShadeDepthController,
@Main Handler mainHandler,
@@ -577,6 +580,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
mSysUiFlagsContainer = sysUiFlagsContainer;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
mShadeController = shadeController;
+ mShadeViewController = shadeViewController;
mNotificationRemoteInputManager = notificationRemoteInputManager;
mOverviewProxyService = overviewProxyService;
mNavigationModeController = navigationModeController;
@@ -739,8 +743,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
final Display display = mView.getDisplay();
mView.setComponents(mRecentsOptional);
if (mCentralSurfacesOptionalLazy.get().isPresent()) {
- mView.setComponents(
- mCentralSurfacesOptionalLazy.get().get().getShadeViewController());
+ mView.setComponents(mShadeViewController);
}
mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer);
mView.setOnVerticalChangedListener(this::onVerticalChanged);
@@ -1341,9 +1344,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
}
private void onVerticalChanged(boolean isVertical) {
- Optional<CentralSurfaces> cs = mCentralSurfacesOptionalLazy.get();
- if (cs.isPresent() && cs.get().getShadeViewController() != null) {
- cs.get().getShadeViewController().setQsScrimEnabled(!isVertical);
+ // This check can probably be safely removed. It only remained to reduce regression
+ // risk for a broad change that removed the CentralSurfaces reference in the if block
+ if (mCentralSurfacesOptionalLazy.get().isPresent()) {
+ mShadeViewController.setQsScrimEnabled(!isVertical);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 77e2847cbe76..c4749e093854 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -26,6 +26,7 @@ import android.os.VibrationEffect
import android.util.Log
import android.util.MathUtils
import android.view.Gravity
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
@@ -36,6 +37,8 @@ import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -76,27 +79,24 @@ private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f
private const val POP_ON_INACTIVE_VELOCITY = -1.5f
internal val VIBRATE_ACTIVATED_EFFECT =
- VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
internal val VIBRATE_DEACTIVATED_EFFECT =
- VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
private const val DEBUG = false
-class BackPanelController internal constructor(
- context: Context,
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(
- BackPanel(
- context,
- latencyTracker
- )
-), NavigationEdgeBackPlugin {
+class BackPanelController
+internal constructor(
+ context: Context,
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker,
+ private val featureFlags: FeatureFlags
+) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
/**
* Injectable instance to create a new BackPanelController.
@@ -104,34 +104,37 @@ class BackPanelController internal constructor(
* Necessary because EdgeBackGestureHandler sometimes needs to create new instances of
* BackPanelController, and we need to match EdgeBackGestureHandler's context.
*/
- class Factory @Inject constructor(
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
+ class Factory
+ @Inject
+ constructor(
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker,
+ private val featureFlags: FeatureFlags
) {
- /** Construct a [BackPanelController]. */
+ /** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
- val backPanelController = BackPanelController(
+ val backPanelController =
+ BackPanelController(
context,
windowManager,
viewConfiguration,
mainHandler,
vibratorHelper,
configurationController,
- latencyTracker
- )
+ latencyTracker,
+ featureFlags
+ )
backPanelController.init()
return backPanelController
}
}
- @VisibleForTesting
- internal var params: EdgePanelParams = EdgePanelParams(resources)
- @VisibleForTesting
- internal var currentState: GestureState = GestureState.GONE
+ @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources)
+ @VisibleForTesting internal var currentState: GestureState = GestureState.GONE
private var previousState: GestureState = GestureState.GONE
// Screen attributes
@@ -167,7 +170,6 @@ class BackPanelController internal constructor(
private val elapsedTimeSinceEntry
get() = SystemClock.uptimeMillis() - gestureEntryTime
-
private var pastThresholdWhileEntryOrInactiveTime = 0L
private var entryToActiveDelay = 0F
private val entryToActiveDelayCalculation = {
@@ -206,24 +208,25 @@ class BackPanelController internal constructor(
COMMITTED,
/* back action currently cancelling, arrow soon to be GONE */
- CANCELLED;
+ CANCELLED
}
/**
* Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The
* runnable is not called if the animation is cancelled
*/
- inner class DelayedOnAnimationEndListener internal constructor(
- private val handler: Handler,
- private val runnableDelay: Long,
- val runnable: Runnable,
+ inner class DelayedOnAnimationEndListener
+ internal constructor(
+ private val handler: Handler,
+ private val runnableDelay: Long,
+ val runnable: Runnable,
) : DynamicAnimation.OnAnimationEndListener {
override fun onAnimationEnd(
- animation: DynamicAnimation<*>,
- canceled: Boolean,
- value: Float,
- velocity: Float
+ animation: DynamicAnimation<*>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
) {
animation.removeEndListener(this)
@@ -239,45 +242,43 @@ class BackPanelController internal constructor(
internal fun run() = runnable.run()
}
- private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
- updateArrowState(GestureState.COMMITTED)
- }
-
+ private val onEndSetCommittedStateListener =
+ DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) }
private val onEndSetGoneStateListener =
- DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
- cancelFailsafe()
- updateArrowState(GestureState.GONE)
- }
+ DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
+ cancelFailsafe()
+ updateArrowState(GestureState.GONE)
+ }
- private val onAlphaEndSetGoneStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
- updateRestingArrowDimens()
- if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
- scheduleFailsafe()
+ private val onAlphaEndSetGoneStateListener =
+ DelayedOnAnimationEndListener(mainHandler, 0L) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+ scheduleFailsafe()
+ }
}
- }
// Minimum of the screen's width or the predefined threshold
private var fullyStretchedThreshold = 0f
- /**
- * Used for initialization and configuration changes
- */
+ /** Used for initialization and configuration changes */
private fun updateConfiguration() {
params.update(resources)
mView.updateArrowPaint(params.arrowThickness)
minFlingDistance = viewConfiguration.scaledTouchSlop * 3
}
- private val configurationListener = object : ConfigurationController.ConfigurationListener {
- override fun onConfigChanged(newConfig: Configuration?) {
- updateConfiguration()
- }
+ private val configurationListener =
+ object : ConfigurationController.ConfigurationListener {
+ override fun onConfigChanged(newConfig: Configuration?) {
+ updateConfiguration()
+ }
- override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
- updateArrowDirection(isLayoutRtl)
+ override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
+ updateArrowDirection(isLayoutRtl)
+ }
}
- }
override fun onViewAttached() {
updateConfiguration()
@@ -320,8 +321,9 @@ class BackPanelController internal constructor(
MotionEvent.ACTION_UP -> {
when (currentState) {
GestureState.ENTRY -> {
- if (isFlungAwayFromEdge(endX = event.x) ||
- previousXTranslation > params.staticTriggerThreshold
+ if (
+ isFlungAwayFromEdge(endX = event.x) ||
+ previousXTranslation > params.staticTriggerThreshold
) {
updateArrowState(GestureState.FLUNG)
} else {
@@ -342,14 +344,16 @@ class BackPanelController internal constructor(
}
}
GestureState.ACTIVE -> {
- if (previousState == GestureState.ENTRY &&
- elapsedTimeSinceEntry
- < MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
+ if (
+ previousState == GestureState.ENTRY &&
+ elapsedTimeSinceEntry <
+ MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
) {
updateArrowState(GestureState.FLUNG)
- } else if (previousState == GestureState.INACTIVE &&
- elapsedTimeSinceInactive
- < MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
+ } else if (
+ previousState == GestureState.INACTIVE &&
+ elapsedTimeSinceInactive <
+ MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
) {
// A delay is added to allow the background to transition back to ACTIVE
// since it was briefly in INACTIVE. Without this delay, setting it
@@ -390,10 +394,10 @@ class BackPanelController internal constructor(
}
/**
- * Returns false until the current gesture exceeds the touch slop threshold,
- * and returns true thereafter (we reset on the subsequent back gesture).
- * The moment it switches from false -> true is important,
- * because that's when we switch state, from GONE -> ENTRY.
+ * Returns false until the current gesture exceeds the touch slop threshold, and returns true
+ * thereafter (we reset on the subsequent back gesture). The moment it switches from false ->
+ * true is important, because that's when we switch state, from GONE -> ENTRY.
+ *
* @return whether the current gesture has moved past a minimum threshold.
*/
private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
@@ -416,7 +420,8 @@ class BackPanelController internal constructor(
val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold
when (currentState) {
GestureState.ENTRY -> {
- if (isPastThresholdToActive(
+ if (
+ isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold,
dynamicDelay = entryToActiveDelayCalculation
)
@@ -428,8 +433,10 @@ class BackPanelController internal constructor(
val isPastDynamicReactivationThreshold =
totalTouchDeltaInactive >= params.reactivationTriggerThreshold
- if (isPastThresholdToActive(
- isPastThreshold = isPastStaticThreshold &&
+ if (
+ isPastThresholdToActive(
+ isPastThreshold =
+ isPastStaticThreshold &&
isPastDynamicReactivationThreshold &&
isWithinYActivationThreshold,
delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
@@ -489,19 +496,19 @@ class BackPanelController internal constructor(
// Add a slop to to prevent small jitters when arrow is at edge in
// emitting small values that cause the arrow to poke out slightly
val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat()
- totalTouchDeltaInactive = totalTouchDeltaInactive
- .plus(xDelta)
- .coerceAtLeast(minimumDelta)
+ totalTouchDeltaInactive =
+ totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta)
}
updateArrowStateOnMove(yTranslation, xTranslation)
- val gestureProgress = when (currentState) {
- GestureState.ACTIVE -> fullScreenProgress(xTranslation)
- GestureState.ENTRY -> staticThresholdProgress(xTranslation)
- GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
- else -> null
- }
+ val gestureProgress =
+ when (currentState) {
+ GestureState.ACTIVE -> fullScreenProgress(xTranslation)
+ GestureState.ENTRY -> staticThresholdProgress(xTranslation)
+ GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
+ else -> null
+ }
gestureProgress?.let {
when (currentState) {
@@ -517,27 +524,30 @@ class BackPanelController internal constructor(
}
private fun setArrowStrokeAlpha(gestureProgress: Float?) {
- val strokeAlphaProgress = when (currentState) {
- GestureState.ENTRY -> gestureProgress
- GestureState.INACTIVE -> gestureProgress
- GestureState.ACTIVE,
- GestureState.FLUNG,
- GestureState.COMMITTED -> 1f
- GestureState.CANCELLED,
- GestureState.GONE -> 0f
- }
+ val strokeAlphaProgress =
+ when (currentState) {
+ GestureState.ENTRY -> gestureProgress
+ GestureState.INACTIVE -> gestureProgress
+ GestureState.ACTIVE,
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> 1f
+ GestureState.CANCELLED,
+ GestureState.GONE -> 0f
+ }
- val indicator = when (currentState) {
- GestureState.ENTRY -> params.entryIndicator
- GestureState.INACTIVE -> params.preThresholdIndicator
- GestureState.ACTIVE -> params.activeIndicator
- else -> params.preThresholdIndicator
- }
+ val indicator =
+ when (currentState) {
+ GestureState.ENTRY -> params.entryIndicator
+ GestureState.INACTIVE -> params.preThresholdIndicator
+ GestureState.ACTIVE -> params.activeIndicator
+ else -> params.preThresholdIndicator
+ }
strokeAlphaProgress?.let { progress ->
- indicator.arrowDimens.alphaSpring?.get(progress)?.takeIf { it.isNewState }?.let {
- mView.popArrowAlpha(0f, it.value)
- }
+ indicator.arrowDimens.alphaSpring
+ ?.get(progress)
+ ?.takeIf { it.isNewState }
+ ?.let { mView.popArrowAlpha(0f, it.value) }
}
}
@@ -546,15 +556,16 @@ class BackPanelController internal constructor(
val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
val rubberbandAmount = 15f
val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
- val yPosition = params.verticalTranslationInterpolator.getInterpolation(yProgress) *
+ val yPosition =
+ params.verticalTranslationInterpolator.getInterpolation(yProgress) *
maxYOffset *
sign(yOffset)
mView.animateVertically(yPosition)
}
/**
- * Tracks the relative position of the drag from the time after the arrow is activated until
- * the arrow is fully stretched (between 0.0 - 1.0f)
+ * Tracks the relative position of the drag from the time after the arrow is activated until the
+ * arrow is fully stretched (between 0.0 - 1.0f)
*/
private fun fullScreenProgress(xTranslation: Float): Float {
val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold
@@ -575,35 +586,32 @@ class BackPanelController internal constructor(
private fun stretchActiveBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = params.horizontalTranslationInterpolator
- .getInterpolation(progress),
- arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
- backgroundWidthStretchAmount = params.activeWidthInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- backgroundHeightStretchAmount = 1f,
- arrowAlphaStretchAmount = 1f,
- edgeCornerStretchAmount = 1f,
- farCornerStretchAmount = 1f,
- fullyStretchedDimens = params.fullyStretchedIndicator
+ horizontalTranslationStretchAmount =
+ params.horizontalTranslationInterpolator.getInterpolation(progress),
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount =
+ params.activeWidthInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ backgroundHeightStretchAmount = 1f,
+ arrowAlphaStretchAmount = 1f,
+ edgeCornerStretchAmount = 1f,
+ farCornerStretchAmount = 1f,
+ fullyStretchedDimens = params.fullyStretchedIndicator
)
}
private fun stretchEntryBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = params.arrowAngleInterpolator
- .getInterpolation(progress),
- backgroundWidthStretchAmount = params.entryWidthInterpolator
- .getInterpolation(progress),
- backgroundHeightStretchAmount = params.heightInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- arrowAlphaStretchAmount = params.entryIndicator.arrowDimens
- .alphaInterpolator?.get(progress)?.value ?: 0f,
- edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
- farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
- fullyStretchedDimens = params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount =
+ params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -612,31 +620,32 @@ class BackPanelController internal constructor(
val interpolator = run {
val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
if (isPastSlop) {
- if (totalTouchDeltaInactive > 0) {
- params.entryWidthInterpolator
+ if (totalTouchDeltaInactive > 0) {
+ params.entryWidthInterpolator
+ } else {
+ params.entryWidthTowardsEdgeInterpolator
+ }
} else {
- params.entryWidthTowardsEdgeInterpolator
+ previousPreThresholdWidthInterpolator
}
- } else {
- previousPreThresholdWidthInterpolator
- }.also { previousPreThresholdWidthInterpolator = it }
+ .also { previousPreThresholdWidthInterpolator = it }
}
return interpolator.getInterpolation(progress).coerceAtLeast(0f)
}
private fun stretchInactiveBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
- backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
- backgroundHeightStretchAmount = params.heightInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- arrowAlphaStretchAmount = params.preThresholdIndicator.arrowDimens
- .alphaInterpolator?.get(progress)?.value ?: 0f,
- edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
- farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
- fullyStretchedDimens = params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount =
+ params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value
+ ?: 0f,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -647,11 +656,12 @@ class BackPanelController internal constructor(
override fun setIsLeftPanel(isLeftPanel: Boolean) {
mView.isLeftPanel = isLeftPanel
- layoutParams.gravity = if (isLeftPanel) {
- Gravity.LEFT or Gravity.TOP
- } else {
- Gravity.RIGHT or Gravity.TOP
- }
+ layoutParams.gravity =
+ if (isLeftPanel) {
+ Gravity.LEFT or Gravity.TOP
+ } else {
+ Gravity.RIGHT or Gravity.TOP
+ }
}
override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
@@ -667,12 +677,14 @@ class BackPanelController internal constructor(
private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX
- val flingVelocity = velocityTracker?.run {
- computeCurrentVelocity(PX_PER_SEC)
- xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
- } ?: 0f
+ val flingVelocity =
+ velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_SEC)
+ xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+ }
+ ?: 0f
val isPastFlingVelocityThreshold =
- flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
+ flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
}
@@ -699,8 +711,8 @@ class BackPanelController internal constructor(
}
private fun playWithBackgroundWidthAnimation(
- onEnd: DelayedOnAnimationEndListener,
- delay: Long = 0L
+ onEnd: DelayedOnAnimationEndListener,
+ delay: Long = 0L
) {
if (delay == 0L) {
updateRestingArrowDimens()
@@ -724,104 +736,103 @@ class BackPanelController internal constructor(
fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold)
}
- /**
- * Updates resting arrow and background size not accounting for stretch
- */
+ /** Updates resting arrow and background size not accounting for stretch */
private fun updateRestingArrowDimens() {
when (currentState) {
GestureState.GONE,
GestureState.ENTRY -> {
mView.setSpring(
- arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
- scale = params.entryIndicator.scaleSpring,
- verticalTranslation = params.entryIndicator.verticalTranslationSpring,
- horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
- backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
- backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+ scale = params.entryIndicator.scaleSpring,
+ verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+ horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+ backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.entryIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.INACTIVE -> {
mView.setSpring(
- arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
- horizontalTranslation = params.preThresholdIndicator
- .horizontalTranslationSpring,
- scale = params.preThresholdIndicator.scaleSpring,
- backgroundWidth = params.preThresholdIndicator.backgroundDimens
- .widthSpring,
- backgroundHeight = params.preThresholdIndicator.backgroundDimens
- .heightSpring,
- backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+ horizontalTranslation =
+ params.preThresholdIndicator.horizontalTranslationSpring,
+ scale = params.preThresholdIndicator.scaleSpring,
+ backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.ACTIVE -> {
mView.setSpring(
- arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
- scale = params.activeIndicator.scaleSpring,
- horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
- backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+ scale = params.activeIndicator.scaleSpring,
+ horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+ backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.activeIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.FLUNG -> {
mView.setSpring(
- arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
- backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+ backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.flungIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.COMMITTED -> {
mView.setSpring(
- arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
- scale = params.committedIndicator.scaleSpring,
- backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
- backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+ scale = params.committedIndicator.scaleSpring,
+ backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.committedIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.CANCELLED -> {
mView.setSpring(
- backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring)
+ backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring
+ )
}
else -> {}
}
mView.setRestingDimens(
- animate = !(currentState == GestureState.FLUNG ||
- currentState == GestureState.COMMITTED),
- restingParams = EdgePanelParams.BackIndicatorDimens(
- scale = when (currentState) {
+ animate =
+ !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED),
+ restingParams =
+ EdgePanelParams.BackIndicatorDimens(
+ scale =
+ when (currentState) {
GestureState.ACTIVE,
- GestureState.FLUNG,
- -> params.activeIndicator.scale
+ GestureState.FLUNG, -> params.activeIndicator.scale
GestureState.COMMITTED -> params.committedIndicator.scale
else -> params.preThresholdIndicator.scale
},
- scalePivotX = when (currentState) {
+ scalePivotX =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE,
@@ -830,7 +841,8 @@ class BackPanelController internal constructor(
GestureState.FLUNG,
GestureState.COMMITTED -> params.committedIndicator.scalePivotX
},
- horizontalTranslation = when (currentState) {
+ horizontalTranslation =
+ when (currentState) {
GestureState.GONE -> {
params.activeIndicator.backgroundDimens.width?.times(-1)
}
@@ -843,7 +855,8 @@ class BackPanelController internal constructor(
}
else -> null
},
- arrowDimens = when (currentState) {
+ arrowDimens =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.arrowDimens
@@ -852,7 +865,8 @@ class BackPanelController internal constructor(
GestureState.COMMITTED -> params.committedIndicator.arrowDimens
GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
},
- backgroundDimens = when (currentState) {
+ backgroundDimens =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
@@ -894,7 +908,7 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
backCallback.setTriggerBack(true)
}
- GestureState.GONE -> { }
+ GestureState.GONE -> {}
}
when (currentState) {
@@ -913,18 +927,25 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
previousXTranslationOnActiveOffset = previousXTranslation
updateRestingArrowDimens()
- vibratorHelper.cancel()
- mainHandler.postDelayed(10L) {
- vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
- }
- val popVelocity = if (previousState == GestureState.INACTIVE) {
- POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
+ if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ vibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
+ )
} else {
- POP_ON_ENTRY_TO_ACTIVE_VELOCITY
+ vibratorHelper.cancel()
+ mainHandler.postDelayed(10L) {
+ vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+ }
}
+ val popVelocity =
+ if (previousState == GestureState.INACTIVE) {
+ POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
+ } else {
+ POP_ON_ENTRY_TO_ACTIVE_VELOCITY
+ }
mView.popOffEdge(popVelocity)
}
-
GestureState.INACTIVE -> {
gestureInactiveTime = SystemClock.uptimeMillis()
@@ -937,7 +958,14 @@ class BackPanelController internal constructor(
mView.popOffEdge(POP_ON_INACTIVE_VELOCITY)
- vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ vibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
+ )
+ } else {
+ vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ }
updateRestingArrowDimens()
}
GestureState.FLUNG -> {
@@ -945,8 +973,10 @@ class BackPanelController internal constructor(
mView.popScale(POP_ON_FLING_VELOCITY)
}
updateRestingArrowDimens()
- mainHandler.postDelayed(onEndSetCommittedStateListener.runnable,
- MIN_DURATION_FLING_ANIMATION)
+ mainHandler.postDelayed(
+ onEndSetCommittedStateListener.runnable,
+ MIN_DURATION_FLING_ANIMATION
+ )
}
GestureState.COMMITTED -> {
// In most cases, animating between states is handled via `updateRestingArrowDimens`
@@ -956,36 +986,43 @@ class BackPanelController internal constructor(
// manually play these kinds of animations in parallel.
if (previousState == GestureState.FLUNG) {
updateRestingArrowDimens()
- mainHandler.postDelayed(onEndSetGoneStateListener.runnable,
- MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION)
+ mainHandler.postDelayed(
+ onEndSetGoneStateListener.runnable,
+ MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION
+ )
} else {
mView.popScale(POP_ON_COMMITTED_VELOCITY)
- mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable,
- MIN_DURATION_COMMITTED_ANIMATION)
+ mainHandler.postDelayed(
+ onAlphaEndSetGoneStateListener.runnable,
+ MIN_DURATION_COMMITTED_ANIMATION
+ )
}
}
GestureState.CANCELLED -> {
val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
- val springForceOnCancelled = params.cancelledIndicator
- .arrowDimens.alphaSpring?.get(0f)?.value
+ val springForceOnCancelled =
+ params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value
mView.popArrowAlpha(0f, springForceOnCancelled)
- mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+ if (!featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION))
+ mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
}
}
}
private fun convertVelocityToAnimationFactor(
- valueOnFastVelocity: Float,
- valueOnSlowVelocity: Float,
- fastVelocityBound: Float = 1f,
- slowVelocityBound: Float = 0.5f,
+ valueOnFastVelocity: Float,
+ valueOnSlowVelocity: Float,
+ fastVelocityBound: Float = 1f,
+ slowVelocityBound: Float = 0.5f,
): Float {
- val factor = velocityTracker?.run {
- computeCurrentVelocity(PX_PER_MS)
- MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
- } ?: valueOnFastVelocity
+ val factor =
+ velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_MS)
+ MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
+ }
+ ?: valueOnFastVelocity
return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
}
@@ -1014,77 +1051,76 @@ class BackPanelController internal constructor(
}
init {
- if (DEBUG) mView.drawDebugInfo = { canvas ->
- val debugStrings = listOf(
- "$currentState",
- "startX=$startX",
- "startY=$startY",
- "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
- "xTranslation=${"%.1f".format(previousXTranslation)}",
- "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
- "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
- )
- val debugPaint = Paint().apply {
- color = Color.WHITE
- }
- val debugInfoBottom = debugStrings.size * 32f + 4f
- canvas.drawRect(
+ if (DEBUG)
+ mView.drawDebugInfo = { canvas ->
+ val preProgress = staticThresholdProgress(previousXTranslation) * 100
+ val postProgress = fullScreenProgress(previousXTranslation) * 100
+ val debugStrings =
+ listOf(
+ "$currentState",
+ "startX=$startX",
+ "startY=$startY",
+ "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
+ "xTranslation=${"%.1f".format(previousXTranslation)}",
+ "pre=${"%.0f".format(preProgress)}%",
+ "post=${"%.0f".format(postProgress)}%"
+ )
+ val debugPaint = Paint().apply { color = Color.WHITE }
+ val debugInfoBottom = debugStrings.size * 32f + 4f
+ canvas.drawRect(
4f,
4f,
canvas.width.toFloat(),
debugStrings.size * 32f + 4f,
debugPaint
- )
- debugPaint.apply {
- color = Color.BLACK
- textSize = 32f
- }
- var offset = 32f
- for (debugText in debugStrings) {
- canvas.drawText(debugText, 10f, offset, debugPaint)
- offset += 32f
- }
- debugPaint.apply {
- color = Color.RED
- style = Paint.Style.STROKE
- strokeWidth = 4f
- }
- val canvasWidth = canvas.width.toFloat()
- val canvasHeight = canvas.height.toFloat()
- canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
-
- fun drawVerticalLine(x: Float, color: Int) {
- debugPaint.color = color
- val x = if (mView.isLeftPanel) x else canvasWidth - x
- canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
- }
+ )
+ debugPaint.apply {
+ color = Color.BLACK
+ textSize = 32f
+ }
+ var offset = 32f
+ for (debugText in debugStrings) {
+ canvas.drawText(debugText, 10f, offset, debugPaint)
+ offset += 32f
+ }
+ debugPaint.apply {
+ color = Color.RED
+ style = Paint.Style.STROKE
+ strokeWidth = 4f
+ }
+ val canvasWidth = canvas.width.toFloat()
+ val canvasHeight = canvas.height.toFloat()
+ canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
+
+ fun drawVerticalLine(x: Float, color: Int) {
+ debugPaint.color = color
+ val x = if (mView.isLeftPanel) x else canvasWidth - x
+ canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
+ }
- drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
- drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
- drawVerticalLine(x = startX, color = Color.GREEN)
- drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
- }
+ drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = startX, color = Color.GREEN)
+ drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
+ }
}
}
/**
- * In addition to a typical step function which returns one or two
- * values based on a threshold, `Step` also gracefully handles quick
- * changes in input near the threshold value that would typically
- * result in the output rapidly changing.
+ * In addition to a typical step function which returns one or two values based on a threshold,
+ * `Step` also gracefully handles quick changes in input near the threshold value that would
+ * typically result in the output rapidly changing.
*
- * In the context of Back arrow, the arrow's stroke opacity should
- * always appear transparent or opaque. Using a typical Step function,
- * this would resulting in a flickering appearance as the output would
- * change rapidly. `Step` addresses this by moving the threshold after
- * it is crossed so it cannot be easily crossed again with small changes
- * in touch events.
+ * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or
+ * opaque. Using a typical Step function, this would resulting in a flickering appearance as the
+ * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so
+ * it cannot be easily crossed again with small changes in touch events.
*/
class Step<T>(
- private val threshold: Float,
- private val factor: Float = 1.1f,
- private val postThreshold: T,
- private val preThreshold: T
+ private val threshold: Float,
+ private val factor: Float = 1.1f,
+ private val postThreshold: T,
+ private val preThreshold: T
) {
data class Value<T>(val value: T, val isNewState: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 48790c23e688..2adc211ef23f 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -41,6 +41,8 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
+import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser
import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
@@ -121,23 +123,26 @@ constructor(
/**
* Returns the [UserHandle] of an android user that should handle the notes taking [entryPoint].
- *
- * On company owned personally enabled (COPE) devices, if the given [entryPoint] is in the
- * [FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES] list, the default notes app in the work
- * profile user will always be launched.
- *
- * On non managed devices or devices with other management modes, the current [UserHandle] is
- * returned.
+ * 1. tail button entry point: In COPE or work profile devices, the user can select whether the
+ * work or main profile notes app should be launched in the Settings app. In non-management
+ * or device owner devices, the user can only select main profile notes app.
+ * 2. lock screen quick affordance: since there is no user setting, the main profile notes app
+ * is used as default for work profile devices while the work profile notes app is used for
+ * COPE devices.
+ * 3. Other entry point: the current user from [UserTracker.userHandle].
*/
fun getUserForHandlingNotesTaking(entryPoint: NoteTaskEntryPoint): UserHandle =
- if (
- entryPoint in FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES &&
- devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile
- ) {
- userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle
- ?: userTracker.userHandle
- } else {
- secureSettings.preferredUser
+ when {
+ entryPoint == TAIL_BUTTON -> secureSettings.preferredUser
+ devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile &&
+ entryPoint == QUICK_AFFORDANCE -> {
+ userTracker.userProfiles
+ .firstOrNull { userManager.isManagedProfile(it.id) }
+ ?.userHandle
+ ?: userTracker.userHandle
+ }
+ // On work profile devices, SysUI always run in the main user.
+ else -> userTracker.userHandle
}
/**
@@ -267,15 +272,7 @@ constructor(
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
- // If the required user matches the tracking user, the injected context is already a context
- // of the required user. Avoid calling #createContextAsUser because creating a context for
- // a user takes time.
- val userContext =
- if (user == userTracker.userHandle) {
- context
- } else {
- context.createContextAsUser(user, /* flags= */ 0)
- }
+ val userContext = context.createContextAsUser(user, /* flags= */ 0)
userContext.packageManager.setComponentEnabledSetting(
componentName,
@@ -283,7 +280,7 @@ constructor(
PackageManager.DONT_KILL_APP,
)
- debugLog { "setNoteTaskShortcutEnabled - completed: $isEnabled" }
+ debugLog { "setNoteTaskShortcutEnabled for user $user- completed: $enabledState" }
}
/**
@@ -359,10 +356,12 @@ constructor(
private val SecureSettings.preferredUser: UserHandle
get() {
+ val trackingUserId = userTracker.userHandle.identifier
val userId =
- secureSettings.getInt(
- Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
- userTracker.userHandle.identifier,
+ secureSettings.getIntForUser(
+ /* name= */ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
+ /* def= */ trackingUserId,
+ /* userHandle= */ trackingUserId,
)
return UserHandle.of(userId)
}
@@ -381,16 +380,6 @@ constructor(
* @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE
*/
const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package"
-
- /**
- * A list of entry points which should be redirected to the work profile default notes app
- * on company owned personally enabled (COPE) devices.
- *
- * Entry points in this list don't let users / admin to select the work or personal default
- * notes app to be launched.
- */
- val FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES =
- listOf(NoteTaskEntryPoint.TAIL_BUTTON, NoteTaskEntryPoint.QUICK_AFFORDANCE)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
new file mode 100644
index 000000000000..fdc70a83e8b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.privacy
+
+import android.Manifest
+import android.app.ActivityManager
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.annotation.WorkerThread
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private val defaultDialogProvider =
+ object : PrivacyDialogControllerV2.DialogProvider {
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2 {
+ return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ }
+ }
+
+/**
+ * Controller for [PrivacyDialogV2].
+ *
+ * This controller shows and dismissed the dialog, as well as determining the information to show in
+ * it.
+ */
+@SysUISingleton
+class PrivacyDialogControllerV2(
+ private val permissionManager: PermissionManager,
+ private val packageManager: PackageManager,
+ private val privacyItemController: PrivacyItemController,
+ private val userTracker: UserTracker,
+ private val activityStarter: ActivityStarter,
+ private val backgroundExecutor: Executor,
+ private val uiExecutor: Executor,
+ private val privacyLogger: PrivacyLogger,
+ private val keyguardStateController: KeyguardStateController,
+ private val appOpsController: AppOpsController,
+ private val uiEventLogger: UiEventLogger,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val dialogProvider: DialogProvider
+) {
+
+ @Inject
+ constructor(
+ permissionManager: PermissionManager,
+ packageManager: PackageManager,
+ privacyItemController: PrivacyItemController,
+ userTracker: UserTracker,
+ activityStarter: ActivityStarter,
+ @Background backgroundExecutor: Executor,
+ @Main uiExecutor: Executor,
+ privacyLogger: PrivacyLogger,
+ keyguardStateController: KeyguardStateController,
+ appOpsController: AppOpsController,
+ uiEventLogger: UiEventLogger,
+ dialogLaunchAnimator: DialogLaunchAnimator
+ ) : this(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogLaunchAnimator,
+ defaultDialogProvider
+ )
+
+ private var dialog: Dialog? = null
+
+ private val onDialogDismissed =
+ object : PrivacyDialogV2.OnDialogDismissed {
+ override fun onDialogDismissed() {
+ privacyLogger.logPrivacyDialogDismissed()
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ dialog = null
+ }
+ }
+
+ @WorkerThread
+ private fun closeApp(packageName: String, userId: Int) {
+ uiEventLogger.log(
+ PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
+ userId,
+ packageName
+ )
+ privacyLogger.logCloseAppFromDialog(packageName, userId)
+ ActivityManager.getService().stopAppForUser(packageName, userId)
+ }
+
+ @MainThread
+ private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) {
+ uiEventLogger.log(
+ PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ userId,
+ packageName
+ )
+ privacyLogger.logStartSettingsActivityFromDialog(packageName, userId)
+ startActivity(navigationIntent)
+ }
+
+ @MainThread
+ private fun openPrivacyDashboard() {
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
+ privacyLogger.logStartPrivacyDashboardFromDialog()
+ startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE))
+ }
+
+ @MainThread
+ private fun startActivity(navigationIntent: Intent) {
+ if (!keyguardStateController.isUnlocked) {
+ // If we are locked, hide the dialog so the user can unlock
+ dialog?.hide()
+ }
+ // startActivity calls internally startActivityDismissingKeyguard
+ activityStarter.startActivity(navigationIntent, true) {
+ if (ActivityManager.isStartResultSuccessful(it)) {
+ dismissDialog()
+ } else {
+ dialog?.show()
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun getStartViewPermissionUsageIntent(
+ packageName: String,
+ permGroupName: String,
+ attributionTag: CharSequence?,
+ isAttributionSupported: Boolean
+ ): Intent? {
+ if (attributionTag != null && isAttributionSupported) {
+ val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ intent.setPackage(packageName)
+ intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
+ intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString()))
+ intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true)
+ val resolveInfo =
+ packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0))
+ if (
+ resolveInfo?.activityInfo?.permission ==
+ Manifest.permission.START_VIEW_PERMISSION_USAGE
+ ) {
+ intent.component = ComponentName(packageName, resolveInfo.activityInfo.name)
+ return intent
+ }
+ }
+ return null
+ }
+
+ fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
+ val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS)
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
+ intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
+ return intent
+ }
+
+ @WorkerThread
+ private fun permGroupUsage(): List<PermissionGroupUsage> {
+ return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
+ }
+
+ /**
+ * Show the [PrivacyDialogV2]
+ *
+ * This retrieves the permission usage from [PermissionManager] and creates a new
+ * [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show.
+ *
+ * This list will be filtered by [filterAndSelect]. Only types available by
+ * [PrivacyItemController] will be shown.
+ *
+ * @param context A context to use to create the dialog.
+ * @see filterAndSelect
+ */
+ fun showDialog(context: Context, view: View? = null) {
+ dismissDialog()
+ backgroundExecutor.execute {
+ val usage = permGroupUsage()
+ val userInfos = userTracker.userProfiles
+ privacyLogger.logUnfilteredPermGroupUsage(usage)
+ val items =
+ usage.mapNotNull {
+ val userInfo =
+ userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
+ if (
+ isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall)
+ ) {
+ // Only try to get the app name if we actually need it
+ val appName =
+ if (it.isPhoneCall) {
+ ""
+ } else {
+ getLabelForPackage(it.packageName, it.uid)
+ }
+ val userId = UserHandle.getUserId(it.uid)
+ val viewUsageIntent =
+ getStartViewPermissionUsageIntent(
+ it.packageName,
+ it.permissionGroupName,
+ it.attributionTag,
+ // attributionLabel is set only when subattribution policies
+ // are supported and satisfied
+ it.attributionLabel != null
+ )
+ PrivacyDialogV2.PrivacyElement(
+ permGroupToPrivacyType(it.permissionGroupName)!!,
+ it.packageName,
+ userId,
+ appName,
+ it.attributionTag,
+ it.attributionLabel,
+ it.proxyLabel,
+ it.lastAccessTimeMillis,
+ it.isActive,
+ it.isPhoneCall,
+ viewUsageIntent != null,
+ it.permissionGroupName,
+ viewUsageIntent
+ ?: getDefaultManageAppPermissionsIntent(it.packageName, userId)
+ )
+ } else {
+ null
+ }
+ }
+ uiExecutor.execute {
+ val elements = filterAndSelect(items)
+ if (elements.isNotEmpty()) {
+ val d =
+ dialogProvider.makeDialog(
+ context,
+ elements,
+ this::manageApp,
+ this::closeApp,
+ this::openPrivacyDashboard
+ )
+ d.setShowForAllUsers(true)
+ d.addOnDismissListener(onDialogDismissed)
+ if (view != null) {
+ dialogLaunchAnimator.showFromView(d, view)
+ } else {
+ d.show()
+ }
+ privacyLogger.logShowDialogV2Contents(elements)
+ dialog = d
+ } else {
+ privacyLogger.logEmptyDialog()
+ }
+ }
+ }
+ }
+
+ /** Dismisses the dialog */
+ fun dismissDialog() {
+ dialog?.dismiss()
+ }
+
+ @WorkerThread
+ private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
+ return try {
+ packageManager
+ .getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
+ .loadLabel(packageManager)
+ } catch (_: PackageManager.NameNotFoundException) {
+ privacyLogger.logLabelNotFound(packageName)
+ packageName
+ }
+ }
+
+ private fun permGroupToPrivacyType(group: String): PrivacyType? {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
+ Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
+ Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
+ else -> null
+ }
+ }
+
+ private fun isAvailable(group: String): Boolean {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable
+ Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable
+ Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable
+ else -> false
+ }
+ }
+
+ /**
+ * Filters the list of elements to show.
+ *
+ * For each privacy type, it'll return all active elements. If there are no active elements,
+ * it'll return the most recent access
+ */
+ private fun filterAndSelect(
+ list: List<PrivacyDialogV2.PrivacyElement>
+ ): List<PrivacyDialogV2.PrivacyElement> {
+ return list
+ .groupBy { it.type }
+ .toSortedMap()
+ .flatMap { (_, elements) ->
+ val actives = elements.filter { it.isActive }
+ if (actives.isNotEmpty()) {
+ actives.sortedByDescending { it.lastActiveTimestamp }
+ } else {
+ elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) }
+ ?: emptyList()
+ }
+ }
+ }
+
+ /**
+ * Interface to create a [PrivacyDialogV2].
+ *
+ * Can be used to inject a mock creator.
+ */
+ interface DialogProvider {
+ /** Create a [PrivacyDialogV2]. */
+ fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
index 3ecc5a5e5b00..250976cf47a4 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
@@ -18,13 +18,20 @@ package com.android.systemui.privacy
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
@UiEvent(doc = "Privacy dialog is dismissed by user.")
- PRIVACY_DIALOG_DISMISSED(905);
+ PRIVACY_DIALOG_DISMISSED(905),
+
+ @UiEvent(doc = "Privacy dialog item is clicked by user to close the app using a sensor.")
+ PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP(1396),
+
+ @UiEvent(doc = "Privacy dialog is clicked by user to see the privacy dashboard.")
+ PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD(1397);
override fun getId() = _id
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
new file mode 100644
index 000000000000..f4aa27d5fcbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.privacy
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageItemInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.res.Resources.NotFoundException
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.annotation.WorkerThread
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.maybeForceFullscreen
+import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Dialog to show ongoing and recent app ops element.
+ *
+ * @param context A context to create the dialog
+ * @param list list of elements to show in the dialog. The elements will show in the same order they
+ * appear in the list
+ * @param manageApp a callback to start an activity for a given package name, user id, and intent
+ * @param closeApp a callback to close an app for a given package name, user id
+ * @param openPrivacyDashboard a callback to open the privacy dashboard
+ * @see PrivacyDialogControllerV2
+ */
+class PrivacyDialogV2(
+ context: Context,
+ private val list: List<PrivacyElement>,
+ private val manageApp: (String, Int, Intent) -> Unit,
+ private val closeApp: (String, Int) -> Unit,
+ private val openPrivacyDashboard: () -> Unit
+) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) {
+
+ private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
+ private val dismissed = AtomicBoolean(false)
+ // Note: this will call the dialog create method during init
+ private val decorViewLayoutListener = maybeForceFullscreen()?.component2()
+
+ /**
+ * Add a listener that will be called when the dialog is dismissed.
+ *
+ * If the dialog has already been dismissed, the listener will be called immediately, in the
+ * same thread.
+ */
+ fun addOnDismissListener(listener: OnDialogDismissed) {
+ if (dismissed.get()) {
+ listener.onDialogDismissed()
+ } else {
+ dismissListeners.add(WeakReference(listener))
+ }
+ }
+
+ override fun stop() {
+ dismissed.set(true)
+ val iterator = dismissListeners.iterator()
+ while (iterator.hasNext()) {
+ val el = iterator.next()
+ iterator.remove()
+ el.get()?.onDialogDismissed()
+ }
+ // Remove the layout change listener we may have added to the DecorView.
+ if (decorViewLayoutListener != null) {
+ window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window!!.setGravity(Gravity.CENTER)
+ setTitle(R.string.privacy_dialog_title)
+ setContentView(R.layout.privacy_dialog_v2)
+
+ val closeButton = requireViewById<Button>(R.id.privacy_dialog_close_button)
+ closeButton.setOnClickListener { dismiss() }
+
+ val moreButton = requireViewById<Button>(R.id.privacy_dialog_more_button)
+ moreButton.setOnClickListener { openPrivacyDashboard() }
+
+ val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
+ list.forEach { itemsContainer.addView(createView(it, itemsContainer)) }
+ }
+
+ private fun createView(element: PrivacyElement, itemsContainer: ViewGroup): View {
+ val itemCard =
+ LayoutInflater.from(context)
+ .inflate(R.layout.privacy_dialog_item_v2, itemsContainer, false) as ViewGroup
+
+ updateItemHeader(element, itemCard)
+
+ if (element.isPhoneCall) {
+ return itemCard
+ }
+
+ setItemExpansionBehavior(itemCard)
+
+ configureIndicatorActionButtons(element, itemCard)
+
+ return itemCard
+ }
+
+ private fun updateItemHeader(element: PrivacyElement, itemCard: View) {
+ val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
+ val permGroupLabel = context.packageManager.getDefaultPermGroupLabel(element.permGroupName)
+
+ val iconView = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
+ val indicatorIcon = context.getPermGroupIcon(element.permGroupName)
+ updateIconView(iconView, indicatorIcon, element.isActive)
+ iconView.contentDescription = permGroupLabel
+
+ val titleView = itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_title)!!
+ titleView.text = permGroupLabel
+ titleView.contentDescription = permGroupLabel
+
+ val usageText = getUsageText(element)
+ val summaryView =
+ itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
+ summaryView.text = usageText
+ summaryView.contentDescription = usageText
+ }
+
+ private fun configureIndicatorActionButtons(element: PrivacyElement, itemCard: View) {
+ val expandedLayout =
+ itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header_expanded_layout)!!
+
+ val buttons: MutableList<View> = mutableListOf()
+ configureCloseAppButton(element, expandedLayout)?.also { buttons.add(it) }
+ buttons.add(configureManageButton(element, expandedLayout))
+
+ val backgroundColor = getBackgroundColor(element.isActive)
+ when (buttons.size) {
+ 0 -> return
+ 1 -> {
+ val background =
+ getMutableDrawable(R.drawable.privacy_dialog_background_large_top_large_bottom)
+ background.setTint(backgroundColor)
+ buttons[0].background = background
+ }
+ else -> {
+ val firstBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_large_top_small_bottom)
+ val middleBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_small_top_small_bottom)
+ val lastBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_small_top_large_bottom)
+ firstBackground.setTint(backgroundColor)
+ middleBackground.setTint(backgroundColor)
+ lastBackground.setTint(backgroundColor)
+ buttons.forEach { it.background = middleBackground }
+ buttons.first().background = firstBackground
+ buttons.last().background = lastBackground
+ }
+ }
+ }
+
+ private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? {
+ if (element.isService || !element.isActive) {
+ return null
+ }
+ val closeAppButton =
+ window.layoutInflater.inflate(
+ R.layout.privacy_dialog_card_button,
+ expandedLayout,
+ false
+ ) as Button
+ expandedLayout.addView(closeAppButton)
+ closeAppButton.id = R.id.privacy_dialog_close_app_button
+ closeAppButton.setText(R.string.privacy_dialog_close_app_button)
+ closeAppButton.setTextColor(getForegroundColor(true))
+ closeAppButton.tag = element
+ closeAppButton.setOnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ closeApp(element.packageName, element.userId)
+ closeAppTransition(element.packageName, element.userId)
+ }
+ }
+ return closeAppButton
+ }
+
+ private fun closeAppTransition(packageName: String, userId: Int) {
+ val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
+ var shouldTransition = false
+ for (i in 0 until itemsContainer.getChildCount()) {
+ val itemCard = itemsContainer.getChildAt(i)
+ val button = itemCard.findViewById<Button>(R.id.privacy_dialog_close_app_button)
+ if (button == null || button.tag == null) {
+ continue
+ }
+ val element = button.tag as PrivacyElement
+ if (element.packageName != packageName || element.userId != userId) {
+ continue
+ }
+
+ itemCard.setEnabled(false)
+
+ val expandToggle =
+ itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
+ expandToggle.visibility = View.GONE
+
+ disableIndicatorCardUi(itemCard, element.applicationName)
+
+ val expandedLayout =
+ itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ shouldTransition = true
+ }
+ }
+ if (shouldTransition) {
+ ViewHierarchyAnimator.animateNextUpdate(window!!.decorView)
+ }
+ }
+
+ private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
+ val manageButton =
+ window.layoutInflater.inflate(
+ R.layout.privacy_dialog_card_button,
+ expandedLayout,
+ false
+ ) as Button
+ expandedLayout.addView(manageButton)
+ manageButton.id = R.id.privacy_dialog_manage_app_button
+ manageButton.setText(
+ if (element.isService) R.string.privacy_dialog_manage_service
+ else R.string.privacy_dialog_manage_permissions
+ )
+ manageButton.setTextColor(getForegroundColor(element.isActive))
+ manageButton.tag = element
+ manageButton.setOnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ manageApp(element.packageName, element.userId, element.navigationIntent)
+ }
+ }
+ return manageButton
+ }
+
+ private fun disableIndicatorCardUi(itemCard: View, applicationName: CharSequence) {
+ val iconView = itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
+ val indicatorIcon = getMutableDrawable(R.drawable.privacy_dialog_check_icon)
+ updateIconView(iconView, indicatorIcon, false)
+
+ val closedAppText =
+ context.getString(R.string.privacy_dialog_close_app_message, applicationName)
+ val summaryView = itemCard.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
+ summaryView.text = closedAppText
+ summaryView.contentDescription = closedAppText
+ }
+
+ private fun setItemExpansionBehavior(itemCard: ViewGroup) {
+ val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
+
+ val expandToggle =
+ itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ expandToggle.visibility = View.VISIBLE
+
+ ViewCompat.replaceAccessibilityAction(
+ itemCard,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null
+ )
+
+ val expandedLayout =
+ itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
+ expandedLayout.setOnClickListener {
+ // Stop clicks from propagating
+ }
+
+ itemCard.setOnClickListener {
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null
+ )
+ } else {
+ expandedLayout.visibility = View.VISIBLE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_collapse_action),
+ null
+ )
+ }
+ ViewHierarchyAnimator.animateNextUpdate(
+ rootView = window!!.decorView,
+ excludedViews = setOf(expandedLayout)
+ )
+ }
+ }
+
+ private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) {
+ indicatorIcon.setTint(getForegroundColor(active))
+ val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle)
+ backgroundIcon.setTint(getBackgroundColor(active))
+ val backgroundSize =
+ context.resources.getDimension(R.dimen.ongoing_appops_dialog_circle_size).toInt()
+ val indicatorSize =
+ context.resources.getDimension(R.dimen.ongoing_appops_dialog_icon_size).toInt()
+ iconView.setImageDrawable(
+ constructLayeredIcon(indicatorIcon, indicatorSize, backgroundIcon, backgroundSize)
+ )
+ }
+
+ @ColorInt
+ private fun getForegroundColor(active: Boolean) =
+ Utils.getColorAttrDefaultColor(
+ context,
+ if (active) com.android.internal.R.attr.materialColorOnPrimaryFixed
+ else com.android.internal.R.attr.materialColorOnSurface
+ )
+
+ @ColorInt
+ private fun getBackgroundColor(active: Boolean) =
+ Utils.getColorAttrDefaultColor(
+ context,
+ if (active) com.android.internal.R.attr.materialColorPrimaryFixed
+ else com.android.internal.R.attr.materialColorSurfaceContainerHigh
+ )
+
+ private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate()
+
+ private fun getUsageText(element: PrivacyElement) =
+ if (element.isPhoneCall) {
+ val phoneCallResId =
+ if (element.isActive) R.string.privacy_dialog_active_call_usage
+ else R.string.privacy_dialog_recent_call_usage
+ context.getString(phoneCallResId)
+ } else if (element.attributionLabel == null && element.proxyLabel == null) {
+ val usageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage
+ else R.string.privacy_dialog_recent_app_usage
+ context.getString(usageResId, element.applicationName)
+ } else if (element.attributionLabel == null || element.proxyLabel == null) {
+ val singleUsageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage_1
+ else R.string.privacy_dialog_recent_app_usage_1
+ context.getString(
+ singleUsageResId,
+ element.applicationName,
+ element.attributionLabel ?: element.proxyLabel
+ )
+ } else {
+ val doubleUsageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage_2
+ else R.string.privacy_dialog_recent_app_usage_2
+ context.getString(
+ doubleUsageResId,
+ element.applicationName,
+ element.attributionLabel,
+ element.proxyLabel
+ )
+ }
+
+ companion object {
+ private const val LOG_TAG = "PrivacyDialogV2"
+ private const val REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"
+
+ /**
+ * Gets a permission group's icon from the system.
+ *
+ * @param groupName The name of the permission group whose icon we want
+ * @return The permission group's icon, the privacy_dialog_default_permission_icon icon if
+ * the group has no icon, or the group does not exist
+ */
+ @WorkerThread
+ private fun Context.getPermGroupIcon(groupName: String): Drawable {
+ val groupInfo = packageManager.getGroupInfo(groupName)
+ if (groupInfo != null && groupInfo.icon != 0) {
+ val icon = packageManager.loadDrawable(groupInfo.packageName, groupInfo.icon)
+ if (icon != null) {
+ return icon
+ }
+ }
+
+ return getDrawable(R.drawable.privacy_dialog_default_permission_icon)!!.mutate()
+ }
+
+ /**
+ * Gets a permission group's label from the system.
+ *
+ * @param groupName The name of the permission group whose label we want
+ * @return The permission group's label, or the group name, if the group is invalid
+ */
+ @WorkerThread
+ private fun PackageManager.getDefaultPermGroupLabel(groupName: String): CharSequence {
+ val groupInfo = getGroupInfo(groupName) ?: return groupName
+ return groupInfo.loadSafeLabel(
+ this,
+ 0f,
+ TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
+ )
+ }
+
+ /**
+ * Get the [infos][PackageItemInfo] for the given permission group.
+ *
+ * @param groupName the group
+ * @return The info of permission group or null if the group does not have runtime
+ * permissions.
+ */
+ @WorkerThread
+ private fun PackageManager.getGroupInfo(groupName: String): PackageItemInfo? {
+ try {
+ return getPermissionGroupInfo(groupName, 0)
+ } catch (e: NameNotFoundException) {
+ /* ignore */
+ }
+ try {
+ return getPermissionInfo(groupName, 0)
+ } catch (e: NameNotFoundException) {
+ /* ignore */
+ }
+ return null
+ }
+
+ @WorkerThread
+ private fun PackageManager.loadDrawable(pkg: String, @DrawableRes resId: Int): Drawable? {
+ return try {
+ getResourcesForApplication(pkg).getDrawable(resId, null)?.mutate()
+ } catch (e: NotFoundException) {
+ Log.w(LOG_TAG, "Couldn't get resource", e)
+ null
+ } catch (e: NameNotFoundException) {
+ Log.w(LOG_TAG, "Couldn't get resource", e)
+ null
+ }
+ }
+
+ private fun constructLayeredIcon(
+ icon: Drawable,
+ iconSize: Int,
+ background: Drawable,
+ backgroundSize: Int
+ ): Drawable {
+ val layered = LayerDrawable(arrayOf(background, icon))
+ layered.setLayerSize(0, backgroundSize, backgroundSize)
+ layered.setLayerGravity(0, Gravity.CENTER)
+ layered.setLayerSize(1, iconSize, iconSize)
+ layered.setLayerGravity(1, Gravity.CENTER)
+ return layered
+ }
+ }
+
+ /** */
+ data class PrivacyElement(
+ val type: PrivacyType,
+ val packageName: String,
+ val userId: Int,
+ val applicationName: CharSequence,
+ val attributionTag: CharSequence?,
+ val attributionLabel: CharSequence?,
+ val proxyLabel: CharSequence?,
+ val lastActiveTimestamp: Long,
+ val isActive: Boolean,
+ val isPhoneCall: Boolean,
+ val isService: Boolean,
+ val permGroupName: String,
+ val navigationIntent: Intent
+ ) {
+ private val builder = StringBuilder("PrivacyElement(")
+
+ init {
+ builder.append("type=${type.logName}")
+ builder.append(", packageName=$packageName")
+ builder.append(", userId=$userId")
+ builder.append(", appName=$applicationName")
+ if (attributionTag != null) {
+ builder.append(", attributionTag=$attributionTag")
+ }
+ if (attributionLabel != null) {
+ builder.append(", attributionLabel=$attributionLabel")
+ }
+ if (proxyLabel != null) {
+ builder.append(", proxyLabel=$proxyLabel")
+ }
+ builder.append(", lastActive=$lastActiveTimestamp")
+ if (isActive) {
+ builder.append(", active")
+ }
+ if (isPhoneCall) {
+ builder.append(", phoneCall")
+ }
+ if (isService) {
+ builder.append(", service")
+ }
+ builder.append(", permGroupName=$permGroupName")
+ builder.append(", navigationIntent=$navigationIntent)")
+ }
+
+ override fun toString(): String = builder.toString()
+ }
+
+ /** */
+ interface OnDialogDismissed {
+ fun onDialogDismissed()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index f934346d9775..1a4642f4df74 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -18,11 +18,12 @@ package com.android.systemui.privacy.logging
import android.icu.text.SimpleDateFormat
import android.permission.PermissionGroupUsage
-import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.privacy.PrivacyDialog
+import com.android.systemui.privacy.PrivacyDialogV2
import com.android.systemui.privacy.PrivacyItem
import java.util.Locale
import javax.inject.Inject
@@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) {
+ log(LogLevel.INFO, {
+ str1 = contents.toString()
+ }, {
+ "Privacy dialog shown. Contents: $str1"
+ })
+ }
+
fun logEmptyDialog() {
log(LogLevel.WARNING, {}, {
"Trying to show an empty dialog"
@@ -147,6 +156,23 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logCloseAppFromDialog(packageName: String, userId: Int) {
+ log(LogLevel.INFO, {
+ str1 = packageName
+ int1 = userId
+ }, {
+ "Close app from dialog for packageName=$str1, userId=$int1"
+ })
+ }
+
+ fun logStartPrivacyDashboardFromDialog() {
+ log(LogLevel.INFO, {}, { "Start privacy dashboard from dialog" })
+ }
+
+ fun logLabelNotFound(packageName: String) {
+ log(LogLevel.WARNING, { str1 = packageName }, { "Label not found for: $str1" })
+ }
+
private fun listToString(list: List<PrivacyItem>): String {
return list.joinToString(separator = ", ", transform = PrivacyItem::log)
}
@@ -158,4 +184,4 @@ class PrivacyLogger @Inject constructor(
) {
buffer.log(TAG, logLevel, initializer, printer)
}
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
index 33c47cc082e1..0941a2082cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
@@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyChipEvent
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
@@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor(
private val uiEventLogger: UiEventLogger,
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
private val privacyDialogController: PrivacyDialogController,
+ private val privacyDialogControllerV2: PrivacyDialogControllerV2,
private val privacyLogger: PrivacyLogger,
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
private val permissionManager: PermissionManager,
@@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor(
private val appOpsController: AppOpsController,
private val broadcastDispatcher: BroadcastDispatcher,
private val safetyCenterManager: SafetyCenterManager,
- private val deviceProvisionedController: DeviceProvisionedController
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val featureFlags: FeatureFlags
) {
var chipVisibilityListener: ChipVisibilityListener? = null
@@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor(
// If the privacy chip is visible, it means there were some indicators
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) {
- showSafetyCenter()
+ if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
+ privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
+ } else {
+ showSafetyCenter()
+ }
} else {
privacyDialogController.showDialog(privacyChip.context)
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 207cc1398279..bf40a2d0ad51 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -78,13 +78,14 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AssistUtils;
import com.android.internal.app.IVoiceInteractionSessionListener;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -95,6 +96,8 @@ import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.buttons.KeyButtonView;
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeViewController;
@@ -121,6 +124,7 @@ import java.util.concurrent.Executor;
import java.util.function.Supplier;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* Class to send information from overview to launcher with a binder.
@@ -140,13 +144,17 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
private final Executor mMainExecutor;
private final ShellInterface mShellInterface;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
+ private final Lazy<ShadeViewController> mShadeViewControllerLazy;
private SysUiState mSysUiState;
private final Handler mHandler;
private final Lazy<NavigationBarController> mNavBarControllerLazy;
private final NotificationShadeWindowController mStatusBarWinController;
+ private final Provider<SceneInteractor> mSceneInteractor;
+
private final Runnable mConnectionRunnable = () ->
internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
private final ComponentName mRecentsComponentName;
@@ -201,11 +209,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
// TODO move this logic to message queue
mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
if (event.getActionMasked() == ACTION_DOWN) {
- ShadeViewController shadeViewController =
- centralSurfaces.getShadeViewController();
- if (shadeViewController != null) {
- shadeViewController.startExpandLatencyTracking();
- }
+ mShadeViewControllerLazy.get().startExpandLatencyTracking();
}
mHandler.post(() -> {
int action = event.getActionMasked();
@@ -213,17 +217,28 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mInputFocusTransferStarted = true;
mInputFocusTransferStartY = event.getY();
mInputFocusTransferStartMillis = event.getEventTime();
- centralSurfaces.onInputFocusTransfer(
- mInputFocusTransferStarted, false /* cancel */,
- 0 /* velocity */);
+
+ // If scene framework is enabled, set the scene container window to
+ // visible and let the touch "slip" into that window.
+ if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ mSceneInteractor.get().setVisible(
+ SceneContainerNames.SYSTEM_UI_DEFAULT, true);
+ } else {
+ centralSurfaces.onInputFocusTransfer(
+ mInputFocusTransferStarted, false /* cancel */,
+ 0 /* velocity */);
+ }
}
if (action == ACTION_UP || action == ACTION_CANCEL) {
mInputFocusTransferStarted = false;
- float velocity = (event.getY() - mInputFocusTransferStartY)
- / (event.getEventTime() - mInputFocusTransferStartMillis);
- centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
- action == ACTION_CANCEL,
- velocity);
+
+ if (!mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ float velocity = (event.getY() - mInputFocusTransferStartY)
+ / (event.getEventTime() - mInputFocusTransferStartMillis);
+ centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
+ action == ACTION_CANCEL,
+ velocity);
+ }
}
event.recycle();
});
@@ -552,8 +567,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
ShellInterface shellInterface,
Lazy<NavigationBarController> navBarControllerLazy,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+ Lazy<ShadeViewController> shadeViewControllerLazy,
NavigationModeController navModeController,
- NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
+ NotificationShadeWindowController statusBarWinController,
+ SysUiState sysUiState,
+ Provider<SceneInteractor> sceneInteractor,
UserTracker userTracker,
ScreenLifecycle screenLifecycle,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -561,6 +579,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
DisplayTracker displayTracker,
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
AssistUtils assistUtils,
+ FeatureFlags featureFlags,
DumpManager dumpManager,
Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder
) {
@@ -570,12 +589,15 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
mContext = context;
+ mFeatureFlags = featureFlags;
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
+ mShadeViewControllerLazy = shadeViewControllerLazy;
mHandler = new Handler();
mNavBarControllerLazy = navBarControllerLazy;
mStatusBarWinController = statusBarWinController;
+ mSceneInteractor = sceneInteractor;
mUserTracker = userTracker;
mConnectionBackoffAttempts = 0;
mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
@@ -677,13 +699,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mNavBarControllerLazy.get().getDefaultNavigationBar();
final NavigationBarView navBarView =
mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId());
- final ShadeViewController panelController =
- mCentralSurfacesOptionalLazy.get()
- .map(CentralSurfaces::getShadeViewController)
- .orElse(null);
if (SysUiState.DEBUG) {
Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment
- + " navBarView=" + navBarView + " panelController=" + panelController);
+ + " navBarView=" + navBarView
+ + " shadeViewController=" + mShadeViewControllerLazy.get());
}
if (navBarFragment != null) {
@@ -692,9 +711,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
if (navBarView != null) {
navBarView.updateDisabledSystemUiStateFlags(mSysUiState);
}
- if (panelController != null) {
- panelController.updateSystemUiStateFlags();
- }
+ mShadeViewControllerLazy.get().updateSystemUiStateFlags();
if (mStatusBarWinController != null) {
mStatusBarWinController.notifyStateChangedCallbacks();
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 4582370679ab..f03f040c206d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -18,11 +18,14 @@ package com.android.systemui.scene.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.SceneTransitionModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
/**
* Generic business logic and app state accessors for the scene framework.
@@ -92,4 +95,14 @@ constructor(
fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> {
return repository.sceneTransitions(containerName)
}
+
+ private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null)
+
+ /** A flow of motion events originating from outside of the scene framework. */
+ val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow()
+
+ /** Handles a remote user input. */
+ fun onRemoteUserInput(input: RemoteUserInput) {
+ _remoteUserInput.value = input
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index b23c4ec9aa8b..92384d68157b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -20,14 +20,22 @@ import com.android.systemui.CoreStartable
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.DisplayId
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
+import com.android.systemui.model.updateFlags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -49,12 +57,15 @@ constructor(
private val authenticationInteractor: AuthenticationInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val featureFlags: FeatureFlags,
+ private val sysUiState: SysUiState,
+ @DisplayId private val displayId: Int,
) : CoreStartable {
override fun start() {
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
hydrateVisibility()
automaticallySwitchScenes()
+ hydrateSystemUiState()
}
}
@@ -121,6 +132,27 @@ constructor(
}
}
+ /** Keeps [SysUiState] up-to-date */
+ private fun hydrateSystemUiState() {
+ applicationScope.launch {
+ sceneInteractor
+ .currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ .map { it.key }
+ .distinctUntilChanged()
+ .collect { sceneKey ->
+ sysUiState.updateFlags(
+ displayId,
+ SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
+ (sceneKey == SceneKey.Lockscreen),
+ )
+ }
+ }
+ }
+
private fun switchToScene(targetSceneKey: SceneKey) {
sceneInteractor.setCurrentScene(
containerName = CONTAINER_NAME,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt
new file mode 100644
index 000000000000..680de590a3fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt
@@ -0,0 +1,35 @@
+package com.android.systemui.scene.shared.model
+
+import android.view.MotionEvent
+
+/** A representation of user input that is used by the scene framework. */
+data class RemoteUserInput(
+ val x: Float,
+ val y: Float,
+ val action: RemoteUserInputAction,
+) {
+ companion object {
+ fun translateMotionEvent(event: MotionEvent): RemoteUserInput {
+ return RemoteUserInput(
+ x = event.x,
+ y = event.y,
+ action =
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> RemoteUserInputAction.DOWN
+ MotionEvent.ACTION_MOVE -> RemoteUserInputAction.MOVE
+ MotionEvent.ACTION_UP -> RemoteUserInputAction.UP
+ MotionEvent.ACTION_CANCEL -> RemoteUserInputAction.CANCEL
+ else -> RemoteUserInputAction.UNKNOWN
+ }
+ )
+ }
+ }
+}
+
+enum class RemoteUserInputAction {
+ DOWN,
+ MOVE,
+ UP,
+ CANCEL,
+ UNKNOWN,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index c456be6e5ab2..b89179289a3d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -2,6 +2,7 @@ package com.android.systemui.scene.ui.view
import android.content.Context
import android.util.AttributeSet
+import android.view.MotionEvent
import android.view.View
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -16,11 +17,15 @@ class SceneWindowRootView(
context,
attrs,
) {
+
+ private lateinit var viewModel: SceneContainerViewModel
+
fun init(
viewModel: SceneContainerViewModel,
containerConfig: SceneContainerConfig,
scenes: Set<Scene>,
) {
+ this.viewModel = viewModel
SceneWindowRootViewBinder.bind(
view = this@SceneWindowRootView,
viewModel = viewModel,
@@ -32,6 +37,14 @@ class SceneWindowRootView(
)
}
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return event?.let {
+ viewModel.onRemoteUserInput(event)
+ true
+ }
+ ?: false
+ }
+
override fun setVisibility(visibility: Int) {
// Do nothing. We don't want external callers to invoke this. Instead, we drive our own
// visibility from our view-binder.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 8c1ad9b4571b..005f48d9f250 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -16,7 +16,9 @@
package com.android.systemui.scene.ui.viewmodel
+import android.view.MotionEvent
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import kotlinx.coroutines.flow.StateFlow
@@ -26,6 +28,9 @@ class SceneContainerViewModel(
private val interactor: SceneInteractor,
val containerName: String,
) {
+ /** A flow of motion events originating from outside of the scene framework. */
+ val remoteUserInput: StateFlow<RemoteUserInput?> = interactor.remoteUserInput
+
/**
* Keys of all scenes in the container.
*
@@ -49,4 +54,9 @@ class SceneContainerViewModel(
fun setSceneTransitionProgress(progress: Float) {
interactor.setSceneTransitionProgress(containerName, progress)
}
+
+ /** Handles a [MotionEvent] representing remote user input. */
+ fun onRemoteUserInput(event: MotionEvent) {
+ interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 202d6e66d800..ed7cbffc880b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -28,6 +28,7 @@ import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -70,6 +71,7 @@ import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
+import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -147,7 +149,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -365,7 +366,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private KeyguardBottomAreaView mKeyguardBottomArea;
private boolean mExpanding;
private boolean mSplitShadeEnabled;
- private boolean mDualShadeEnabled;
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
private float mKeyguardNotificationBottomPadding;
/**
@@ -599,7 +599,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final KeyguardInteractor mKeyguardInteractor;
private final KeyguardViewConfigurator mKeyguardViewConfigurator;
- private final @Nullable MultiShadeInteractor mMultiShadeInteractor;
private final CoroutineDispatcher mMainDispatcher;
private boolean mIsAnyMultiShadeExpanded;
private boolean mIsOcclusionTransitionRunning = false;
@@ -735,7 +734,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
- Provider<MultiShadeInteractor> multiShadeInteractorProvider,
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
KeyguardInteractor keyguardInteractor,
@@ -839,8 +837,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mFeatureFlags = featureFlags;
mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
- mDualShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE);
- mMultiShadeInteractor = mDualShadeEnabled ? multiShadeInteractorProvider.get() : null;
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
@@ -1079,11 +1075,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
- if (mDualShadeEnabled) {
- collectFlow(mView, mMultiShadeInteractor.isAnyShadeExpanded(),
- mMultiShadeExpansionConsumer, mMainDispatcher);
- }
-
// Dreaming->Lockscreen
collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
mDreamingToLockscreenTransition, mMainDispatcher);
@@ -2643,12 +2634,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
if (!mStatusBarStateController.isDozing()) {
- mVibratorHelper.vibrate(
- Process.myUid(),
- mView.getContext().getPackageName(),
- ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
- "falsing-additional-tap-required",
- TOUCH_VIBRATION_ATTRIBUTES);
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
+ } else {
+ mVibratorHelper.vibrate(
+ Process.myUid(),
+ mView.getContext().getPackageName(),
+ ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
+ "falsing-additional-tap-required",
+ TOUCH_VIBRATION_ATTRIBUTES);
+ }
}
}
@@ -3515,7 +3510,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private void maybeVibrateOnOpening(boolean openingWithTouch) {
if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) {
if (!openingWithTouch || !mHasVibratedOnOpen) {
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_START
+ );
+ } else {
+ mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ }
mHasVibratedOnOpen = true;
mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 108ea68ae8e0..6afed1d76918 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -31,9 +31,6 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewStub;
-
-import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
@@ -46,7 +43,6 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dock.DockManager;
import com.android.systemui.flags.FeatureFlags;
@@ -57,9 +53,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.log.BouncerLogger;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
-import com.android.systemui.multishade.ui.view.MultiShadeView;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.DragDownHelper;
@@ -83,7 +76,6 @@ import java.util.Optional;
import java.util.function.Consumer;
import javax.inject.Inject;
-import javax.inject.Provider;
/**
* Controller for {@link NotificationShadeWindowView}.
@@ -135,7 +127,6 @@ public class NotificationShadeWindowViewController {
step.getTransitionState() == TransitionState.RUNNING;
};
private final SystemClock mClock;
- private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor;
@Inject
public NotificationShadeWindowViewController(
@@ -167,9 +158,7 @@ public class NotificationShadeWindowViewController {
KeyguardTransitionInteractor keyguardTransitionInteractor,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
FeatureFlags featureFlags,
- Provider<MultiShadeInteractor> multiShadeInteractorProvider,
SystemClock clock,
- Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
BouncerMessageInteractor bouncerMessageInteractor,
BouncerLogger bouncerLogger) {
mLockscreenShadeTransitionController = transitionController;
@@ -219,17 +208,6 @@ public class NotificationShadeWindowViewController {
progressProvider -> progressProvider.addCallback(
mDisableSubpixelTextTransitionListener));
}
- if (ComposeFacade.INSTANCE.isComposeAvailable()
- && featureFlags.isEnabled(Flags.DUAL_SHADE)) {
- mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get();
- final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub);
- if (multiShadeViewStub != null) {
- final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate();
- multiShadeView.init(multiShadeInteractorProvider.get(), clock);
- }
- } else {
- mMultiShadeMotionEventInteractor = null;
- }
}
/**
@@ -395,10 +373,7 @@ public class NotificationShadeWindowViewController {
return true;
}
- if (mMultiShadeMotionEventInteractor != null) {
- // This interactor is not null only if the dual shade feature is enabled.
- return mMultiShadeMotionEventInteractor.shouldIntercept(ev);
- } else if (mNotificationPanelViewController.isFullyExpanded()
+ if (mNotificationPanelViewController.isFullyExpanded()
&& mDragDownHelper.isDragDownEnabled()
&& !mService.isBouncerShowing()
&& !mStatusBarStateController.isDozing()) {
@@ -428,10 +403,7 @@ public class NotificationShadeWindowViewController {
return true;
}
- if (mMultiShadeMotionEventInteractor != null) {
- // This interactor is not null only if the dual shade feature is enabled.
- return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth());
- } else if (mDragDownHelper.isDragDownEnabled()
+ if (mDragDownHelper.isDragDownEnabled()
|| mDragDownHelper.isDraggingDown()) {
// we still want to finish our drag down gesture when locking the screen
return mDragDownHelper.onTouchEvent(ev) || handled;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index ee4e98e094fc..fe4832f0895b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
+import android.graphics.Point
import android.hardware.display.AmbientDisplayConfiguration
import android.os.PowerManager
import android.provider.Settings
@@ -25,6 +26,7 @@ import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.DozeInteractor
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -52,6 +54,7 @@ class PulsingGestureListener @Inject constructor(
private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
private val statusBarStateController: StatusBarStateController,
private val shadeLogger: ShadeLogger,
+ private val dozeInteractor: DozeInteractor,
userTracker: UserTracker,
tunerService: TunerService,
dumpManager: DumpManager
@@ -86,6 +89,7 @@ class PulsingGestureListener @Inject constructor(
shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
if (proximityIsNotNear && isNotAFalseTap) {
shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
+ dozeInteractor.setLastTapToWakePosition(Point(e.x.toInt(), e.y.toInt()))
powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP)
}
return true
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 92df78bac17f..6304c1ea2635 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -168,7 +168,10 @@ public class CommandQueue extends IStatusBar.Stub implements
private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT;
private static final int MSG_TOGGLE_TASKBAR = 73 << MSG_SHIFT;
-
+ private static final int MSG_SETTING_CHANGED = 74 << MSG_SHIFT;
+ private static final int MSG_LOCK_TASK_MODE_CHANGED = 75 << MSG_SHIFT;
+ private static final int MSG_CONFIRM_IMMERSIVE_PROMPT = 77 << MSG_SHIFT;
+ private static final int MSG_IMMERSIVE_CHANGED = 78 << MSG_SHIFT;
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -498,6 +501,16 @@ public class CommandQueue extends IStatusBar.Stub implements
* @see IStatusBar#showMediaOutputSwitcher
*/
default void showMediaOutputSwitcher(String packageName) {}
+
+ /**
+ * @see IStatusBar#confirmImmersivePrompt
+ */
+ default void confirmImmersivePrompt() {}
+
+ /**
+ * @see IStatusBar#immersiveModeChanged
+ */
+ default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
}
@VisibleForTesting
@@ -783,6 +796,23 @@ public class CommandQueue extends IStatusBar.Stub implements
}
@Override
+ public void confirmImmersivePrompt() {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_CONFIRM_IMMERSIVE_PROMPT).sendToTarget();
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ synchronized (mLock) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = rootDisplayAreaId;
+ args.argi2 = isImmersiveMode ? 1 : 0;
+ mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
+ }
+ }
+
+ @Override
public void appTransitionPending(int displayId) {
appTransitionPending(displayId, false /* forced */);
}
@@ -1810,6 +1840,19 @@ public class CommandQueue extends IStatusBar.Stub implements
mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName);
}
break;
+ case MSG_CONFIRM_IMMERSIVE_PROMPT:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).confirmImmersivePrompt();
+ }
+ break;
+ case MSG_IMMERSIVE_CHANGED:
+ args = (SomeArgs) msg.obj;
+ int rootDisplayAreaId = args.argi1;
+ boolean isImmersiveMode = args.argi2 != 0;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ }
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
new file mode 100644
index 000000000000..a7ec02ff43c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
+import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.StatusBarManager.DISABLE_BACK;
+import static android.app.StatusBarManager.DISABLE_HOME;
+import static android.app.StatusBarManager.DISABLE_RECENT;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
+import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
+import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
+
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.R;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.systemui.util.settings.SecureSettings;
+
+import javax.inject.Inject;
+
+/**
+ * Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
+ * entering immersive mode.
+ */
+public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Callbacks,
+ TaskStackChangeListener {
+ private static final String TAG = "ImmersiveModeConfirm";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
+ private static final String CONFIRMED = "confirmed";
+ private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE =
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
+
+ private static boolean sConfirmed;
+ private final SecureSettings mSecureSettings;
+
+ private Context mDisplayContext;
+ private final Context mSysUiContext;
+ private final Handler mHandler = new H(Looper.getMainLooper());
+ private long mShowDelayMs = 0L;
+ private final IBinder mWindowToken = new Binder();
+ private final CommandQueue mCommandQueue;
+
+ private ClingWindowView mClingWindow;
+ /** The last {@link WindowManager} that is used to add the confirmation window. */
+ @Nullable
+ private WindowManager mWindowManager;
+ /**
+ * The WindowContext that is registered with {@link #mWindowManager} with options to specify the
+ * {@link RootDisplayArea} to attach the confirmation window.
+ */
+ @Nullable
+ private Context mWindowContext;
+ /**
+ * The root display area feature id that the {@link #mWindowContext} is attaching to.
+ */
+ private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED;
+ // Local copy of vr mode enabled state, to avoid calling into VrManager with
+ // the lock held.
+ private boolean mVrModeEnabled = false;
+ private boolean mCanSystemBarsBeShownByUser = true;
+ private int mLockTaskState = LOCK_TASK_MODE_NONE;
+ private boolean mNavBarEmpty;
+
+ private ContentObserver mContentObserver;
+
+ @Inject
+ public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue,
+ SecureSettings secureSettings) {
+ mSysUiContext = context;
+ final Display display = mSysUiContext.getDisplay();
+ mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY
+ ? mSysUiContext : mSysUiContext.createDisplayContext(display);
+ mCommandQueue = commandQueue;
+ mSecureSettings = secureSettings;
+ }
+
+ boolean loadSetting(int currentUserId) {
+ final boolean wasConfirmed = sConfirmed;
+ sConfirmed = false;
+ if (DEBUG) Log.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
+ String value = null;
+ try {
+ value = mSecureSettings.getStringForUser(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
+ UserHandle.USER_CURRENT);
+ sConfirmed = CONFIRMED.equals(value);
+ if (DEBUG) Log.d(TAG, "Loaded sConfirmed=" + sConfirmed);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error loading confirmations, value=" + value, t);
+ }
+ return sConfirmed != wasConfirmed;
+ }
+
+ private static void saveSetting(Context context) {
+ if (DEBUG) Log.d(TAG, "saveSetting()");
+ try {
+ final String value = sConfirmed ? CONFIRMED : null;
+ Settings.Secure.putStringForUser(context.getContentResolver(),
+ Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
+ value,
+ UserHandle.USER_CURRENT);
+ if (DEBUG) Log.d(TAG, "Saved value=" + value);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ if (displayId != mSysUiContext.getDisplayId()) {
+ return;
+ }
+ mHandler.removeMessages(H.SHOW);
+ mHandler.removeMessages(H.HIDE);
+ IVrManager vrManager = IVrManager.Stub.asInterface(
+ ServiceManager.getService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ vrManager.unregisterListener(mVrStateCallbacks);
+ } catch (RemoteException ex) {
+ }
+ }
+ mCommandQueue.removeCallback(this);
+ }
+
+ private void onSettingChanged(int currentUserId) {
+ final boolean changed = loadSetting(currentUserId);
+ // Remove the window if the setting changes to be confirmed.
+ if (changed && sConfirmed) {
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ mHandler.removeMessages(H.SHOW);
+ if (isImmersiveMode) {
+ if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
+ boolean userSetupComplete = (mSecureSettings.getIntForUser(
+ Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0);
+
+ if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed)
+ && userSetupComplete
+ && !mVrModeEnabled
+ && mCanSystemBarsBeShownByUser
+ && !mNavBarEmpty
+ && !UserManager.isDeviceInDemoMode(mDisplayContext)
+ && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+ final Message msg = mHandler.obtainMessage(
+ H.SHOW);
+ msg.arg1 = rootDisplayAreaId;
+ mHandler.sendMessageDelayed(msg, mShowDelayMs);
+ }
+ } else {
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+
+ @Override
+ public void disable(int displayId, int disableFlag, int disableFlag2, boolean animate) {
+ if (mSysUiContext.getDisplayId() != displayId) {
+ return;
+ }
+ final int disableNavigationBar = (DISABLE_HOME | DISABLE_BACK | DISABLE_RECENT);
+ mNavBarEmpty = (disableFlag & disableNavigationBar) == disableNavigationBar;
+ }
+
+ @Override
+ public void confirmImmersivePrompt() {
+ if (mClingWindow != null) {
+ if (DEBUG) Log.d(TAG, "confirmImmersivePrompt()");
+ mHandler.post(mConfirm);
+ }
+ }
+
+ private void handleHide() {
+ if (mClingWindow != null) {
+ if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation");
+ if (mWindowManager != null) {
+ try {
+ mWindowManager.removeView(mClingWindow);
+ } catch (WindowManager.InvalidDisplayException e) {
+ Log.w(TAG, "Fail to hide the immersive confirmation window because of "
+ + e);
+ }
+ mWindowManager = null;
+ mWindowContext = null;
+ }
+ mClingWindow = null;
+ }
+ }
+
+ private WindowManager.LayoutParams getClingWindowLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+ PixelFormat.TRANSLUCENT);
+ lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
+ // Trusted overlay so touches outside the touchable area are allowed to pass through
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
+ lp.setTitle("ImmersiveModeConfirmation");
+ lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
+ lp.token = getWindowToken();
+ return lp;
+ }
+
+ private FrameLayout.LayoutParams getBubbleLayoutParams() {
+ return new FrameLayout.LayoutParams(
+ mSysUiContext.getResources().getDimensionPixelSize(
+ R.dimen.immersive_mode_cling_width),
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER_HORIZONTAL | Gravity.TOP);
+ }
+
+ /**
+ * @return the window token that's used by all ImmersiveModeConfirmation windows.
+ */
+ IBinder getWindowToken() {
+ return mWindowToken;
+ }
+
+ @Override
+ public void start() {
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ mCommandQueue.addCallback(this);
+
+ final Resources r = mSysUiContext.getResources();
+ mShowDelayMs = r.getInteger(R.integer.dock_enter_exit_duration) * 3L;
+ mCanSystemBarsBeShownByUser = !r.getBoolean(
+ R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
+ R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
+ IVrManager vrManager = IVrManager.Stub.asInterface(
+ ServiceManager.getService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ mVrModeEnabled = vrManager.getVrModeState();
+ vrManager.registerListener(mVrStateCallbacks);
+ mVrStateCallbacks.onVrStateChanged(mVrModeEnabled);
+ } catch (RemoteException e) {
+ // Ignore, we cannot do anything if we failed to access vr manager.
+ }
+ }
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onSettingChanged(mSysUiContext.getUserId());
+ }
+ };
+
+ // Register to listen for changes in Settings.Secure settings.
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, mContentObserver,
+ UserHandle.USER_CURRENT);
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.USER_SETUP_COMPLETE, mContentObserver,
+ UserHandle.USER_CURRENT);
+ }
+ }
+
+ private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(boolean enabled) {
+ mVrModeEnabled = enabled;
+ if (mVrModeEnabled) {
+ mHandler.removeMessages(H.SHOW);
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+ };
+
+ private class ClingWindowView extends FrameLayout {
+ private static final int BGCOLOR = 0x80000000;
+ private static final int OFFSET_DP = 96;
+ private static final int ANIMATION_DURATION = 250;
+
+ private final Runnable mConfirm;
+ private final ColorDrawable mColor = new ColorDrawable(0);
+ private final Interpolator mInterpolator;
+ private ValueAnimator mColorAnim;
+ private ViewGroup mClingLayout;
+
+ private Runnable mUpdateLayoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mClingLayout != null && mClingLayout.getParent() != null) {
+ mClingLayout.setLayoutParams(getBubbleLayoutParams());
+ }
+ }
+ };
+
+ private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
+ new ViewTreeObserver.OnComputeInternalInsetsListener() {
+ private final int[] mTmpInt2 = new int[2];
+
+ @Override
+ public void onComputeInternalInsets(
+ ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ // Set touchable region to cover the cling layout.
+ mClingLayout.getLocationInWindow(mTmpInt2);
+ inoutInfo.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ inoutInfo.touchableRegion.set(
+ mTmpInt2[0],
+ mTmpInt2[1],
+ mTmpInt2[0] + mClingLayout.getWidth(),
+ mTmpInt2[1] + mClingLayout.getHeight());
+ }
+ };
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ post(mUpdateLayoutRunnable);
+ }
+ }
+ };
+
+ ClingWindowView(Context context, Runnable confirm) {
+ super(context);
+ mConfirm = confirm;
+ setBackground(mColor);
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mInterpolator = AnimationUtils
+ .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ mContext.getDisplay().getMetrics(metrics);
+ float density = metrics.density;
+
+ getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
+
+ // create the confirmation cling
+ mClingLayout = (ViewGroup)
+ View.inflate(mSysUiContext, R.layout.immersive_mode_cling, null);
+
+ TypedArray ta = mDisplayContext.obtainStyledAttributes(
+ new int[]{android.R.attr.colorAccent});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ mClingLayout.setBackgroundColor(colorAccent);
+ ImageView expandMore = mClingLayout.findViewById(R.id.immersive_cling_ic_expand_more);
+ if (expandMore != null) {
+ expandMore.setImageTintList(ColorStateList.valueOf(colorAccent));
+ }
+ ImageView lightBgCirc = mClingLayout.findViewById(R.id.immersive_cling_back_bg_light);
+ if (lightBgCirc != null) {
+ // Set transparency to 50%
+ lightBgCirc.setImageAlpha(128);
+ }
+
+ final Button ok = mClingLayout.findViewById(R.id.ok);
+ ok.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mConfirm.run();
+ }
+ });
+ addView(mClingLayout, getBubbleLayoutParams());
+
+ if (ActivityManager.isHighEndGfx()) {
+ final View cling = mClingLayout;
+ cling.setAlpha(0f);
+ cling.setTranslationY(-OFFSET_DP * density);
+
+ postOnAnimation(new Runnable() {
+ @Override
+ public void run() {
+ cling.animate()
+ .alpha(1f)
+ .translationY(0)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(mInterpolator)
+ .withLayer()
+ .start();
+
+ mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
+ mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final int c = (Integer) animation.getAnimatedValue();
+ mColor.setColor(c);
+ }
+ });
+ mColorAnim.setDuration(ANIMATION_DURATION);
+ mColorAnim.setInterpolator(mInterpolator);
+ mColorAnim.start();
+ }
+ });
+ } else {
+ mColor.setColor(BGCOLOR);
+ }
+
+ mContext.registerReceiver(mReceiver,
+ new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent motion) {
+ return true;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ // we will be hiding the nav bar, so layout as if it's already hidden
+ return new WindowInsets.Builder(insets).setInsets(
+ Type.systemBars(), Insets.NONE).build();
+ }
+ }
+
+ /**
+ * To get window manager for the display.
+ *
+ * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the
+ * confirmation window.
+ */
+ @NonNull
+ private WindowManager createWindowManager(int rootDisplayAreaId) {
+ if (mWindowManager != null) {
+ throw new IllegalStateException(
+ "Must not create a new WindowManager while there is an existing one");
+ }
+ // Create window context to specify the RootDisplayArea
+ final Bundle options = getOptionsForWindowContext(rootDisplayAreaId);
+ mWindowContextRootDisplayAreaId = rootDisplayAreaId;
+ mWindowContext = mDisplayContext.createWindowContext(
+ IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
+ mWindowManager = mWindowContext.getSystemService(WindowManager.class);
+ return mWindowManager;
+ }
+
+ /**
+ * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window.
+ * {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}.
+ */
+ @Nullable
+ private Bundle getOptionsForWindowContext(int rootDisplayAreaId) {
+ // In case we don't care which root display area the window manager is specifying.
+ if (rootDisplayAreaId == FEATURE_UNDEFINED) {
+ return null;
+ }
+
+ final Bundle options = new Bundle();
+ options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
+ return options;
+ }
+
+ private void handleShow(int rootDisplayAreaId) {
+ if (mClingWindow != null) {
+ if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) {
+ if (DEBUG) Log.d(TAG, "Immersive mode confirmation has already been shown");
+ return;
+ } else {
+ // Hide the existing confirmation before show a new one in the new root.
+ if (DEBUG) Log.d(TAG, "Immersive mode confirmation was shown in a different root");
+ handleHide();
+ }
+ }
+ if (DEBUG) Log.d(TAG, "Showing immersive mode confirmation");
+ mClingWindow = new ClingWindowView(mDisplayContext, mConfirm);
+ // show the confirmation
+ final WindowManager.LayoutParams lp = getClingWindowLayoutParams();
+ try {
+ createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp);
+ } catch (WindowManager.InvalidDisplayException e) {
+ Log.w(TAG, "Fail to show the immersive confirmation window because of " + e);
+ }
+ }
+
+ private final Runnable mConfirm = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.d(TAG, "mConfirm.run()");
+ if (!sConfirmed) {
+ sConfirmed = true;
+ saveSetting(mDisplayContext);
+ }
+ handleHide();
+ }
+ };
+
+ private final class H extends Handler {
+ private static final int SHOW = 1;
+ private static final int HIDE = 2;
+
+ H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ return;
+ }
+ switch(msg.what) {
+ case SHOW:
+ handleShow(msg.arg1);
+ break;
+ case HIDE:
+ handleHide();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onLockTaskModeChanged(int lockTaskState) {
+ mLockTaskState = lockTaskState;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 73d844541259..feb02586a820 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1035,7 +1035,13 @@ public class KeyguardIndicationController {
return; // udfps affordance is highlighted, no need to show action to unlock
} else if (mKeyguardUpdateMonitor.isFaceEnrolled()
&& !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) {
- String message = mContext.getString(R.string.keyguard_retry);
+ String message;
+ if (mAccessibilityManager.isEnabled()
+ || mAccessibilityManager.isTouchExplorationEnabled()) {
+ message = mContext.getString(R.string.accesssibility_keyguard_retry);
+ } else {
+ message = mContext.getString(R.string.keyguard_retry);
+ }
mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState);
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index 324e97294f4e..645595c1f7bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -23,6 +23,7 @@ import android.os.Process;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.view.View;
import androidx.annotation.VisibleForTesting;
@@ -151,4 +152,20 @@ public class VibratorHelper {
BIOMETRIC_ERROR_VIBRATION_EFFECT, reason,
HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
}
+
+ /**
+ * Perform a vibration using a view and the one-way API with flags
+ * @see View#performHapticFeedback(int feedbackConstant, int flags)
+ */
+ public void performHapticFeedback(@NonNull View view, int feedbackConstant, int flags) {
+ view.performHapticFeedback(feedbackConstant, flags);
+ }
+
+ /**
+ * Perform a vibration using a view and the one-way API
+ * @see View#performHapticFeedback(int feedbackConstant)
+ */
+ public void performHapticFeedback(@NonNull View view, int feedbackConstant) {
+ view.performHapticFeedback(feedbackConstant);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 56390002490c..6e8b8bdebbe3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -265,6 +265,11 @@ class SystemEventChipAnimationController @Inject constructor(
// not animating then [prepareChipAnimation] will take care of it for us
currentAnimatedView?.let {
updateChipBounds(it, newContentArea)
+ // Since updateCurrentAnimatedView can only be called during an animation, we
+ // have to create a dummy animator here to apply the new chip bounds
+ val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
+ animator.addUpdateListener { updateCurrentAnimatedView() }
+ animator.start()
}
}
})
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 62a0d138fd05..5c2f9a8d28ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -39,7 +39,10 @@ class StackCoordinator @Inject internal constructor(
override fun attach(pipeline: NotifPipeline) {
pipeline.addOnAfterRenderListListener(::onAfterRenderList)
- groupExpansionManagerImpl.attach(pipeline)
+ // TODO(b/282865576): This has an issue where it makes changes to some groups without
+ // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the
+ // group expansion bug.
+ // groupExpansionManagerImpl.attach(pipeline)
}
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 4568c0ca1458..46af03a438f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -21,6 +21,8 @@ import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -44,14 +46,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
private final GroupMembershipManager mGroupMembershipManager;
private final Set<OnGroupExpansionChangeListener> mOnGroupChangeListeners = new HashSet<>();
- // Set of summary keys whose groups are expanded
+ /**
+ * Set of summary keys whose groups are expanded.
+ * NOTE: This should not be modified without notifying listeners, so prefer using
+ * {@code setGroupExpanded} when making changes.
+ */
private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
+ private final FeatureFlags mFeatureFlags;
+
@Inject
public GroupExpansionManagerImpl(DumpManager dumpManager,
- GroupMembershipManager groupMembershipManager) {
+ GroupMembershipManager groupMembershipManager, FeatureFlags featureFlags) {
mDumpManager = dumpManager;
mGroupMembershipManager = groupMembershipManager;
+ mFeatureFlags = featureFlags;
}
/**
@@ -85,13 +94,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
@Override
public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
+ boolean changed;
if (expanded) {
- mExpandedGroups.add(groupSummary);
+ changed = mExpandedGroups.add(groupSummary);
} else {
- mExpandedGroups.remove(groupSummary);
+ changed = mExpandedGroups.remove(groupSummary);
}
- sendOnGroupExpandedChange(entry, expanded);
+ // Only notify listeners if something changed.
+ if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE) || changed) {
+ sendOnGroupExpandedChange(entry, expanded);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 42b99a1dc68c..ed489a6c5343 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -3678,6 +3678,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
pw.print(", mShowingPublicInitialized: " + mShowingPublicInitialized);
NotificationContentView showingLayout = getShowingLayout();
pw.print(", privateShowing: " + (showingLayout == mPrivateLayout));
+ pw.print(", mShowNoBackground: " + mShowNoBackground);
pw.println();
showingLayout.dump(pw, args);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a4e8c2ece894..80f5d1939ac0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -21,12 +21,16 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
@@ -71,6 +75,10 @@ import javax.inject.Named;
@NotificationRowScope
public class ExpandableNotificationRowController implements NotifViewController {
private static final String TAG = "NotifRowController";
+
+ static final Uri BUBBLES_SETTING_URI =
+ Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES);
+ private static final String BUBBLES_SETTING_ENABLED_VALUE = "1";
private final ExpandableNotificationRow mView;
private final NotificationListContainer mListContainer;
private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory;
@@ -104,6 +112,23 @@ public class ExpandableNotificationRowController implements NotifViewController
private final ExpandableNotificationRowDragController mDragController;
private final NotificationDismissibilityProvider mDismissibilityProvider;
private final IStatusBarService mStatusBarService;
+
+ private final NotificationSettingsController mSettingsController;
+
+ @VisibleForTesting
+ final NotificationSettingsController.Listener mSettingsListener =
+ new NotificationSettingsController.Listener() {
+ @Override
+ public void onSettingChanged(Uri setting, int userId, String value) {
+ if (BUBBLES_SETTING_URI.equals(setting)) {
+ final int viewUserId = mView.getEntry().getSbn().getUserId();
+ if (viewUserId == UserHandle.USER_ALL || viewUserId == userId) {
+ mView.getPrivateLayout().setBubblesEnabledForUser(
+ BUBBLES_SETTING_ENABLED_VALUE.equals(value));
+ }
+ }
+ }
+ };
private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback =
new ExpandableNotificationRow.ExpandableNotificationRowLogger() {
@Override
@@ -201,6 +226,7 @@ public class ExpandableNotificationRowController implements NotifViewController
FeatureFlags featureFlags,
PeopleNotificationIdentifier peopleNotificationIdentifier,
Optional<BubblesManager> bubblesManagerOptional,
+ NotificationSettingsController settingsController,
ExpandableNotificationRowDragController dragController,
NotificationDismissibilityProvider dismissibilityProvider,
IStatusBarService statusBarService) {
@@ -229,6 +255,7 @@ public class ExpandableNotificationRowController implements NotifViewController
mFeatureFlags = featureFlags;
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
mBubblesManagerOptional = bubblesManagerOptional;
+ mSettingsController = settingsController;
mDragController = dragController;
mMetricsLogger = metricsLogger;
mChildrenContainerLogger = childrenContainerLogger;
@@ -298,12 +325,14 @@ public class ExpandableNotificationRowController implements NotifViewController
NotificationMenuRowPlugin.class, false /* Allow multiple */);
mView.setOnKeyguard(mStatusBarStateController.getState() == KEYGUARD);
mStatusBarStateController.addCallback(mStatusBarStateListener);
+ mSettingsController.addCallback(BUBBLES_SETTING_URI, mSettingsListener);
}
@Override
public void onViewDetachedFromWindow(View v) {
mPluginManager.removePluginListener(mView);
mStatusBarStateController.removeCallback(mStatusBarStateListener);
+ mSettingsController.removeCallback(BUBBLES_SETTING_URI, mSettingsListener);
}
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 20f4429f294b..7b6802f95cda 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -44,6 +44,7 @@ import android.widget.LinearLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SmartReplyController;
@@ -65,7 +66,6 @@ import com.android.systemui.statusbar.policy.SmartReplyStateInflaterKt;
import com.android.systemui.statusbar.policy.SmartReplyView;
import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
import com.android.systemui.util.Compile;
-import com.android.systemui.wmshell.BubblesManager;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -134,6 +134,7 @@ public class NotificationContentView extends FrameLayout implements Notification
private PeopleNotificationIdentifier mPeopleIdentifier;
private RemoteInputViewSubcomponent.Factory mRemoteInputSubcomponentFactory;
private IStatusBarService mStatusBarService;
+ private boolean mBubblesEnabledForUser;
/**
* List of listeners for when content views become inactive (i.e. not the showing view).
@@ -1440,12 +1441,17 @@ public class NotificationContentView extends FrameLayout implements Notification
}
}
+ @Background
+ public void setBubblesEnabledForUser(boolean enabled) {
+ mBubblesEnabledForUser = enabled;
+ }
+
@VisibleForTesting
boolean shouldShowBubbleButton(NotificationEntry entry) {
boolean isPersonWithShortcut =
mPeopleIdentifier.getPeopleNotificationType(entry)
>= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
- return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+ return mBubblesEnabledForUser
&& isPersonWithShortcut
&& entry.getBubbleMetadata() != null;
}
@@ -2079,6 +2085,7 @@ public class NotificationContentView extends FrameLayout implements Notification
pw.print("null");
}
pw.println();
+ pw.println("mBubblesEnabledForUser: " + mBubblesEnabledForUser);
pw.print("RemoteInputViews { ");
pw.print(" visibleType: " + mVisibleType);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java
new file mode 100644
index 000000000000..585ff523b9a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import javax.inject.Inject;
+
+/**
+ * Centralized controller for listening to Secure Settings changes and informing in-process
+ * listeners, on a background thread.
+ */
+@SysUISingleton
+public class NotificationSettingsController implements Dumpable {
+
+ private final static String TAG = "NotificationSettingsController";
+ private final UserTracker mUserTracker;
+ private final UserTracker.Callback mCurrentUserTrackerCallback;
+ private final Handler mHandler;
+ private final ContentObserver mContentObserver;
+ private final SecureSettings mSecureSettings;
+ private final HashMap<Uri, ArrayList<Listener>> mListeners = new HashMap<>();
+
+ @Inject
+ public NotificationSettingsController(UserTracker userTracker,
+ @Background Handler handler,
+ SecureSettings secureSettings,
+ DumpManager dumpManager) {
+ mUserTracker = userTracker;
+ mHandler = handler;
+ mSecureSettings = secureSettings;
+ mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ synchronized (mListeners) {
+ if (mListeners.containsKey(uri)) {
+ for (Listener listener : mListeners.get(uri)) {
+ notifyListener(listener, uri);
+ }
+ }
+ }
+ }
+ };
+
+ mCurrentUserTrackerCallback = new UserTracker.Callback() {
+ @Override
+ public void onUserChanged(int newUser, Context userContext) {
+ synchronized (mListeners) {
+ if (mListeners.size() > 0) {
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ for (Uri uri : mListeners.keySet()) {
+ mSecureSettings.registerContentObserverForUser(
+ uri, false, mContentObserver, newUser);
+ }
+ }
+ }
+ }
+ };
+ mUserTracker.addCallback(mCurrentUserTrackerCallback, new HandlerExecutor(handler));
+
+ dumpManager.registerNormalDumpable(TAG, this);
+ }
+
+ /**
+ * Register callback whenever the given secure settings changes.
+ *
+ * On registration, will call back on the provided handler with the current value of
+ * the setting.
+ */
+ public void addCallback(@NonNull Uri uri, @NonNull Listener listener) {
+ if (uri == null || listener == null) {
+ return;
+ }
+ synchronized (mListeners) {
+ ArrayList<Listener> currentListeners = mListeners.get(uri);
+ if (currentListeners == null) {
+ currentListeners = new ArrayList<>();
+ }
+ if (!currentListeners.contains(listener)) {
+ currentListeners.add(listener);
+ }
+ mListeners.put(uri, currentListeners);
+ if (currentListeners.size() == 1) {
+ mSecureSettings.registerContentObserverForUser(
+ uri, false, mContentObserver, mUserTracker.getUserId());
+ }
+ }
+ mHandler.post(() -> notifyListener(listener, uri));
+
+ }
+
+ public void removeCallback(Uri uri, Listener listener) {
+ synchronized (mListeners) {
+ ArrayList<Listener> currentListeners = mListeners.get(uri);
+
+ if (currentListeners != null) {
+ currentListeners.remove(listener);
+ }
+ if (currentListeners == null || currentListeners.size() == 0) {
+ mListeners.remove(uri);
+ }
+
+ if (mListeners.size() == 0) {
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ }
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ synchronized (mListeners) {
+ pw.println("Settings Uri Listener List:");
+ for (Uri uri : mListeners.keySet()) {
+ pw.println(" Uri=" + uri);
+ for (Listener listener : mListeners.get(uri)) {
+ pw.println(" Listener=" + listener.getClass().getName());
+ }
+ }
+ }
+ }
+
+ private void notifyListener(Listener listener, Uri uri) {
+ final String setting = uri == null ? null : uri.getLastPathSegment();
+ int userId = mUserTracker.getUserId();
+ listener.onSettingChanged(uri, userId, mSecureSettings.getStringForUser(setting, userId));
+ }
+
+ /**
+ * Listener invoked whenever settings are changed.
+ */
+ public interface Listener {
+ void onSettingChanged(@NonNull Uri setting, int userId, @Nullable String value);
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 26b51a95acad..dcd18dd7d1bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -45,6 +45,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -69,6 +70,7 @@ constructor(
private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
private val shadeControllerLazy: Lazy<ShadeController>,
+ private val shadeViewControllerLazy: Lazy<ShadeViewController>,
private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
private val activityLaunchAnimator: ActivityLaunchAnimator,
@@ -896,7 +898,7 @@ constructor(
if (dismissShade) {
return StatusBarLaunchAnimatorController(
animationController,
- it.shadeViewController,
+ shadeViewControllerLazy.get(),
shadeControllerLazy.get(),
notifShadeWindowControllerLazy.get(),
isLaunchForActivity
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 1f9c9f2b8c14..58d6bb024476 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME;
import android.annotation.IntDef;
@@ -30,6 +31,7 @@ import android.metrics.LogMaker;
import android.os.Handler;
import android.os.PowerManager;
import android.os.Trace;
+import android.view.HapticFeedbackConstants;
import androidx.annotation.Nullable;
@@ -51,6 +53,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -160,7 +163,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
private KeyguardViewController mKeyguardViewController;
private DozeScrimController mDozeScrimController;
private KeyguardViewMediator mKeyguardViewMediator;
- private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private PendingAuthenticated mPendingAuthenticated = null;
private boolean mHasScreenTurnedOnSinceAuthenticating;
private boolean mFadedAwayAfterWakeAndUnlock;
@@ -178,6 +180,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
private long mLastFpFailureUptimeMillis;
private int mNumConsecutiveFpFailures;
+ private final FeatureFlags mFeatureFlags;
+
private static final class PendingAuthenticated {
public final int userId;
public final BiometricSourceType biometricSourceType;
@@ -282,7 +286,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
ScreenOffAnimationController screenOffAnimationController,
VibratorHelper vibrator,
SystemClock systemClock,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager
+ FeatureFlags featureFlags
) {
mPowerManager = powerManager;
mUpdateMonitor = keyguardUpdateMonitor;
@@ -310,7 +314,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
mVibratorHelper = vibrator;
mLogger = biometricUnlockLogger;
mSystemClock = systemClock;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ mFeatureFlags = featureFlags;
dumpManager.registerDumpable(getClass().getName(), this);
}
@@ -452,19 +456,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
// During wake and unlock, we need to draw black before waking up to avoid abrupt
// brightness changes due to display state transitions.
Runnable wakeUp = ()-> {
- // Check to see if we are still locked when we are waking and unlocking from dream.
- // This runnable should be executed after unlock. If that's true, we could be not
- // dreaming, but still locked. In this case, we should attempt to authenticate instead
- // of waking up.
- if (mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
- && !mKeyguardStateController.isUnlocked()
- && !mUpdateMonitor.isDreaming()) {
- // Post wakeUp runnable is called from a callback in keyguard.
- mHandler.post(() -> mKeyguardViewController.notifyKeyguardAuthenticated(
- false /* primaryAuth */));
- } else if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
+ if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
mLogger.i("bio wakelock: Authenticated, waking up...");
-
mPowerManager.wakeUp(
mSystemClock.uptimeMillis(),
PowerManager.WAKE_REASON_BIOMETRIC,
@@ -476,7 +469,10 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
Trace.endSection();
};
- if (mMode != MODE_NONE && mMode != MODE_WAKE_AND_UNLOCK_FROM_DREAM) {
+ final boolean wakingFromDream = mMode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
+ && !mStatusBarStateController.isDozing();
+
+ if (mMode != MODE_NONE && !wakingFromDream) {
wakeUp.run();
}
switch (mMode) {
@@ -498,10 +494,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
Trace.endSection();
break;
case MODE_WAKE_AND_UNLOCK_FROM_DREAM:
- // In the case of waking and unlocking from dream, waking up is delayed until after
- // unlock is complete to avoid conflicts during each sequence's transitions.
- mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(wakeUp);
- // Execution falls through here to proceed unlocking.
case MODE_WAKE_AND_UNLOCK_PULSING:
case MODE_WAKE_AND_UNLOCK:
if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) {
@@ -765,8 +757,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
mLogger.d("Skip auth success haptic. Power button was recently pressed.");
return;
}
- mVibratorHelper.vibrateAuthSuccess(
- getClass().getSimpleName() + ", type =" + type + "device-entry::success");
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mKeyguardViewController.getViewRootImpl().getView(),
+ HapticFeedbackConstants.CONFIRM
+ );
+ } else {
+ mVibratorHelper.vibrateAuthSuccess(
+ getClass().getSimpleName() + ", type =" + type + "device-entry::success");
+ }
}
private boolean lastWakeupFromPowerButtonWithinHapticThreshold() {
@@ -779,8 +778,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
}
private void vibrateError(BiometricSourceType type) {
- mVibratorHelper.vibrateAuthError(
- getClass().getSimpleName() + ", type =" + type + "device-entry::error");
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mKeyguardViewController.getViewRootImpl().getView(),
+ HapticFeedbackConstants.REJECT
+ );
+ } else {
+ mVibratorHelper.vibrateAuthError(
+ getClass().getSimpleName() + ", type =" + type + "device-entry::error");
+ }
}
private void cleanup() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index acd6e49fe8c1..5c28be3bc678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -44,7 +44,6 @@ import com.android.systemui.display.data.repository.DisplayMetricsRepository;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.qs.QSPanelController;
-import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -195,9 +194,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner {
@Override
Lifecycle getLifecycle();
- /** */
- ShadeViewController getShadeViewController();
-
/** Get the Keyguard Message Area that displays auth messages. */
AuthKeyguardMessageArea getKeyguardMessageArea();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index c3c9a61df2ea..6eeb25fdeb9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -1326,7 +1326,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
});
- mScreenOffAnimationController.initialize(this, mLightRevealScrim);
+ mScreenOffAnimationController.initialize(this, mShadeSurface, mLightRevealScrim);
updateLightRevealScrimVisibility();
mShadeSurface.initDependencies(
@@ -1677,8 +1677,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
Trace.endSection();
}
- @Override
- public ShadeViewController getShadeViewController() {
+ protected ShadeViewController getShadeViewController() {
return mShadeSurface;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 862f169b2176..4e136deab5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -28,6 +28,9 @@ import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -43,6 +46,7 @@ import com.android.systemui.util.view.ViewUtil
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
private const val TAG = "PhoneStatusBarViewController"
@@ -53,10 +57,12 @@ class PhoneStatusBarViewController private constructor(
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
private val shadeViewController: ShadeViewController,
+ private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
private val userChipViewModel: StatusBarUserChipViewModel,
private val viewUtil: ViewUtil,
+ private val featureFlags: FeatureFlags,
private val configurationController: ConfigurationController
) : ViewController<PhoneStatusBarView>(view) {
@@ -164,6 +170,16 @@ class PhoneStatusBarViewController private constructor(
return false
}
+ // If scene framework is enabled, route the touch to it and
+ // ignore the rest of the gesture.
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ sceneInteractor.get()
+ .onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
+ // TODO(b/291965119): remove once view is expanded to cover the status bar
+ sceneInteractor.get().setVisible(SceneContainerNames.SYSTEM_UI_DEFAULT, true)
+ return false
+ }
+
if (event.action == MotionEvent.ACTION_DOWN) {
// If the view that would receive the touch is disabled, just have status
// bar eat the gesture.
@@ -225,6 +241,7 @@ class PhoneStatusBarViewController private constructor(
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
private val shadeViewController: ShadeViewController,
+ private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController,
@@ -245,10 +262,12 @@ class PhoneStatusBarViewController private constructor(
centralSurfaces,
shadeController,
shadeViewController,
+ sceneInteractor,
shadeLogger,
statusBarMoveFromCenterAnimationController,
userChipViewModel,
viewUtil,
+ featureFlags,
configurationController
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
index c8174669cc65..89c3a02f9401 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.unfold.FoldAodAnimationController
import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -37,8 +38,12 @@ class ScreenOffAnimationController @Inject constructor(
private val animations: List<ScreenOffAnimation> =
listOfNotNull(foldToAodAnimation, unlockedScreenOffAnimation)
- fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
- animations.forEach { it.initialize(centralSurfaces, lightRevealScrim) }
+ fun initialize(
+ centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
+ lightRevealScrim: LightRevealScrim,
+ ) {
+ animations.forEach { it.initialize(centralSurfaces, shadeViewController, lightRevealScrim) }
wakefulnessLifecycle.addObserver(this)
}
@@ -197,7 +202,11 @@ class ScreenOffAnimationController @Inject constructor(
}
interface ScreenOffAnimation {
- fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {}
+ fun initialize(
+ centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
+ lightRevealScrim: LightRevealScrim,
+ ) {}
/**
* Called when started going to sleep, should return true if the animation will be played
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 2a039dade059..68a6b3d62bae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -34,15 +34,21 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.ScreenDecorations;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
import java.io.PrintWriter;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* Manages what parts of the status bar are touchable. Clients are primarily UI that display in the
@@ -78,6 +84,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
ConfigurationController configurationController,
HeadsUpManagerPhone headsUpManager,
ShadeExpansionStateManager shadeExpansionStateManager,
+ Provider<SceneInteractor> sceneInteractor,
+ Provider<JavaAdapter> javaAdapter,
+ FeatureFlags featureFlags,
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
) {
mContext = context;
@@ -115,6 +124,12 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ javaAdapter.get().alwaysCollectFlow(
+ sceneInteractor.get().isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT),
+ this::onShadeExpansionFullyChanged);
+ }
+
mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 96a4d900c160..e8da9519c7ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -20,6 +20,7 @@ import com.android.app.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.NotificationShadeWindowController
@@ -66,7 +67,8 @@ class UnlockedScreenOffAnimationController @Inject constructor(
private val powerManager: PowerManager,
private val handler: Handler = Handler()
) : WakefulnessLifecycle.Observer, ScreenOffAnimation {
- private lateinit var mCentralSurfaces: CentralSurfaces
+ private lateinit var centralSurfaces: CentralSurfaces
+ private lateinit var shadeViewController: ShadeViewController
/**
* Whether or not [initialize] has been called to provide us with the StatusBar,
* NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen
@@ -126,7 +128,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
lightRevealAnimator.start()
}
- val animatorDurationScaleObserver = object : ContentObserver(null) {
+ private val animatorDurationScaleObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
updateAnimatorDurationScale()
}
@@ -134,11 +136,13 @@ class UnlockedScreenOffAnimationController @Inject constructor(
override fun initialize(
centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
lightRevealScrim: LightRevealScrim
) {
this.initialized = true
this.lightRevealScrim = lightRevealScrim
- this.mCentralSurfaces = centralSurfaces
+ this.centralSurfaces = centralSurfaces
+ this.shadeViewController = shadeViewController
updateAnimatorDurationScale()
globalSettings.registerContentObserver(
@@ -184,7 +188,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Cancel any existing CUJs before starting the animation
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
-
+ PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
PropertyAnimator.setProperty(
keyguardView, AnimatableProperty.ALPHA, 1f,
AnimationProperties()
@@ -198,7 +202,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Tell the CentralSurfaces to become keyguard for real - we waited on that
// since it is slow and would have caused the animation to jank.
- mCentralSurfaces.updateIsKeyguard()
+ centralSurfaces.updateIsKeyguard()
// Run the callback given to us by the KeyguardVisibilityHelper.
after.run()
@@ -251,7 +255,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
// changed parts of the UI (such as showing AOD in the shade) without actually changing
// the StatusBarState. This ensures that the UI definitely reflects the desired state.
- mCentralSurfaces.updateIsKeyguard(true /* forceStateChange */)
+ centralSurfaces.updateIsKeyguard(true /* forceStateChange */)
}
}
@@ -280,7 +284,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Show AOD. That'll cause the KeyguardVisibilityHelper to call
// #animateInKeyguard.
- mCentralSurfaces.shadeViewController.showAodUi()
+ shadeViewController.showAodUi()
}
}, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
@@ -328,8 +332,8 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
// already expanded and showing notifications/QS, the animation looks really messy. For now,
// disable it if the notification panel is expanded.
- if ((!this::mCentralSurfaces.isInitialized ||
- mCentralSurfaces.shadeViewController.isPanelExpanded) &&
+ if ((!this::centralSurfaces.isInitialized ||
+ shadeViewController.isPanelExpanded) &&
// Status bar might be expanded because we have started
// playing the animation already
!isAnimationPlaying()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 02b7e9176cf2..e00365d8fbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -30,6 +30,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
+import android.os.Trace;
import android.os.UserHandle;
import android.text.Editable;
import android.text.SpannedString;
@@ -1032,10 +1033,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
}
private void hideIme() {
+ Trace.beginSection("RemoteEditText#hideIme");
final WindowInsetsController insetsController = getWindowInsetsController();
if (insetsController != null) {
insetsController.hide(WindowInsets.Type.ime());
}
+ Trace.endSection();
}
private void defocusIfNeeded(boolean animate) {
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index cbe402017c41..098d51e94fc7 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -31,6 +31,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.shade.ShadeFoldAnimator
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.ScreenOffAnimation
@@ -62,7 +63,7 @@ constructor(
private val keyguardInteractor: Lazy<KeyguardInteractor>,
) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer {
- private lateinit var centralSurfaces: CentralSurfaces
+ private lateinit var shadeViewController: ShadeViewController
private var isFolded = false
private var isFoldHandled = true
@@ -87,8 +88,12 @@ constructor(
)
}
- override fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
- this.centralSurfaces = centralSurfaces
+ override fun initialize(
+ centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
+ lightRevealScrim: LightRevealScrim,
+ ) {
+ this.shadeViewController = shadeViewController
deviceStateManager.registerCallback(mainExecutor, FoldListener())
wakefulnessLifecycle.addObserver(this)
@@ -128,7 +133,7 @@ constructor(
}
private fun getShadeFoldAnimator(): ShadeFoldAnimator =
- centralSurfaces.shadeViewController.shadeFoldAnimator
+ shadeViewController.shadeFoldAnimator
private fun setAnimationState(playing: Boolean) {
shouldPlayAnimation = playing
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index d44717420bdf..3abae6bcd197 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -37,6 +37,7 @@ import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserS
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate
import com.android.systemui.biometrics.SideFpsController
import com.android.systemui.biometrics.SideFpsUiRequestSource
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
@@ -123,6 +124,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
@Mock private lateinit var viewMediatorCallback: ViewMediatorCallback
@Mock private lateinit var audioManager: AudioManager
@Mock private lateinit var userInteractor: UserInteractor
+ @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
@Captor
private lateinit var swipeListenerArgumentCaptor:
@@ -224,6 +226,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
mock(),
{ JavaAdapter(sceneTestUtils.testScope.backgroundScope) },
userInteractor,
+ faceAuthAccessibilityDelegate,
) {
sceneInteractor
}
@@ -244,6 +247,11 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
}
@Test
+ fun setAccessibilityDelegate() {
+ verify(view).accessibilityDelegate = eq(faceAuthAccessibilityDelegate)
+ }
+
+ @Test
fun showSecurityScreen_canInflateAllModes() {
val modes = SecurityMode.values()
for (mode in modes) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
index 025c88c36203..576f689a16d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
@@ -39,6 +39,7 @@ import com.android.systemui.recents.Recents;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -65,6 +66,8 @@ public class SystemActionsTest extends SysuiTestCase {
@Mock
private ShadeController mShadeController;
@Mock
+ private ShadeViewController mShadeViewController;
+ @Mock
private Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@Mock
private Optional<Recents> mRecentsOptional;
@@ -82,7 +85,8 @@ public class SystemActionsTest extends SysuiTestCase {
mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
mContext.addMockSystemService(InputManager.class, mInputManager);
mSystemActions = new SystemActions(mContext, mUserTracker, mNotificationShadeController,
- mShadeController, mCentralSurfacesOptionalLazy, mRecentsOptional, mDisplayTracker);
+ mShadeController, () -> mShadeViewController, mCentralSurfacesOptionalLazy,
+ mRecentsOptional, mDisplayTracker);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
index d5e6881500bc..7b99314692b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
@@ -15,11 +15,13 @@
*/
package com.android.systemui.accessibility.fontscaling
+import android.content.res.Configuration
import android.os.Handler
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.ViewGroup
+import android.widget.Button
import android.widget.SeekBar
import androidx.test.filters.SmallTest
import com.android.systemui.R
@@ -61,6 +63,7 @@ class FontScalingDialogTest : SysuiTestCase() {
private lateinit var secureSettings: SecureSettings
private lateinit var systemClock: FakeSystemClock
private lateinit var backgroundDelayableExecutor: FakeExecutor
+ private lateinit var testableLooper: TestableLooper
private val fontSizeValueArray: Array<String> =
mContext
.getResources()
@@ -73,7 +76,8 @@ class FontScalingDialogTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- val mainHandler = Handler(TestableLooper.get(this).getLooper())
+ testableLooper = TestableLooper.get(this)
+ val mainHandler = Handler(testableLooper.looper)
systemSettings = FakeSettings()
// Guarantee that the systemSettings always starts with the default font scale.
systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
@@ -286,4 +290,26 @@ class FontScalingDialogTest : SysuiTestCase() {
verify(fontScalingDialog).createTextPreview(/* index= */ 0)
fontScalingDialog.dismiss()
}
+
+ @Test
+ fun changeFontSize_buttonIsDisabledBeforeFontSizeChangeFinishes() {
+ fontScalingDialog.show()
+
+ val iconEndFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_end_frame)!!
+ val doneButton: Button = fontScalingDialog.findViewById(com.android.internal.R.id.button1)!!
+
+ iconEndFrame.performClick()
+ backgroundDelayableExecutor.runAllReady()
+ backgroundDelayableExecutor.advanceClockToNext()
+ backgroundDelayableExecutor.runAllReady()
+
+ // Verify that the button is disabled before receiving onConfigurationChanged
+ assertThat(doneButton.isEnabled).isFalse()
+
+ val config = Configuration()
+ config.fontScale = 1.15f
+ fontScalingDialog.onConfigurationChanged(config)
+ testableLooper.processAllMessages()
+ assertThat(doneButton.isEnabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
new file mode 100644
index 000000000000..005697044c0f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.authentication.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class AuthenticationRepositoryTest : SysuiTestCase() {
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+ private val testUtils = SceneTestUtils(this)
+ private val testScope = testUtils.testScope
+ private val userRepository = FakeUserRepository()
+
+ private lateinit var underTest: AuthenticationRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ userRepository.setUserInfos(USER_INFOS)
+ runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) }
+
+ underTest =
+ AuthenticationRepositoryImpl(
+ applicationScope = testScope.backgroundScope,
+ getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN },
+ backgroundDispatcher = testUtils.testDispatcher,
+ userRepository = userRepository,
+ keyguardRepository = testUtils.keyguardRepository,
+ lockPatternUtils = lockPatternUtils,
+ )
+ }
+
+ @Test
+ fun isAutoConfirmEnabled() =
+ testScope.runTest {
+ whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true)
+ whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false)
+
+ val values by collectValues(underTest.isAutoConfirmEnabled)
+ assertThat(values.first()).isFalse()
+ assertThat(values.last()).isTrue()
+
+ userRepository.setSelectedUserInfo(USER_INFOS[1])
+ assertThat(values.last()).isFalse()
+ }
+
+ @Test
+ fun isPatternVisible() =
+ testScope.runTest {
+ whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[0].id)).thenReturn(false)
+ whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[1].id)).thenReturn(true)
+
+ val values by collectValues(underTest.isPatternVisible)
+ assertThat(values.first()).isTrue()
+ assertThat(values.last()).isFalse()
+
+ userRepository.setSelectedUserInfo(USER_INFOS[1])
+ assertThat(values.last()).isTrue()
+ }
+
+ companion object {
+ private val USER_INFOS =
+ listOf(
+ UserInfo(
+ /* id= */ 100,
+ /* name= */ "First user",
+ /* flags= */ 0,
+ ),
+ UserInfo(
+ /* id= */ 101,
+ /* name= */ "Second user",
+ /* flags= */ 0,
+ ),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
index 9cabd35cb1e5..5766f1be8894 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.biometrics
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import androidx.test.filters.RequiresDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.shade.ShadeExpansionStateManager
import org.junit.Assert
@@ -30,6 +31,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.junit.MockitoJUnit
+@RequiresDevice
@SmallTest
@RunWith(AndroidTestingRunner::class)
class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt
new file mode 100644
index 000000000000..ec17794d4ee2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.testing.TestableLooper
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.FaceAuthApiRequestReason
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class FaceAuthAccessibilityDelegateTest : SysuiTestCase() {
+
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var hostView: View
+ @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor
+ private lateinit var underTest: FaceAuthAccessibilityDelegate
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest =
+ FaceAuthAccessibilityDelegate(
+ context.resources,
+ keyguardUpdateMonitor,
+ faceAuthInteractor,
+ )
+ }
+
+ @Test
+ fun shouldListenForFaceTrue_onInitializeAccessibilityNodeInfo_clickActionAdded() {
+ whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true)
+
+ // WHEN node is initialized
+ val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+ underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+ // THEN a11y action is added
+ val argumentCaptor = argumentCaptor<AccessibilityNodeInfo.AccessibilityAction>()
+ verify(mockedNodeInfo).addAction(argumentCaptor.capture())
+
+ // AND the a11y action is a click action
+ assertEquals(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+ argumentCaptor.value.id
+ )
+ }
+
+ @Test
+ fun shouldListenForFaceFalse_onInitializeAccessibilityNodeInfo_clickActionNotAdded() {
+ whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false)
+
+ // WHEN node is initialized
+ val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+ underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+ // THEN a11y action is NOT added
+ verify(mockedNodeInfo, never())
+ .addAction(any(AccessibilityNodeInfo.AccessibilityAction::class.java))
+ }
+
+ @Test
+ fun performAccessibilityAction_actionClick_retriesFaceAuth() {
+ whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true)
+
+ // WHEN click action is performed
+ underTest.performAccessibilityAction(
+ hostView,
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+ null
+ )
+
+ // THEN retry face auth
+ verify(keyguardUpdateMonitor)
+ .requestFaceAuth(eq(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION))
+ verify(faceAuthInteractor).onAccessibilityAction()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index ecc776b98c6c..3169b091217b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -44,6 +44,9 @@ import android.view.View
import android.view.ViewPropertyAnimator
import android.view.WindowInsets
import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
import android.view.WindowMetrics
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -53,14 +56,9 @@ import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dump.DumpManager
@@ -101,7 +99,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
@SmallTest
@RoboPilotTest
@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
class SideFpsControllerTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@ class SideFpsControllerTest : SysuiTestCase() {
private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
private lateinit var displayStateInteractor: DisplayStateInteractor
- private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel
- private val fingerprintRepository = FakeFingerprintPropertyRepository()
private val executor = FakeExecutor(FakeSystemClock())
private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
@@ -163,15 +159,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
rearDisplayStateRepository
)
- sideFpsOverlayViewModel =
- SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository))
-
- fingerprintRepository.setProperties(
- sensorId = 1,
- strength = SensorStrength.STRONG,
- sensorType = FingerprintSensorType.REAR,
- sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0))
- )
context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, windowManager)
@@ -278,7 +265,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
handler,
alternateBouncerInteractor,
- { sideFpsOverlayViewModel },
TestCoroutineScope(),
dumpManager
)
@@ -697,6 +683,106 @@ class SideFpsControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any())
}
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForXAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
@Test
fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
// By default all those tests assume the side fps sensor is available.
@@ -709,6 +795,51 @@ class SideFpsControllerTest : SysuiTestCase() {
assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
}
+
+ @Test
+ fun testLayoutParams_isKeyguardDialogType() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpType = overlayViewParamsCaptor.value.type
+
+ assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue()
+ }
+
+ @Test
+ fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+ assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
+ }
+
+ @Test
+ fun testLayoutParams_hasTrustedOverlayWindowFlag() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+ assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
+ }
}
private fun insetsForSmallNavbar() = insetsWithBottom(60)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
new file mode 100644
index 000000000000..fcc40404bf7d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.repository
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+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.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class FacePropertyRepositoryImplTest : SysuiTestCase() {
+ @JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ private lateinit var underTest: FacePropertyRepository
+ private lateinit var testScope: TestScope
+
+ @Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback>
+ @Mock private lateinit var faceManager: FaceManager
+ @Before
+ fun setup() {
+ testScope = TestScope()
+ underTest = createRepository(faceManager)
+ }
+
+ private fun createRepository(manager: FaceManager? = faceManager) =
+ FacePropertyRepositoryImpl(testScope.backgroundScope, manager)
+
+ @Test
+ fun whenFaceManagerIsNotPresentIsNull() =
+ testScope.runTest {
+ underTest = createRepository(null)
+ val sensor = collectLastValue(underTest.sensorInfo)
+
+ assertThat(sensor()).isNull()
+ }
+
+ @Test
+ fun providesTheValuePassedToTheAuthenticatorsRegisteredCallback() {
+ testScope.runTest {
+ val sensor by collectLastValue(underTest.sensorInfo)
+ runCurrent()
+ verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
+
+ callback.value.onAllAuthenticatorsRegistered(
+ listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
+ )
+
+ assertThat(sensor).isEqualTo(FaceSensorInfo(1, SensorStrength.STRONG))
+ }
+ }
+
+ private fun createSensorProperties(id: Int, strength: Int) =
+ FaceSensorPropertiesInternal(id, strength, 0, emptyList(), 1, false, false, false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index ea2561594793..239e317b92f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -73,15 +73,10 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
@Test
fun initializeProperties() =
testScope.runTest {
- val sensorId by collectLastValue(repository.sensorId)
- val strength by collectLastValue(repository.strength)
- val sensorType by collectLastValue(repository.sensorType)
- val sensorLocations by collectLastValue(repository.sensorLocations)
+ val isInitialized = collectLastValue(repository.isInitialized)
- // Assert default properties.
- assertThat(sensorId).isEqualTo(-1)
- assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
- assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
+ assertDefaultProperties()
+ assertThat(isInitialized()).isFalse()
val fingerprintProps =
listOf(
@@ -120,24 +115,31 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
- assertThat(sensorId).isEqualTo(1)
- assertThat(strength).isEqualTo(SensorStrength.STRONG)
- assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
+ assertThat(repository.sensorId.value).isEqualTo(1)
+ assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
+ assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
- assertThat(sensorLocations?.size).isEqualTo(2)
- assertThat(sensorLocations).containsKey("display_id_1")
- with(sensorLocations?.get("display_id_1")!!) {
+ assertThat(repository.sensorLocations.value.size).isEqualTo(2)
+ assertThat(repository.sensorLocations.value).containsKey("display_id_1")
+ with(repository.sensorLocations.value["display_id_1"]!!) {
assertThat(displayId).isEqualTo("display_id_1")
assertThat(sensorLocationX).isEqualTo(100)
assertThat(sensorLocationY).isEqualTo(300)
assertThat(sensorRadius).isEqualTo(20)
}
- assertThat(sensorLocations).containsKey("")
- with(sensorLocations?.get("")!!) {
+ assertThat(repository.sensorLocations.value).containsKey("")
+ with(repository.sensorLocations.value[""]!!) {
assertThat(displayId).isEqualTo("")
assertThat(sensorLocationX).isEqualTo(540)
assertThat(sensorLocationY).isEqualTo(1636)
assertThat(sensorRadius).isEqualTo(130)
}
+ assertThat(isInitialized()).isTrue()
}
+
+ private fun assertDefaultProperties() {
+ assertThat(repository.sensorId.value).isEqualTo(-1)
+ assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE)
+ assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
index 896f9b114679..fd96cf45504b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
@@ -22,7 +22,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -52,9 +51,8 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
}
@Test
- fun testGetOverlayoffsets() =
+ fun testGetOverlayOffsets() =
testScope.runTest {
- // Arrange.
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
)
)
- // Act.
- val offsets by collectLastValue(interactor.overlayOffsets)
- val displayId by collectLastValue(interactor.displayId)
+ var offsets = interactor.getOverlayOffsets("display_id_1")
+ assertThat(offsets.displayId).isEqualTo("display_id_1")
+ assertThat(offsets.sensorLocationX).isEqualTo(100)
+ assertThat(offsets.sensorLocationY).isEqualTo(300)
+ assertThat(offsets.sensorRadius).isEqualTo(20)
- // Assert offsets of empty displayId.
- assertThat(displayId).isEqualTo("")
- assertThat(offsets?.displayId).isEqualTo("")
- assertThat(offsets?.sensorLocationX).isEqualTo(540)
- assertThat(offsets?.sensorLocationY).isEqualTo(1636)
- assertThat(offsets?.sensorRadius).isEqualTo(130)
-
- // Offsets should be updated correctly.
- interactor.changeDisplay("display_id_1")
- assertThat(displayId).isEqualTo("display_id_1")
- assertThat(offsets?.displayId).isEqualTo("display_id_1")
- assertThat(offsets?.sensorLocationX).isEqualTo(100)
- assertThat(offsets?.sensorLocationY).isEqualTo(300)
- assertThat(offsets?.sensorRadius).isEqualTo(20)
-
- // Should return default offset when the displayId is invalid.
- interactor.changeDisplay("invalid_display_id")
- assertThat(displayId).isEqualTo("invalid_display_id")
- assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId)
- assertThat(offsets?.sensorLocationX)
- .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX)
- assertThat(offsets?.sensorLocationY)
- .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY)
- assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius)
+ offsets = interactor.getOverlayOffsets("invalid_display_id")
+ assertThat(offsets.displayId).isEqualTo("")
+ assertThat(offsets.sensorLocationX).isEqualTo(540)
+ assertThat(offsets.sensorLocationY).isEqualTo(1636)
+ assertThat(offsets.sensorRadius).isEqualTo(130)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
deleted file mode 100644
index a8593216e22a..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.ui.viewmodel
-
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.display.DisplayManagerGlobal
-import android.view.Display
-import android.view.DisplayAdjustments
-import android.view.DisplayInfo
-import android.view.Surface
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-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
-import org.mockito.Mockito
-import org.mockito.junit.MockitoJUnit
-
-private const val DISPLAY_ID = 2
-
-@SmallTest
-@RunWith(JUnit4::class)
-class SideFpsOverlayViewModelTest : SysuiTestCase() {
-
- @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private var testScope: TestScope = TestScope(StandardTestDispatcher())
-
- private val fingerprintRepository = FakeFingerprintPropertyRepository()
- private lateinit var interactor: SideFpsOverlayInteractor
- private lateinit var viewModel: SideFpsOverlayViewModel
-
- enum class DeviceConfig {
- X_ALIGNED,
- Y_ALIGNED,
- }
-
- private lateinit var deviceConfig: DeviceConfig
- private lateinit var indicatorBounds: Rect
- private lateinit var displayBounds: Rect
- private lateinit var sensorLocation: SensorLocationInternal
- private var displayWidth: Int = 0
- private var displayHeight: Int = 0
- private var boundsWidth: Int = 0
- private var boundsHeight: Int = 0
-
- @Before
- fun setup() {
- interactor = SideFpsOverlayInteractorImpl(fingerprintRepository)
-
- fingerprintRepository.setProperties(
- sensorId = 1,
- strength = SensorStrength.STRONG,
- sensorType = FingerprintSensorType.REAR,
- sensorLocations =
- mapOf(
- "" to
- SensorLocationInternal(
- "" /* displayId */,
- 540 /* sensorLocationX */,
- 1636 /* sensorLocationY */,
- 130 /* sensorRadius */
- ),
- "display_id_1" to
- SensorLocationInternal(
- "display_id_1" /* displayId */,
- 100 /* sensorLocationX */,
- 300 /* sensorLocationY */,
- 20 /* sensorRadius */
- )
- )
- )
- }
-
- @Test
- fun testOverlayOffsets() =
- testScope.runTest {
- viewModel = SideFpsOverlayViewModel(mContext, interactor)
-
- val interactorOffsets by collectLastValue(interactor.overlayOffsets)
- val viewModelOffsets by collectLastValue(viewModel.overlayOffsets)
-
- assertThat(viewModelOffsets).isEqualTo(interactorOffsets)
- }
-
- private fun testWithDisplay(
- deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
- isReverseDefaultRotation: Boolean = false,
- initInfo: DisplayInfo.() -> Unit = {},
- block: () -> Unit
- ) {
- this.deviceConfig = deviceConfig
-
- when (deviceConfig) {
- DeviceConfig.X_ALIGNED -> {
- displayWidth = 3000
- displayHeight = 1500
- sensorLocation = SensorLocationInternal("", 2500, 0, 0)
- boundsWidth = 200
- boundsHeight = 100
- }
- DeviceConfig.Y_ALIGNED -> {
- displayWidth = 2500
- displayHeight = 2000
- sensorLocation = SensorLocationInternal("", 0, 300, 0)
- boundsWidth = 100
- boundsHeight = 200
- }
- }
-
- indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
- displayBounds = Rect(0, 0, displayWidth, displayHeight)
-
- val displayInfo = DisplayInfo()
- displayInfo.initInfo()
-
- val dmGlobal = Mockito.mock(DisplayManagerGlobal::class.java)
- val display =
- Display(
- dmGlobal,
- DISPLAY_ID,
- displayInfo,
- DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
- )
-
- whenever(dmGlobal.getDisplayInfo(ArgumentMatchers.eq(DISPLAY_ID))).thenReturn(displayInfo)
-
- val sideFpsOverlayViewModelContext =
- context.createDisplayContext(display) as SysuiTestableContext
- sideFpsOverlayViewModelContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_reverseDefaultRotation,
- isReverseDefaultRotation
- )
- viewModel = SideFpsOverlayViewModel(sideFpsOverlayViewModelContext, interactor)
-
- block()
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
- * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
- * placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForXAlignedSensor_0() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
- isReverseDefaultRotation = false,
- { rotation = Surface.ROTATION_0 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- val displayInfo: DisplayInfo = DisplayInfo()
- context.display!!.getDisplayInfo(displayInfo)
- assertThat(displayInfo.rotation).isEqualTo(Surface.ROTATION_0)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left)
- .isEqualTo(sensorLocation.sensorLocationX)
- assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
- }
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
- * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
- * works correctly, tests for indicator placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
- isReverseDefaultRotation = true,
- { rotation = Surface.ROTATION_270 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left)
- .isEqualTo(sensorLocation.sensorLocationX)
- assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
- }
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
- * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
- * placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForYAlignedSensor_0() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED,
- isReverseDefaultRotation = false,
- { rotation = Surface.ROTATION_0 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
- assertThat(viewModel.sensorBounds.value.top)
- .isEqualTo(sensorLocation.sensorLocationY)
- }
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
- * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
- * works correctly, tests for indicator placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED,
- isReverseDefaultRotation = true,
- { rotation = Surface.ROTATION_270 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
- assertThat(viewModel.sensorBounds.value.top)
- .isEqualTo(sensorLocation.sensorLocationY)
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 45d1af722369..8edc6cf8dd54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -29,6 +29,7 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -77,7 +78,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
SceneTestUtils.CONTAINER_1,
@@ -88,7 +89,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -98,7 +99,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -112,8 +113,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(1)
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(1)
- assertThat(entries?.map { it.input }).containsExactly(1)
+ assertThat(pin).containsExactly(1)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -123,7 +123,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -134,12 +134,12 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown()
runCurrent()
underTest.onPinButtonClicked(1)
- assertThat(entries).hasSize(1)
+ assertThat(pin).hasSize(1)
underTest.onBackspaceButtonClicked()
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -148,7 +148,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
testScope.runTest {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -166,9 +166,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(4)
underTest.onPinButtonClicked(5)
- assertThat(entries).hasSize(3)
- assertThat(entries?.map { it.input }).containsExactly(1, 4, 5).inOrder()
- assertThat(entries?.map { it.sequenceNumber }).isInStrictOrder()
+ assertThat(pin).containsExactly(1, 4, 5).inOrder()
}
@Test
@@ -177,7 +175,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -195,7 +193,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onBackspaceButtonLongPressed()
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -227,7 +225,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -244,7 +242,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onAuthenticateButtonClicked()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -255,7 +253,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -271,7 +269,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(5) // PIN is now wrong!
underTest.onAuthenticateButtonClicked()
assertThat(message?.text).isEqualTo(WRONG_PIN)
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
// Enter the correct PIN:
@@ -312,7 +310,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
@@ -329,7 +327,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
FakeAuthenticationRepository.DEFAULT_PIN.last() + 1
) // PIN is now wrong!
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
new file mode 100644
index 000000000000..4c279ea08fd7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -0,0 +1,277 @@
+package com.android.systemui.bouncer.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+import com.android.systemui.bouncer.ui.viewmodel.PinInputSubject.Companion.assertThat
+import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel.Companion.empty
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
+import com.google.common.truth.Truth.assertThat
+import java.lang.Character.isDigit
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * This test uses a mnemonic code to create and verify PinInput instances: strings of digits [0-9]
+ * for [Digit] tokens, as well as a `C` for the [ClearAll] token.
+ */
+@SmallTest
+@RunWith(JUnit4::class)
+class PinInputViewModelTest : SysuiTestCase() {
+
+ @Test
+ fun create_emptyList_throws() {
+ assertThrows(IllegalArgumentException::class.java) { PinInputViewModel(emptyList()) }
+ }
+
+ @Test
+ fun create_inputWithoutLeadingClearAll_throws() {
+ val exception =
+ assertThrows(IllegalArgumentException::class.java) {
+ PinInputViewModel(listOf(Digit(0)))
+ }
+ assertThat(exception).hasMessageThat().contains("does not begin with a ClearAll token")
+ }
+
+ @Test
+ fun create_inputNotInAscendingOrder_throws() {
+ val sentinel = ClearAll()
+ val first = Digit(0)
+ val second = Digit(1)
+ // [first] is created before [second] is created, thus their sequence numbers are ordered.
+ check(first.sequenceNumber < second.sequenceNumber)
+
+ val exception =
+ assertThrows(IllegalArgumentException::class.java) {
+ // Passing the [Digit] tokens in reverse order throws.
+ PinInputViewModel(listOf(sentinel, second, first))
+ }
+ assertThat(exception).hasMessageThat().contains("EntryTokens are not sorted")
+ }
+
+ @Test
+ fun append_digitToEmptyInput() {
+ val result = empty().append(0)
+ assertThat(result).matches("C0")
+ }
+
+ @Test
+ fun append_digitToExistingPin() {
+ val subject = pinInput("C1")
+ assertThat(subject.append(2)).matches("C12")
+ }
+
+ @Test
+ fun append_withTwoCompletePinsEntered_dropsFirst() {
+ val subject = pinInput("C12C34C")
+ assertThat(subject.append(5)).matches("C34C5")
+ }
+
+ @Test
+ fun deleteLast_removesLastDigit() {
+ val subject = pinInput("C12")
+ assertThat(subject.deleteLast()).matches("C1")
+ }
+
+ @Test
+ fun deleteLast_onEmptyInput_returnsSameInstance() {
+ val subject = empty()
+ assertThat(subject.deleteLast()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun deleteLast_onInputEndingInClearAll_returnsSameInstance() {
+ val subject = pinInput("C12C")
+ assertThat(subject.deleteLast()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun clearAll_appendsClearAllEntryToExistingInput() {
+ val subject = pinInput("C12")
+ assertThat(subject.clearAll()).matches("C12C")
+ }
+
+ @Test
+ fun clearAll_onInputEndingInClearAll_returnsSameInstance() {
+ val subject = pinInput("C12C")
+ assertThat(subject.clearAll()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun clearAll_retainsUpToTwoPinEntries() {
+ val subject = pinInput("C12C34")
+ assertThat(subject.clearAll()).matches("C12C34C")
+ }
+
+ @Test
+ fun isEmpty_onEmptyInput_returnsTrue() {
+ val subject = empty()
+ assertThat(subject.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun isEmpty_whenLastEntryIsDigit_returnsFalse() {
+ val subject = pinInput("C1234")
+ assertThat(subject.isEmpty()).isFalse()
+ }
+
+ @Test
+ fun isEmpty_whenLastEntryIsClearAll_returnsTrue() {
+ val subject = pinInput("C1234C")
+ assertThat(subject.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun getPin_onEmptyInput_returnsEmptyList() {
+ val subject = empty()
+ assertThat(subject.getPin()).isEmpty()
+ }
+
+ @Test
+ fun getPin_whenLastEntryIsDigit_returnsPin() {
+ val subject = pinInput("C1234")
+ assertThat(subject.getPin()).containsExactly(1, 2, 3, 4)
+ }
+
+ @Test
+ fun getPin_withMultiplePins_returnsLastPin() {
+ val subject = pinInput("C1234C5678")
+ assertThat(subject.getPin()).containsExactly(5, 6, 7, 8)
+ }
+
+ @Test
+ fun getPin_whenLastEntryIsClearAll_returnsEmptyList() {
+ val subject = pinInput("C1234C")
+ assertThat(subject.getPin()).isEmpty()
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_onEmptyInput_returnsSentinel() {
+ val subject = empty()
+ val sentinel = subject.input[0] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_whenLastEntryIsDigit_returnsSentinel() {
+ val subject = pinInput("C1234")
+ val sentinel = subject.input[0] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_withMultiplePins_returnsLastMarker() {
+ val subject = pinInput("C1234C5678")
+ val lastMarker = subject.input[5] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastMarker)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_whenLastEntryIsClearAll_returnsLastEntry() {
+ val subject = pinInput("C1234C")
+ val lastEntry = subject.input[5] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastEntry)
+ }
+
+ @Test
+ fun getDigits_invalidClearAllMarker_onEmptyInput_returnsEmptyList() {
+ val subject = empty()
+ assertThat(subject.getDigits(ClearAll())).isEmpty()
+ }
+
+ @Test
+ fun getDigits_invalidClearAllMarker_whenLastEntryIsDigit_returnsEmptyList() {
+ val subject = pinInput("C1234")
+ assertThat(subject.getDigits(ClearAll())).isEmpty()
+ }
+
+ @Test
+ fun getDigits_clearAllMarkerPointsToFirstPin_returnsFirstPinDigits() {
+ val subject = pinInput("C1234C5678")
+ val marker = subject.input[0] as ClearAll
+
+ assertThat(subject.getDigits(marker).map { it.input }).containsExactly(1, 2, 3, 4)
+ }
+
+ @Test
+ fun getDigits_clearAllMarkerPointsToLastPin_returnsLastPinDigits() {
+ val subject = pinInput("C1234C5678")
+ val marker = subject.input[5] as ClearAll
+
+ assertThat(subject.getDigits(marker).map { it.input }).containsExactly(5, 6, 7, 8)
+ }
+
+ @Test
+ fun entryToken_equality() {
+ val clearAll = ClearAll()
+ val zero = Digit(0)
+ val one = Digit(1)
+
+ // Guava's EqualsTester is not available in this codebase.
+ assertThat(zero.equals(zero.copy())).isTrue()
+
+ assertThat(zero.equals(one)).isFalse()
+ assertThat(zero.equals(clearAll)).isFalse()
+
+ assertThat(clearAll.equals(clearAll.copy())).isTrue()
+ assertThat(clearAll.equals(zero)).isFalse()
+
+ // Not equal when the sequence number does not match
+ assertThat(zero.equals(Digit(0))).isFalse()
+ assertThat(clearAll.equals(ClearAll())).isFalse()
+ }
+
+ private fun pinInput(mnemonics: String): PinInputViewModel {
+ return PinInputViewModel(
+ mnemonics.map {
+ when {
+ it == 'C' -> ClearAll()
+ isDigit(it) -> Digit(it.digitToInt())
+ else -> throw AssertionError()
+ }
+ }
+ )
+ }
+}
+
+private class PinInputSubject
+private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) :
+ Subject(metadata, actual) {
+
+ fun matches(mnemonics: String) {
+ val actualMnemonics =
+ actual.input
+ .map { entry ->
+ when (entry) {
+ is Digit -> entry.input.digitToChar()
+ is ClearAll -> 'C'
+ else -> throw IllegalArgumentException()
+ }
+ }
+ .joinToString(separator = "")
+
+ if (mnemonics != actualMnemonics) {
+ failWithActual(
+ Fact.simpleFact(
+ "expected pin input to be '$mnemonics' but is '$actualMnemonics' instead"
+ )
+ )
+ }
+ }
+
+ companion object {
+ fun assertThat(input: PinInputViewModel): PinInputSubject =
+ assertAbout(Factory(::PinInputSubject)).that(input)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
index 2a4c0eb18d02..7628be44755d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
@@ -126,7 +126,8 @@ public class IntentCreatorTest extends SysuiTestCase {
assertEquals(Intent.ACTION_CHOOSER, intent.getAction());
assertFlags(intent, EXTERNAL_INTENT_FLAGS);
Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
- assertEquals(uri, target.getData());
+ assertEquals(uri, target.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class));
+ assertEquals(uri, target.getClipData().getItemAt(0).getUri());
assertEquals("image/png", target.getType());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
index 872c0794ce64..2b9821406fea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
@@ -61,10 +61,8 @@ public class ShadeTouchHandlerTest extends SysuiTestCase {
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces),
+ mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
TOUCH_HEIGHT);
- when(mCentralSurfaces.getShadeViewController())
- .thenReturn(mShadeViewController);
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 7379cd51d383..0ffa2d7b970e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -37,7 +37,9 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -345,6 +347,21 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mViewMediator.setKeyguardEnabled(false);
TestableLooper.get(this).processAllMessages();
+ mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+ mUpdateMonitor.getCurrentUser());
+ mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+ final ArgumentCaptor<Runnable> animationRunnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+ animationRunnableCaptor.capture());
+
+ when(mStatusBarStateController.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ animationRunnableCaptor.getValue().run();
+
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ mViewMediator.mViewMediatorCallback.keyguardGone();
+
// Then dream should wake up
verify(mPowerManager).wakeUp(anyLong(), anyInt(),
eq("com.android.systemui:UNLOCK_DREAMING"));
@@ -695,6 +712,67 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
}
@Test
+ public void testWakeAndUnlockingOverDream() {
+ // Send signal to wake
+ mViewMediator.onWakeAndUnlocking();
+
+ // Ensure not woken up yet
+ verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+ // Verify keyguard told of authentication
+ verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+ mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+ mUpdateMonitor.getCurrentUser());
+ mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+ final ArgumentCaptor<Runnable> animationRunnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+ animationRunnableCaptor.capture());
+
+ when(mStatusBarStateController.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ animationRunnableCaptor.getValue().run();
+
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ mViewMediator.mViewMediatorCallback.keyguardGone();
+
+ // Verify woken up now.
+ verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
+ }
+
+ @Test
+ public void testWakeAndUnlockingOverDream_signalAuthenticateIfStillShowing() {
+ // Send signal to wake
+ mViewMediator.onWakeAndUnlocking();
+
+ // Ensure not woken up yet
+ verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+ // Verify keyguard told of authentication
+ verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+ clearInvocations(mStatusBarKeyguardViewManager);
+ mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+ mUpdateMonitor.getCurrentUser());
+ mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+ final ArgumentCaptor<Runnable> animationRunnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+ animationRunnableCaptor.capture());
+
+ when(mStatusBarStateController.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ animationRunnableCaptor.getValue().run();
+
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+
+ mViewMediator.mViewMediatorCallback.keyguardGone();
+
+
+ // Verify keyguard view controller informed of authentication again
+ verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+ }
+
+ @Test
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public void testDoKeyguardWhileInteractive_resets() {
mViewMediator.setShowingLocked(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 925ac30b99fd..05d6b99fe227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -16,6 +16,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.util.mockito.any
import com.android.systemui.utils.GlobalWindowManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -225,6 +226,9 @@ class ResourceTrimmerTest : SysuiTestCase() {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
)
- verifyNoMoreInteractions(globalWindowManager)
+ // Memory hidden should still be called.
+ verify(globalWindowManager, times(1))
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(0)).trimCaches(any())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 8127ac625748..01a6c64a6898 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -40,6 +40,9 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.coroutines.FlowValue
@@ -151,6 +154,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
+ private lateinit var fakeFacePropertyRepository: FakeFacePropertyRepository
private var wasAuthCancelled = false
private var wasDetectCancelled = false
@@ -224,6 +228,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
repository = keyguardTransitionRepository,
)
.keyguardTransitionInteractor
+ fakeFacePropertyRepository = FakeFacePropertyRepository()
return DeviceEntryFaceAuthRepositoryImpl(
mContext,
fmOverride,
@@ -245,6 +250,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthBuffer,
keyguardTransitionInteractor,
featureFlags,
+ fakeFacePropertyRepository,
dumpManager,
)
}
@@ -265,7 +271,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
val successResult = successResult()
authenticationCallback.value.onAuthenticationSucceeded(successResult)
- assertThat(authStatus()).isEqualTo(SuccessFaceAuthenticationStatus(successResult))
+ val response = authStatus() as SuccessFaceAuthenticationStatus
+ assertThat(response.successResult).isEqualTo(successResult)
assertThat(authenticated()).isTrue()
assertThat(authRunning()).isFalse()
assertThat(canFaceAuthRun()).isFalse()
@@ -494,7 +501,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
- assertThat(authStatus()).isEqualTo(HelpFaceAuthenticationStatus(9, "help msg"))
+ val response = authStatus() as HelpFaceAuthenticationStatus
+ assertThat(response.msg).isEqualTo("help msg")
+ assertThat(response.msgId).isEqualTo(response.msgId)
}
@Test
@@ -550,10 +559,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
- fun authenticateDoesNotRunWhenFpIsLockedOut() =
- testScope.runTest {
- testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) }
- }
+ fun authenticateDoesNotRunWhenFaceIsDisabled() =
+ testScope.runTest { testGatingCheckForFaceAuth { underTest.lockoutFaceAuth() } }
@Test
fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() =
@@ -590,6 +597,17 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun authenticateDoesNotRunWhenStrongBiometricIsNotAllowedAndFaceSensorIsStrong() =
+ testScope.runTest {
+ fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
+ runCurrent()
+
+ testGatingCheckForFaceAuth(isFaceStrong = true) {
+ biometricSettingsRepository.setIsStrongBiometricAllowed(false)
+ }
+ }
+
+ @Test
fun authenticateDoesNotRunWhenSecureCameraIsActive() =
testScope.runTest {
testGatingCheckForFaceAuth {
@@ -858,6 +876,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun disableFaceUnlockLocksOutFaceUnlock() =
+ testScope.runTest {
+ runCurrent()
+ initCollectors()
+ assertThat(underTest.isLockedOut.value).isFalse()
+
+ underTest.lockoutFaceAuth()
+ runCurrent()
+
+ assertThat(underTest.isLockedOut.value).isTrue()
+ }
+
+ @Test
fun detectDoesNotRunWhenFaceAuthNotSupportedInCurrentPosture() =
testScope.runTest {
testGatingCheckForDetect {
@@ -909,6 +940,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun detectDoesNotRunWhenStrongBiometricIsAllowedAndFaceAuthSensorStrengthIsStrong() =
+ testScope.runTest {
+ fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
+ runCurrent()
+
+ testGatingCheckForDetect(isFaceStrong = true) {
+ biometricSettingsRepository.setIsStrongBiometricAllowed(true)
+ // this shouldn't matter as face is set as a strong sensor
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ }
+ }
+
+ @Test
fun detectDoesNotRunIfUdfpsIsRunning() =
testScope.runTest {
testGatingCheckForDetect {
@@ -999,9 +1043,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsCalled()
}
- private suspend fun TestScope.testGatingCheckForFaceAuth(gatingCheckModifier: () -> Unit) {
+ private suspend fun TestScope.testGatingCheckForFaceAuth(
+ isFaceStrong: Boolean = false,
+ gatingCheckModifier: () -> Unit
+ ) {
initCollectors()
- allPreconditionsToRunFaceAuthAreTrue()
+ allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
gatingCheckModifier()
runCurrent()
@@ -1010,7 +1057,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
assertThat(underTest.canRunFaceAuth.value).isFalse()
// flip the gating check back on.
- allPreconditionsToRunFaceAuthAreTrue()
+ allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
triggerFaceAuth(false)
@@ -1029,12 +1076,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsNotCalled()
}
- private suspend fun TestScope.testGatingCheckForDetect(gatingCheckModifier: () -> Unit) {
+ private suspend fun TestScope.testGatingCheckForDetect(
+ isFaceStrong: Boolean = false,
+ gatingCheckModifier: () -> Unit
+ ) {
initCollectors()
allPreconditionsToRunFaceAuthAreTrue()
- // This will stop face auth from running but is required to be false for detect.
- biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ if (isFaceStrong) {
+ biometricSettingsRepository.setStrongBiometricAllowed(false)
+ } else {
+ // This will stop face auth from running but is required to be false for detect.
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ }
runCurrent()
assertThat(canFaceAuthRun()).isFalse()
@@ -1069,11 +1123,14 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true }
}
- private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
+ private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue(
+ isFaceStrong: Boolean = false
+ ) {
+ verify(faceManager, atLeastOnce())
+ .addLockoutResetCallback(faceLockoutResetCallback.capture())
biometricSettingsRepository.setFaceEnrolled(true)
biometricSettingsRepository.setIsFaceAuthEnabled(true)
fakeUserRepository.setUserSwitching(false)
- deviceEntryFingerprintAuthRepository.setLockedOut(false)
trustRepository.setCurrentUserTrusted(false)
keyguardRepository.setKeyguardGoingAway(false)
keyguardRepository.setWakefulnessModel(
@@ -1083,10 +1140,15 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
WakeSleepReason.OTHER
)
)
- biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
+ if (isFaceStrong) {
+ biometricSettingsRepository.setStrongBiometricAllowed(true)
+ } else {
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
+ }
biometricSettingsRepository.setIsUserInLockdown(false)
fakeUserRepository.setSelectedUserInfo(primaryUser)
biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true)
+ faceLockoutResetCallback.value.onLockoutReset(0)
bouncerRepository.setAlternateVisible(true)
keyguardRepository.setKeyguardShowing(true)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index f9745779ecfb..ec30732dda23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -17,31 +17,43 @@
package com.android.systemui.keyguard.data.repository
import android.graphics.Point
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.core.animation.AnimatorTestRule
import androidx.test.filters.SmallTest
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LightRevealEffect
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
@SmallTest
@RoboPilotTest
-@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
class LightRevealScrimRepositoryTest : SysuiTestCase() {
private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
private lateinit var underTest: LightRevealScrimRepositoryImpl
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -50,112 +62,127 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() {
}
@Test
- fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() =
- runTest {
- val values = mutableListOf<LightRevealEffect>()
- val job = launch { underTest.revealEffect.collect { values.add(it) } }
-
- // We should initially emit the default reveal effect.
- runCurrent()
- values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
-
- // The source and sensor locations are still null, so we should still be using the
- // default reveal despite a biometric unlock.
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
-
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
+ fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest {
+ val values = mutableListOf<LightRevealEffect>()
+ val job = launch { underTest.revealEffect.collect { values.add(it) } }
+
+ fakeKeyguardRepository.setWakefulnessModel(
+ WakefulnessModel(
+ WakefulnessState.STARTING_TO_WAKE,
+ WakeSleepReason.OTHER,
+ WakeSleepReason.OTHER
)
+ )
+ // We should initially emit the default reveal effect.
+ runCurrent()
+ values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
- // We got a source but still have no sensor locations, so should be sticking with
- // the default effect.
- fakeKeyguardRepository.setBiometricUnlockSource(
- BiometricUnlockSource.FINGERPRINT_SENSOR
- )
+ // The source and sensor locations are still null, so we should still be using the
+ // default reveal despite a biometric unlock.
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- // We got a location for the face sensor, but we unlocked with fingerprint.
- val faceLocation = Point(250, 0)
- fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
+ // We got a source but still have no sensor locations, so should be sticking with
+ // the default effect.
+ fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
- val fingerprintLocation = Point(500, 500)
- fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
- fakeKeyguardRepository.setBiometricUnlockSource(
- BiometricUnlockSource.FINGERPRINT_SENSOR
- )
- fakeKeyguardRepository.setBiometricUnlockState(
- BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
- )
+ // We got a location for the face sensor, but we unlocked with fingerprint.
+ val faceLocation = Point(250, 0)
+ fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
- // We should now have switched to the circle reveal, at the fingerprint location.
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
- val valuesPrevSize = values.size
- fakeKeyguardRepository.setBiometricUnlockState(
- BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
- )
- fakeKeyguardRepository.setBiometricUnlockState(
- BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
- )
- assertEquals(valuesPrevSize, values.size)
-
- // Non-biometric unlock, we should return to the default reveal.
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
-
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- { it == DEFAULT_REVEAL_EFFECT },
- )
+ // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
+ val fingerprintLocation = Point(500, 500)
+ fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
+ fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
+
+ // We should now have switched to the circle reveal, at the fingerprint location.
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ )
- // We already have a face location, so switching to face source should update the
- // CircleReveal.
- fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
- runCurrent()
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
- runCurrent()
-
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == faceLocation.x &&
- it.centerY == faceLocation.y
- },
- )
+ // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
+ val valuesPrevSize = values.size
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
+ fakeKeyguardRepository.setBiometricUnlockState(
+ BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+ )
+ assertEquals(valuesPrevSize, values.size)
+
+ // Non-biometric unlock, we should return to the default reveal.
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
+
+ // We already have a face location, so switching to face source should update the
+ // CircleReveal.
+ fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
+ runCurrent()
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+ runCurrent()
+
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ { it == DEFAULT_REVEAL_EFFECT },
+ { it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y },
+ )
+
+ job.cancel()
+ }
- job.cancel()
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ fun revealAmount_emitsTo1AfterAnimationStarted() =
+ runTest(UnconfinedTestDispatcher()) {
+ val value by collectLastValue(underTest.revealAmount)
+ underTest.startRevealAmountAnimator(true)
+ assertEquals(0.0f, value)
+ animatorTestRule.advanceTimeBy(500L)
+ assertEquals(1.0f, value)
+ }
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
+ runTest(UnconfinedTestDispatcher()) {
+ val value by collectLastValue(underTest.revealAmount)
+ underTest.startRevealAmountAnimator(false)
+ assertEquals(1.0f, value)
+ animatorTestRule.advanceTimeBy(500L)
+ assertEquals(0.0f, value)
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index ced0a213ca97..8636dd8df3b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -38,6 +38,7 @@ import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
@@ -73,6 +74,8 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository
+ private lateinit var fakeDeviceEntryFingerprintAuthRepository:
+ FakeDeviceEntryFingerprintAuthRepository
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -94,6 +97,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
)
.keyguardTransitionInteractor
+ fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
underTest =
SystemUIKeyguardFaceAuthInteractor(
mContext,
@@ -127,6 +131,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
featureFlags,
FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
keyguardUpdateMonitor,
+ fakeDeviceEntryFingerprintAuthRepository
)
}
@@ -335,4 +340,15 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
assertThat(faceAuthRepository.runningAuthRequest.value)
.isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false))
}
+
+ @Test
+ fun faceUnlockIsDisabledWhenFpIsLockedOut() =
+ testScope.runTest {
+ underTest.start()
+
+ fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.wasDisabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index 6e7ba6dd1ecb..906d94859140 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -27,27 +27,37 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.Spy
@SmallTest
@RoboPilotTest
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class LightRevealScrimInteractorTest : SysuiTestCase() {
private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
- private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+
+ @Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+
+ private val testScope = TestScope()
private val keyguardTransitionInteractor =
KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
+ scope = testScope.backgroundScope,
repository = fakeKeyguardTransitionRepository,
)
.keyguardTransitionInteractor
@@ -69,9 +79,9 @@ class LightRevealScrimInteractorTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
underTest =
LightRevealScrimInteractor(
- fakeKeyguardTransitionRepository,
keyguardTransitionInteractor,
- fakeLightRevealScrimRepository
+ fakeLightRevealScrimRepository,
+ testScope.backgroundScope
)
}
@@ -110,52 +120,36 @@ class LightRevealScrimInteractorTest : SysuiTestCase() {
}
@Test
- fun revealAmount_invertedWhenAppropriate() =
- runTest(UnconfinedTestDispatcher()) {
- val values = mutableListOf<Float>()
- val job = underTest.revealAmount.onEach(values::add).launchIn(this)
-
+ fun lightRevealEffect_startsAnimationOnlyForDifferentStateTargets() =
+ testScope.runTest {
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- value = 0.3f
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.OFF,
+ to = KeyguardState.OFF
)
)
-
- assertEquals(values, listOf(0.3f))
+ runCurrent()
+ verify(fakeLightRevealScrimRepository, never()).startRevealAmountAnimator(anyBoolean())
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.AOD,
- value = 0.3f
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN
)
)
-
- assertEquals(values, listOf(0.3f, 0.7f))
-
- job.cancel()
- }
-
- @Test
- fun revealAmount_ignoresTransitionsThatDoNotAffectRevealAmount() =
- runTest(UnconfinedTestDispatcher()) {
- val values = mutableListOf<Float>()
- val job = underTest.revealAmount.onEach(values::add).launchIn(this)
-
- fakeKeyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.AOD, value = 0.3f)
- )
-
- assertEquals(values, emptyList<Float>())
+ runCurrent()
+ verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(true)
fakeKeyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f)
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING
+ )
)
-
- assertEquals(values, emptyList<Float>())
-
- job.cancel()
+ runCurrent()
+ verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
new file mode 100644
index 000000000000..f5a70f07c968
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.model
+
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class SysUiStateExtTest : SysuiTestCase() {
+
+ private val underTest = SysUiState(FakeDisplayTracker(context))
+
+ @Test
+ fun updateFlags() {
+ underTest.updateFlags(
+ Display.DEFAULT_DISPLAY,
+ 1 to true,
+ 2 to false,
+ 3 to true,
+ )
+
+ assertThat(underTest.flags and 1).isNotEqualTo(0)
+ assertThat(underTest.flags and 2).isEqualTo(0)
+ assertThat(underTest.flags and 3).isNotEqualTo(0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
deleted file mode 100644
index ceacaf9557ca..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.data.repository
-
-import android.content.Context
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeRepositoryTest : SysuiTestCase() {
-
- private lateinit var inputProxy: MultiShadeInputProxy
-
- @Before
- fun setUp() {
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun proxiedInput() = runTest {
- val underTest = create()
- val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
-
- assertWithMessage("proxiedInput should start with null").that(latest).isNull()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
- assertThat(latest).isEqualTo(ProxiedInputModel.OnTap)
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f))
- assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f))
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f))
- assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f))
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd)
- }
-
- @Test
- fun shadeConfig_dualShadeEnabled() = runTest {
- overrideResource(R.bool.dual_shade_enabled, true)
- val underTest = create()
- val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
-
- assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java)
- }
-
- @Test
- fun shadeConfig_dualShadeNotEnabled() = runTest {
- overrideResource(R.bool.dual_shade_enabled, false)
- val underTest = create()
- val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
-
- assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java)
- }
-
- @Test
- fun forceCollapseAll() = runTest {
- val underTest = create()
- val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll)
-
- assertWithMessage("forceCollapseAll should start as false!")
- .that(forceCollapseAll)
- .isFalse()
-
- underTest.setForceCollapseAll(true)
- assertThat(forceCollapseAll).isTrue()
-
- underTest.setForceCollapseAll(false)
- assertThat(forceCollapseAll).isFalse()
- }
-
- @Test
- fun shadeInteraction() = runTest {
- val underTest = create()
- val shadeInteraction: MultiShadeInteractionModel? by
- collectLastValue(underTest.shadeInteraction)
-
- assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull()
-
- underTest.setShadeInteraction(
- MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)
- )
- assertThat(shadeInteraction)
- .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false))
-
- underTest.setShadeInteraction(
- MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)
- )
- assertThat(shadeInteraction)
- .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true))
-
- underTest.setShadeInteraction(null)
- assertThat(shadeInteraction).isNull()
- }
-
- @Test
- fun expansion() = runTest {
- val underTest = create()
- val leftExpansion: Float? by
- collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion })
- val rightExpansion: Float? by
- collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion })
- val singleExpansion: Float? by
- collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion })
-
- assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero()
- assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero()
- assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero()
-
- underTest.setExpansion(
- shadeId = ShadeId.LEFT,
- 0.4f,
- )
- assertThat(leftExpansion).isEqualTo(0.4f)
- assertThat(rightExpansion).isEqualTo(0f)
- assertThat(singleExpansion).isEqualTo(0f)
-
- underTest.setExpansion(
- shadeId = ShadeId.RIGHT,
- 0.73f,
- )
- assertThat(leftExpansion).isEqualTo(0.4f)
- assertThat(rightExpansion).isEqualTo(0.73f)
- assertThat(singleExpansion).isEqualTo(0f)
-
- underTest.setExpansion(
- shadeId = ShadeId.LEFT,
- 0.1f,
- )
- underTest.setExpansion(
- shadeId = ShadeId.SINGLE,
- 0.88f,
- )
- assertThat(leftExpansion).isEqualTo(0.1f)
- assertThat(rightExpansion).isEqualTo(0.73f)
- assertThat(singleExpansion).isEqualTo(0.88f)
- }
-
- private fun create(): MultiShadeRepository {
- return create(
- context = context,
- inputProxy = inputProxy,
- )
- }
-
- companion object {
- fun create(
- context: Context,
- inputProxy: MultiShadeInputProxy,
- ): MultiShadeRepository {
- return MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- )
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
deleted file mode 100644
index bcc99bc8dd0c..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.domain.interactor
-
-import android.content.Context
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeInteractorTest : SysuiTestCase() {
-
- private lateinit var testScope: TestScope
- private lateinit var inputProxy: MultiShadeInputProxy
-
- @Before
- fun setUp() {
- testScope = TestScope()
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun maxShadeExpansion() =
- testScope.runTest {
- val underTest = create()
- val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion)
- assertWithMessage("maxShadeExpansion must start with 0.0!")
- .that(maxShadeExpansion)
- .isEqualTo(0f)
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
- assertThat(maxShadeExpansion).isEqualTo(0.441f)
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
- assertThat(maxShadeExpansion).isEqualTo(0.442f)
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
- assertThat(maxShadeExpansion).isEqualTo(0.441f)
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
- assertThat(maxShadeExpansion).isEqualTo(0f)
- }
-
- @Test
- fun isAnyShadeExpanded() =
- testScope.runTest {
- val underTest = create()
- val isAnyShadeExpanded: Boolean? by collectLastValue(underTest.isAnyShadeExpanded)
- assertWithMessage("isAnyShadeExpanded must start with false!")
- .that(isAnyShadeExpanded)
- .isFalse()
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
- assertThat(isAnyShadeExpanded).isTrue()
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
- assertThat(isAnyShadeExpanded).isTrue()
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
- assertThat(isAnyShadeExpanded).isTrue()
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
- assertThat(isAnyShadeExpanded).isFalse()
- }
-
- @Test
- fun isVisible_dualShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, true)
- val underTest = create()
- val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
- val isRightShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.RIGHT))
- val isSingleShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.SINGLE))
-
- assertThat(isLeftShadeVisible).isTrue()
- assertThat(isRightShadeVisible).isTrue()
- assertThat(isSingleShadeVisible).isFalse()
- }
-
- @Test
- fun isVisible_singleShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, false)
- val underTest = create()
- val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
- val isRightShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.RIGHT))
- val isSingleShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.SINGLE))
-
- assertThat(isLeftShadeVisible).isFalse()
- assertThat(isRightShadeVisible).isFalse()
- assertThat(isSingleShadeVisible).isTrue()
- }
-
- @Test
- fun isNonProxiedInputAllowed() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeNonProxiedInputAllowed: Boolean? by
- collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT))
- assertWithMessage("isNonProxiedInputAllowed should start as true!")
- .that(isLeftShadeNonProxiedInputAllowed)
- .isTrue()
-
- // Need to collect proxied input so the flows become hot as the gesture cancelation code
- // logic sits in side the proxiedInput flow for each shade.
- collectLastValue(underTest.proxiedInput(ShadeId.LEFT))
- collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
-
- // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
- // the
- // same shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
- )
- assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
-
- // Registering the end of the proxied interaction re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
-
- // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
- // disallowing non-proxied input on the LEFT shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
- )
- assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
- }
-
- @Test
- fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
- // shade.
- underTest.onUserInteractionStarted(ShadeId.RIGHT)
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- underTest.onUserInteractionEnded(ShadeId.RIGHT)
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
- // shade.
- underTest.onUserInteractionStarted(ShadeId.LEFT)
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the LEFT shade re-allows it.
- underTest.onUserInteractionEnded(ShadeId.LEFT)
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
- }
-
- @Test
- fun collapseAll() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- underTest.collapseAll()
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isTrue()
-
- // Receiving proxied input on that's not a tap gesture, on the left-hand side resets the
- // "collapse all". Note that now the RIGHT shade is force-collapsed because we're
- // interacting with the LEFT shade.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f))
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isFalse()
- }
-
- @Test
- fun onTapOutside_collapsesAll() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isTrue()
- }
-
- @Test
- fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
- testScope.runTest {
- val underTest = create()
- val proxiedInput: ProxiedInputModel? by
- collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
- underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT)
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNull()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
- assertThat(proxiedInput).isNull()
-
- underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT)
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNotNull()
- }
-
- private fun create(): MultiShadeInteractor {
- return create(
- testScope = testScope,
- context = context,
- inputProxy = inputProxy,
- )
- }
-
- companion object {
- fun create(
- testScope: TestScope,
- context: Context,
- inputProxy: MultiShadeInputProxy,
- ): MultiShadeInteractor {
- return MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository =
- MultiShadeRepositoryTest.create(
- context = context,
- inputProxy = inputProxy,
- ),
- inputProxy = inputProxy,
- )
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
deleted file mode 100644
index 5890cbd06476..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
+++ /dev/null
@@ -1,530 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.domain.interactor
-
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.shade.ShadeController
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.currentTime
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
-
- private lateinit var underTest: MultiShadeMotionEventInteractor
-
- private lateinit var testScope: TestScope
- private lateinit var motionEvents: MutableSet<MotionEvent>
- private lateinit var repository: MultiShadeRepository
- private lateinit var interactor: MultiShadeInteractor
- private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
- private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
- private lateinit var falsingManager: FalsingManagerFake
- @Mock private lateinit var shadeController: ShadeController
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- testScope = TestScope()
- motionEvents = mutableSetOf()
-
- val inputProxy = MultiShadeInputProxy()
- repository =
- MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- )
- interactor =
- MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository = repository,
- inputProxy = inputProxy,
- )
- val featureFlags = FakeFeatureFlags()
- featureFlags.set(Flags.DUAL_SHADE, true)
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- falsingManager = FalsingManagerFake()
-
- underTest =
- MultiShadeMotionEventInteractor(
- applicationContext = context,
- applicationScope = testScope.backgroundScope,
- multiShadeInteractor = interactor,
- featureFlags = featureFlags,
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- repository = keyguardTransitionRepository,
- )
- .keyguardTransitionInteractor,
- falsingManager = falsingManager,
- shadeController = shadeController,
- )
- }
-
- @After
- fun tearDown() {
- motionEvents.forEach { motionEvent -> motionEvent.recycle() }
- }
-
- @Test
- fun listenForIsAnyShadeExpanded_expanded_makesWindowViewVisible() =
- testScope.runTest {
- whenever(shadeController.isKeyguard).thenReturn(false)
- repository.setExpansion(ShadeId.LEFT, 0.1f)
- val expanded by collectLastValue(interactor.isAnyShadeExpanded)
- assertThat(expanded).isTrue()
-
- verify(shadeController).makeExpandedVisible(anyBoolean())
- }
-
- @Test
- fun listenForIsAnyShadeExpanded_collapsed_makesWindowViewInvisible() =
- testScope.runTest {
- whenever(shadeController.isKeyguard).thenReturn(false)
- repository.setForceCollapseAll(true)
- val expanded by collectLastValue(interactor.isAnyShadeExpanded)
- assertThat(expanded).isFalse()
-
- verify(shadeController).makeExpandedInvisible()
- }
-
- @Test
- fun listenForIsAnyShadeExpanded_collapsedOnKeyguard_makesWindowViewVisible() =
- testScope.runTest {
- whenever(shadeController.isKeyguard).thenReturn(true)
- repository.setForceCollapseAll(true)
- val expanded by collectLastValue(interactor.isAnyShadeExpanded)
- assertThat(expanded).isFalse()
-
- verify(shadeController).makeExpandedVisible(anyBoolean())
- }
-
- @Test
- fun shouldIntercept_initialDown_returnsFalse() =
- testScope.runTest {
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveBelowTouchSlop_returnsFalse() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop - 1f,
- )
- )
- )
- .isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlop_returnsTrue() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isTrue()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlop_butHorizontalFirst_returnsFalse() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- }
-
- @Test
- fun shouldIntercept_up_afterMovedAboveTouchSlop_returnsTrue() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
-
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isTrue()
- }
-
- @Test
- fun shouldIntercept_cancel_afterMovedAboveTouchSlop_returnsTrue() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
-
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isTrue()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndUp_butShadeExpanded_returnsFalse() =
- testScope.runTest {
- repository.setExpansion(ShadeId.LEFT, 0.1f)
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndCancel_butShadeExpanded_returnsFalse() =
- testScope.runTest {
- repository.setExpansion(ShadeId.LEFT, 0.1f)
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndUp_butBouncerShowing_returnsFalse() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.PRIMARY_BOUNCER,
- value = 0.1f,
- transitionState = TransitionState.STARTED,
- )
- )
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndCancel_butBouncerShowing_returnsFalse() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.PRIMARY_BOUNCER,
- value = 0.1f,
- transitionState = TransitionState.STARTED,
- )
- )
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
- }
-
- @Test
- fun tap_doesNotSendProxiedInput() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
-
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragBelowTouchSlop_doesNotSendProxiedInput() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop - 1f))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
-
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragShadeAboveTouchSlopAndUp() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 100f, // left shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = touchSlop + 1f
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 100f, // left shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput)
- .isEqualTo(
- ProxiedInputModel.OnDrag(
- xFraction = 0.1f,
- yDragAmountPx = yDragAmountPx,
- )
- )
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val upEvent = motionEvent(MotionEvent.ACTION_UP)
- assertThat(underTest.shouldIntercept(upEvent)).isTrue()
- underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragShadeAboveTouchSlopAndCancel() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 900f, // right shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = touchSlop + 1f
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 900f, // right shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput)
- .isEqualTo(
- ProxiedInputModel.OnDrag(
- xFraction = 0.9f,
- yDragAmountPx = yDragAmountPx,
- )
- )
- assertThat(singleShadeProxiedInput).isNull()
-
- val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
- assertThat(underTest.shouldIntercept(cancelEvent)).isTrue()
- underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragUp_withUp_doesNotShowShade() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 100f, // left shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = -(touchSlop + 1f) // dragging up
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 100f, // left shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val upEvent = motionEvent(MotionEvent.ACTION_UP)
- assertThat(underTest.shouldIntercept(upEvent)).isFalse()
- underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragUp_withCancel_falseTouch_showsThenHidesBouncer() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 900f, // right shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = -(touchSlop + 1f) // drag up
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 900f, // right shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- falsingManager.setIsFalseTouch(true)
- val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
- assertThat(underTest.shouldIntercept(cancelEvent)).isFalse()
- underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- private fun TestScope.motionEvent(
- action: Int,
- downTime: Long = currentTime,
- eventTime: Long = currentTime,
- x: Float = 0f,
- y: Float = 0f,
- ): MotionEvent {
- val motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0)
- motionEvents.add(motionEvent)
- return motionEvent
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt
deleted file mode 100644
index 893530982926..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.shared.math
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@SmallTest
-@RunWith(JUnit4::class)
-class MathTest : SysuiTestCase() {
-
- @Test
- fun isZero_zero_true() {
- assertThat(0f.isZero(epsilon = EPSILON)).isTrue()
- }
-
- @Test
- fun isZero_belowPositiveEpsilon_true() {
- assertThat((EPSILON * 0.999999f).isZero(epsilon = EPSILON)).isTrue()
- }
-
- @Test
- fun isZero_aboveNegativeEpsilon_true() {
- assertThat((EPSILON * -0.999999f).isZero(epsilon = EPSILON)).isTrue()
- }
-
- @Test
- fun isZero_positiveEpsilon_false() {
- assertThat(EPSILON.isZero(epsilon = EPSILON)).isFalse()
- }
-
- @Test
- fun isZero_negativeEpsilon_false() {
- assertThat((-EPSILON).isZero(epsilon = EPSILON)).isFalse()
- }
-
- @Test
- fun isZero_positive_false() {
- assertThat(1f.isZero(epsilon = EPSILON)).isFalse()
- }
-
- @Test
- fun isZero_negative_false() {
- assertThat((-1f).isZero(epsilon = EPSILON)).isFalse()
- }
-
- companion object {
- private const val EPSILON = 0.0001f
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
deleted file mode 100644
index 0484515e38bd..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.viewmodel
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeViewModelTest : SysuiTestCase() {
-
- private lateinit var testScope: TestScope
- private lateinit var inputProxy: MultiShadeInputProxy
-
- @Before
- fun setUp() {
- testScope = TestScope()
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun scrim_whenDualShadeCollapsed() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, true)
-
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
- }
-
- @Test
- fun scrim_whenDualShadeExpanded() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, true)
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
-
- underTest.leftShade.onExpansionChanged(0.5f)
- assertThat(scrimAlpha).isEqualTo(alpha * 0.5f)
- assertThat(isScrimEnabled).isTrue()
-
- underTest.rightShade.onExpansionChanged(1f)
- assertThat(scrimAlpha).isEqualTo(alpha * 1f)
- assertThat(isScrimEnabled).isTrue()
- }
-
- @Test
- fun scrim_whenSingleShadeCollapsed() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, false)
-
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
- }
-
- @Test
- fun scrim_whenSingleShadeExpanded() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, false)
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
- underTest.singleShade.onExpansionChanged(0.95f)
-
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
- }
-
- private fun create(): MultiShadeViewModel {
- return MultiShadeViewModel(
- viewModelScope = testScope.backgroundScope,
- interactor =
- MultiShadeInteractorTest.create(
- testScope = testScope,
- context = context,
- inputProxy = inputProxy,
- ),
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
deleted file mode 100644
index e32aac596e5b..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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.multishade.ui.viewmodel
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class ShadeViewModelTest : SysuiTestCase() {
-
- private lateinit var testScope: TestScope
- private lateinit var inputProxy: MultiShadeInputProxy
- private var interactor: MultiShadeInteractor? = null
-
- @Before
- fun setUp() {
- testScope = TestScope()
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun isVisible_dualShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, true)
- val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
- val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
- val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
-
- assertThat(isLeftShadeVisible).isTrue()
- assertThat(isRightShadeVisible).isTrue()
- assertThat(isSingleShadeVisible).isFalse()
- }
-
- @Test
- fun isVisible_singleShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, false)
- val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
- val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
- val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
-
- assertThat(isLeftShadeVisible).isFalse()
- assertThat(isRightShadeVisible).isFalse()
- assertThat(isSingleShadeVisible).isTrue()
- }
-
- @Test
- fun isSwipingEnabled() =
- testScope.runTest {
- val underTest = create(ShadeId.LEFT)
- val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled)
- assertWithMessage("isSwipingEnabled should start as true!")
- .that(isSwipingEnabled)
- .isTrue()
-
- // Need to collect proxied input so the flows become hot as the gesture cancelation code
- // logic sits in side the proxiedInput flow for each shade.
- collectLastValue(underTest.proxiedInput)
- collectLastValue(create(ShadeId.RIGHT).proxiedInput)
-
- // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
- // the
- // same shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
- )
- assertThat(isSwipingEnabled).isFalse()
-
- // Registering the end of the proxied interaction re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isSwipingEnabled).isTrue()
-
- // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
- // disallowing non-proxied input on the LEFT shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
- )
- assertThat(isSwipingEnabled).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isSwipingEnabled).isTrue()
- }
-
- @Test
- fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
- testScope.runTest {
- val leftShade = create(ShadeId.LEFT)
- val rightShade = create(ShadeId.RIGHT)
- val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed)
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(rightShade.isForceCollapsed)
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
- // shade.
- rightShade.onDragStarted()
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- rightShade.onDragEnded()
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
- // shade.
- leftShade.onDragStarted()
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the LEFT shade re-allows it.
- leftShade.onDragEnded()
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
- }
-
- @Test
- fun onTapOutside_collapsesAll() =
- testScope.runTest {
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.LEFT).isForceCollapsed)
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.RIGHT).isForceCollapsed)
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isTrue()
- }
-
- @Test
- fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
- testScope.runTest {
- val underTest = create(ShadeId.RIGHT)
- val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
- underTest.onDragStarted()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNull()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
- assertThat(proxiedInput).isNull()
-
- underTest.onDragEnded()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNotNull()
- }
-
- private fun create(
- shadeId: ShadeId,
- ): ShadeViewModel {
- return ShadeViewModel(
- viewModelScope = testScope.backgroundScope,
- shadeId = shadeId,
- interactor = interactor
- ?: MultiShadeInteractorTest.create(
- testScope = testScope,
- context = context,
- inputProxy = inputProxy,
- )
- .also { interactor = it },
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 25d494cee5e8..cbfad56ed617 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -94,6 +94,7 @@ import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.rotation.RotationButtonController;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
@@ -467,6 +468,7 @@ public class NavigationBarTest extends SysuiTestCase {
when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
return spy(new NavigationBar(
mNavigationBarView,
+ mock(ShadeController.class),
mNavigationBarFrame,
null,
context,
@@ -485,7 +487,7 @@ public class NavigationBarTest extends SysuiTestCase {
Optional.of(mock(Pip.class)),
Optional.of(mock(Recents.class)),
() -> Optional.of(mCentralSurfaces),
- mock(ShadeController.class),
+ mock(ShadeViewController.class),
mock(NotificationRemoteInputManager.class),
mock(NotificationShadeDepthController.class),
mHandler,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index 611c5b987d84..fab1de00dcbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
@@ -29,6 +30,8 @@ import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -36,6 +39,8 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
@@ -58,6 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Mock private lateinit var latencyTracker: LatencyTracker
@Mock private lateinit var layoutParams: WindowManager.LayoutParams
@Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
+ private val featureFlags = FakeFeatureFlags()
@Before
fun setup() {
@@ -70,7 +76,8 @@ class BackPanelControllerTest : SysuiTestCase() {
Handler.createAsync(Looper.myLooper()),
vibratorHelper,
configurationController,
- latencyTracker
+ latencyTracker,
+ featureFlags
)
mBackPanelController.setLayoutParams(layoutParams)
mBackPanelController.setBackCallback(backCallback)
@@ -99,6 +106,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Test
fun handlesBackCommitted() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -122,7 +130,34 @@ class BackPanelControllerTest : SysuiTestCase() {
}
@Test
+ fun handlesBackCommitted_withOneWayHapticsAPI() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+ // Wait threshold duration and hold touch past trigger threshold
+ Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ACTIVE)
+ verify(backCallback).setTriggerBack(true)
+ testableLooper.moveTimeForward(100)
+ testableLooper.processAllMessages()
+ verify(vibratorHelper)
+ .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE))
+
+ finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.COMMITTED)
+ verify(backCallback).triggerBack()
+ }
+
+ @Test
fun handlesBackCancelled() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -151,6 +186,38 @@ class BackPanelControllerTest : SysuiTestCase() {
verify(backCallback).cancelBack()
}
+ @Test
+ fun handlesBackCancelled_withOneWayHapticsAPI() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationTriggerThreshold
+ )
+ // Wait threshold duration and hold touch before trigger threshold
+ Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationTriggerThreshold
+ )
+ clearInvocations(backCallback)
+ Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
+ // Move in the opposite direction to cross the deactivation threshold and cancel back
+ continueTouch(START_X)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.INACTIVE)
+ verify(backCallback).setTriggerBack(false)
+ verify(vibratorHelper)
+ .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE))
+
+ finishTouchActionUp(START_X)
+ verify(backCallback).cancelBack()
+ }
+
private fun startTouch() {
mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index a76af8e83248..c65a2d36e223 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -118,6 +118,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any()))
.thenReturn(NOTE_TASK_LONG_LABEL)
whenever(context.packageManager).thenReturn(packageManager)
+ whenever(context.createContextAsUser(any(), any())).thenReturn(context)
whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock())
whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL)
whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO)
@@ -353,7 +354,13 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
@Test
fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() {
- whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any()))
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
.thenReturn(10)
val user10 = UserHandle.of(/* userId= */ 10)
@@ -615,13 +622,21 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
- fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInWorkProfile() {
+ fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInTheUserSelectedUser() {
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(mainUserInfo.id)
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON)
- verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle)
+ verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle)
}
@Test
@@ -813,7 +828,15 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
- fun getUserForHandlingNotesTaking_cope_tailButton_shouldReturnWorkProfileUser() {
+ fun getUserForHandlingNotesTaking_cope_userSelectedWorkProfile_tailButton_shouldReturnWorkProfileUser() { // ktlint-disable max-line-length
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(workUserInfo.id)
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
@@ -823,6 +846,24 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
+ fun getUserForHandlingNotesTaking_cope_userSelectedMainProfile_tailButton_shouldReturnMainProfileUser() { // ktlint-disable max-line-length
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(mainUserInfo.id)
+ whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
+ userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
+
+ val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON)
+
+ assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id))
+ }
+
+ @Test
fun getUserForHandlingNotesTaking_cope_appClip_shouldReturnCurrentUser() {
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
new file mode 100644
index 000000000000..0a8c0ab9817d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
@@ -0,0 +1,825 @@
+/*
+ * 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.privacy
+
+import android.app.ActivityManager
+import android.content.Context
+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.UserInfo
+import android.os.Process.SYSTEM_UID
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.testing.AndroidTestingRunner
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PrivacyDialogControllerV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val USER_ID = 0
+ private const val ENT_USER_ID = 10
+
+ private const val TEST_PACKAGE_NAME = "test package name"
+ private const val TEST_ATTRIBUTION_TAG = "test attribution tag"
+ private const val TEST_PROXY_LABEL = "test proxy label"
+
+ private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA
+ private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE
+ private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION
+
+ private val TEST_INTENT = Intent("test_intent_action")
+ }
+
+ @Mock
+ private lateinit var dialog: PrivacyDialogV2
+ @Mock
+ private lateinit var permissionManager: PermissionManager
+ @Mock
+ private lateinit var packageManager: PackageManager
+ @Mock
+ private lateinit var privacyItemController: PrivacyItemController
+ @Mock
+ private lateinit var userTracker: UserTracker
+ @Mock
+ private lateinit var activityStarter: ActivityStarter
+ @Mock
+ private lateinit var privacyLogger: PrivacyLogger
+ @Mock
+ private lateinit var keyguardStateController: KeyguardStateController
+ @Mock
+ private lateinit var appOpsController: AppOpsController
+ @Captor
+ private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed>
+ @Captor
+ private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
+ @Captor
+ private lateinit var intentCaptor: ArgumentCaptor<Intent>
+ @Mock
+ private lateinit var uiEventLogger: UiEventLogger
+ @Mock
+ private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
+ private val backgroundExecutor = FakeExecutor(FakeSystemClock())
+ private val uiExecutor = FakeExecutor(FakeSystemClock())
+ private lateinit var controller: PrivacyDialogControllerV2
+ private var nextUid: Int = 0
+
+ private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
+ var list: List<PrivacyDialogV2.PrivacyElement>? = null
+ var manageApp: ((String, Int, Intent) -> Unit)? = null
+ var closeApp: ((String, Int) -> Unit)? = null
+ var openPrivacyDashboard: (() -> Unit)? = null
+
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2 {
+ this.list = list
+ this.manageApp = manageApp
+ this.closeApp = closeApp
+ this.openPrivacyDashboard = openPrivacyDashboard
+ return dialog
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ nextUid = 0
+ setUpDefaultMockResponses()
+
+ controller = PrivacyDialogControllerV2(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogLaunchAnimator,
+ dialogProvider
+ )
+ }
+
+ @After
+ fun tearDown() {
+ FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
+ dialogProvider.list = null
+ dialogProvider.manageApp = null
+ dialogProvider.closeApp = null
+ dialogProvider.openPrivacyDashboard = null
+ }
+
+ @Test
+ fun testMicMutedParameter() {
+ `when`(appOpsController.isMicMuted).thenReturn(true)
+ controller.showDialog(context)
+ backgroundExecutor.runAllReady()
+
+ verify(permissionManager).getIndicatorAppOpUsageData(true)
+ }
+
+ @Test
+ fun testPermissionManagerOnlyCalledInBackgroundThread() {
+ controller.showDialog(context)
+ verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean())
+ backgroundExecutor.runAllReady()
+ verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean())
+ }
+
+ @Test
+ fun testPackageManagerOnlyCalledInBackgroundThread() {
+ val usage = createMockPermGroupUsage()
+ `when`(usage.isPhoneCall).thenReturn(false)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ backgroundExecutor.runAllReady()
+ verify(packageManager, atLeastOnce())
+ .getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ }
+
+ @Test
+ fun testShowDialogShowsDialogWithoutView() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), anyBoolean())
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testShowDialogShowsDialogWithView() {
+ val view = View(context)
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context, view)
+ exhaustExecutors()
+
+ verify(dialogLaunchAnimator).showFromView(dialog, view)
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testDontShowEmptyDialog() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testHideDialogDismissesDialogIfShown() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ controller.dismissDialog()
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopIfNotShown() {
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopAfterDismissed() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testShowForAllUsers() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+
+ exhaustExecutors()
+ verify(dialog).setShowForAllUsers(true)
+ }
+
+ @Test
+ fun testSingleElementInList() {
+ val usage = createMockPermGroupUsage(
+ packageName = TEST_PACKAGE_NAME,
+ uid = generateUidForUser(USER_ID),
+ permissionGroupName = PERM_CAMERA,
+ lastAccessTimeMillis = 5L,
+ isActive = true,
+ isPhoneCall = false,
+ attributionTag = null,
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
+ assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).userId).isEqualTo(USER_ID)
+ assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).attributionTag).isNull()
+ assertThat(list.get(0).attributionLabel).isNull()
+ assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
+ assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
+ assertThat(list.get(0).isActive).isTrue()
+ assertThat(list.get(0).isPhoneCall).isFalse()
+ assertThat(list.get(0).isService).isFalse()
+ assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ private fun isIntentEqual(actual: Intent, expected: Intent): Boolean {
+ return actual.action == expected.action &&
+ actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) ==
+ expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) &&
+ actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle ==
+ expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle
+ }
+
+ @Test
+ fun testTwoElementsDifferentType_sorted() {
+ val usage_camera = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_camera",
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_microphone",
+ permissionGroupName = PERM_MICROPHONE
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_microphone, usage_camera)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list).hasSize(2)
+ assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
+ }
+ }
+
+ @Test
+ fun testTwoElementsSameType_oneActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true
+ )
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_active)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.isActive).isTrue()
+ }
+
+ @Test
+ fun testTwoElementsSameType_twoActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_active_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active_recent",
+ isActive = true,
+ lastAccessTimeMillis = 1L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_active, usage_active_moreRecent)
+ )
+ controller.showDialog(context)
+ exhaustExecutors()
+ assertThat(dialogProvider.list).hasSize(2)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
+ assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L)
+ }
+
+ @Test
+ fun testManyElementsSameType_bothRecent() {
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_moreRecent",
+ isActive = false,
+ lastAccessTimeMillis = 1L
+ )
+ val usage_mostRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_mostRecent",
+ isActive = false,
+ lastAccessTimeMillis = 2L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_mostRecent, usage_moreRecent)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
+ }
+
+ @Test
+ fun testMicAndCameraDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+
+ @Test
+ fun testLocationDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(2)
+ dialogProvider.list?.forEach {
+ assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+ }
+
+ @Test
+ fun testAllIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(3)
+ }
+
+ @Test
+ fun testNoIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testNotCurrentUser() {
+ val usage_other = createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID + 1)
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
+ .thenReturn(listOf(usage_other))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testStartActivitySuccess() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
+
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testStartActivityFailure() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
+
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testCallOnSecondaryUser() {
+ // Calls happen in
+ val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(ENT_USER_ID, "", 0)
+ ))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testManageAppLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testCloseAppLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.closeApp?.invoke(TEST_PACKAGE_NAME, USER_ID)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testOpenPrivacyDashboardLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.openPrivacyDashboard?.invoke()
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
+ }
+
+ @Test
+ fun testDismissedDialogLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+
+ controller.dismissDialog()
+
+ verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ }
+
+ @Test
+ fun testDefaultIntent() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnEnterpriseUser() {
+ val usage =
+ createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID),
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(
+ TEST_PACKAGE_NAME, ENT_USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnInvalidAttributionTag() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = "INVALID_ATTRIBUTION_TAG",
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testServiceIntentOnCorrectSubAttribution() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG,
+ attributionLabel = "TEST_LABEL"
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ val navigationIntent = list.get(0).navigationIntent!!
+ assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
+ .isEqualTo(PERM_CAMERA)
+ assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS))
+ .isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
+ assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
+ .isTrue()
+ assertThat(list.get(0).isService).isTrue()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnMissingAttributionLabel() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnIncorrectPermission() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo(
+ permission = "INCORRECT_PERMISSION"
+ )
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ private fun exhaustExecutors() {
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+ }
+
+ private fun setUpDefaultMockResponses() {
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList())
+ `when`(appOpsController.isMicMuted).thenReturn(false)
+
+ `when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenAnswer { FakeApplicationInfo(it.getArgument(0)) }
+
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(USER_ID, "", 0),
+ UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
+ ))
+
+ `when`(keyguardStateController.isUnlocked).thenReturn(true)
+ }
+
+ private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return label
+ }
+ }
+
+ private fun generateUidForUser(user: Int): Int {
+ return user * UserHandle.PER_USER_RANGE + nextUid++
+ }
+
+ private fun createMockResolveInfo(
+ activityInfo: ActivityInfo? = null
+ ): ResolveInfo {
+ val resolveInfo = mock(ResolveInfo::class.java)
+ resolveInfo.activityInfo = activityInfo
+ return resolveInfo
+ }
+
+ private fun createMockActivityInfo(
+ permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE,
+ className: String = "TEST_CLASS_NAME"
+ ): ActivityInfo {
+ val activityInfo = mock(ActivityInfo::class.java)
+ activityInfo.permission = permission
+ activityInfo.name = className
+ return activityInfo
+ }
+
+ private fun createMockPermGroupUsage(
+ packageName: String = TEST_PACKAGE_NAME,
+ uid: Int = generateUidForUser(USER_ID),
+ permissionGroupName: String = PERM_CAMERA,
+ lastAccessTimeMillis: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null
+ ): PermissionGroupUsage {
+ val usage = mock(PermissionGroupUsage::class.java)
+ `when`(usage.packageName).thenReturn(packageName)
+ `when`(usage.uid).thenReturn(uid)
+ `when`(usage.permissionGroupName).thenReturn(permissionGroupName)
+ `when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis)
+ `when`(usage.isActive).thenReturn(isActive)
+ `when`(usage.isPhoneCall).thenReturn(isPhoneCall)
+ `when`(usage.attributionTag).thenReturn(attributionTag)
+ `when`(usage.attributionLabel).thenReturn(attributionLabel)
+ `when`(usage.proxyLabel).thenReturn(proxyLabel)
+ return usage
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
new file mode 100644
index 000000000000..f4644a578d24
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.privacy
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+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.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class PrivacyDialogV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val TEST_PACKAGE_NAME = "test_pkg"
+ private const val TEST_USER_ID = 0
+ private const val TEST_PERM_GROUP = "test_perm_group"
+
+ private val TEST_INTENT = Intent("test_intent_action")
+
+ private fun createPrivacyElement(
+ type: PrivacyType = PrivacyType.TYPE_MICROPHONE,
+ packageName: String = TEST_PACKAGE_NAME,
+ userId: Int = TEST_USER_ID,
+ applicationName: CharSequence = "App",
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null,
+ lastActiveTimestamp: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ isService: Boolean = false,
+ permGroupName: String = TEST_PERM_GROUP,
+ navigationIntent: Intent = TEST_INTENT
+ ) =
+ PrivacyDialogV2.PrivacyElement(
+ type,
+ packageName,
+ userId,
+ applicationName,
+ attributionTag,
+ attributionLabel,
+ proxyLabel,
+ lastActiveTimestamp,
+ isActive,
+ isPhoneCall,
+ isService,
+ permGroupName,
+ navigationIntent
+ )
+ }
+
+ @Mock private lateinit var manageApp: (String, Int, Intent) -> Unit
+ @Mock private lateinit var closeApp: (String, Int) -> Unit
+ @Mock private lateinit var openPrivacyDashboard: () -> Unit
+ private lateinit var dialog: PrivacyDialogV2
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @After
+ fun teardown() {
+ if (this::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun testManageAppCalledWithCorrectParams() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_manage_app_button).callOnClick()
+
+ verify(manageApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, TEST_INTENT)
+ }
+
+ @Test
+ fun testCloseAppCalledWithCorrectParams() {
+ val list = listOf(createPrivacyElement(isActive = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_close_app_button).callOnClick()
+
+ verify(closeApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID)
+ }
+
+ @Test
+ fun testCloseAppMissingForService() {
+ val list = listOf(createPrivacyElement(isActive = true, isService = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.findViewById<View>(R.id.privacy_dialog_manage_app_button)).isNotNull()
+ assertThat(dialog.findViewById<View>(R.id.privacy_dialog_close_app_button)).isNull()
+ }
+
+ @Test
+ fun testMoreButton() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_more_button).callOnClick()
+
+ verify(openPrivacyDashboard).invoke()
+ }
+
+ @Test
+ fun testCloseButton() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+ verify(dismissListener, never()).onDialogDismissed()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_close_button).callOnClick()
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledOnDismiss() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+ verify(dismissListener, never()).onDialogDismissed()
+
+ dialog.dismiss()
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.show()
+ dialog.dismiss()
+
+ dialog.addOnDismissListener(dismissListener)
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testCorrectNumElements() {
+ val list =
+ listOf(
+ createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true),
+ createPrivacyElement()
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(
+ dialog.requireViewById<ViewGroup>(R.id.privacy_dialog_items_container).childCount
+ )
+ .isEqualTo(2)
+ }
+
+ @Test
+ fun testHeaderText() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_title).text)
+ .isEqualTo(TEST_PERM_GROUP)
+ }
+
+ @Test
+ fun testUsingText() {
+ val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App")
+ }
+
+ @Test
+ fun testRecentText() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used by App")
+ }
+
+ @Test
+ fun testPhoneCall() {
+ val list = listOf(createPrivacyElement(isPhoneCall = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used in phone call")
+ }
+
+ @Test
+ fun testPhoneCallNotClickable() {
+ val list = listOf(createPrivacyElement(isPhoneCall = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<View>(R.id.privacy_dialog_item_card).isClickable)
+ .isFalse()
+ assertThat(
+ dialog
+ .requireViewById<View>(R.id.privacy_dialog_item_header_expand_toggle)
+ .visibility
+ )
+ .isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testProxyLabel() {
+ val list = listOf(createPrivacyElement(proxyLabel = "proxy label"))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used by App (proxy label)")
+ }
+
+ @Test
+ fun testSubattribution() {
+ val list =
+ listOf(
+ createPrivacyElement(
+ attributionLabel = "For subattribution",
+ isActive = true,
+ isService = true
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App (For subattribution)")
+ }
+
+ @Test
+ fun testSubattributionAndProxyLabel() {
+ val list =
+ listOf(
+ createPrivacyElement(
+ attributionLabel = "For subattribution",
+ proxyLabel = "proxy label",
+ isActive = true
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App (For subattribution \u2022 proxy label)")
+ }
+
+ @Test
+ fun testDialogHasTitle() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ assertThat(dialog.window?.attributes?.title).isEqualTo("Microphone & Camera")
+ }
+
+ @Test
+ fun testDialogIsFullscreen() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ assertThat(dialog.window?.attributes?.width).isEqualTo(MATCH_PARENT)
+ assertThat(dialog.window?.attributes?.height).isEqualTo(MATCH_PARENT)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
index 3620233fc9df..fa02e8cb3e54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
@@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -24,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
@@ -54,6 +58,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
@Mock
private lateinit var privacyDialogController: PrivacyDialogController
@Mock
+ private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2
+ @Mock
private lateinit var privacyLogger: PrivacyLogger
@Mock
private lateinit var iconContainer: StatusIconContainer
@@ -69,6 +75,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
private lateinit var safetyCenterManager: SafetyCenterManager
@Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock
+ private lateinit var featureFlags: FeatureFlags
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
@@ -94,6 +102,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
uiEventLogger,
privacyChip,
privacyDialogController,
+ privacyDialogControllerV2,
privacyLogger,
iconContainer,
permissionManager,
@@ -103,7 +112,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
appOpsController,
broadcastDispatcher,
safetyCenterManager,
- deviceProvisionedController
+ deviceProvisionedController,
+ featureFlags
)
backgroundExecutor.runAllReady()
@@ -154,17 +164,55 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
}
@Test
- fun testPrivacyChipClicked() {
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
+ verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
+ any(), any(), nullable(), anyInt(), nullable())
+ receiverCaptor.value.onReceive(
+ context,
+ Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
+ )
+ backgroundExecutor.runAllReady()
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController, never()).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
}
@Test
- fun testSafetyCenterFlag() {
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
@@ -178,6 +226,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
+ verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip))
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 355c4b667333..49ece66e0cfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -29,6 +29,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -38,6 +39,7 @@ import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.recents.OverviewProxyService.ACTION_QUICKSTEP
import com.android.systemui.settings.FakeDisplayTracker
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.recents.IOverviewProxy
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK
import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP
@@ -48,11 +50,11 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.sysui.ShellInterface
import com.google.common.util.concurrent.MoreExecutors
-import dagger.Lazy
import java.util.Optional
import java.util.concurrent.Executor
import org.junit.After
@@ -81,6 +83,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker)
+ private val featureFlags = FakeFeatureFlags()
private val screenLifecycle = ScreenLifecycle(dumpManager)
private val wakefulnessLifecycle =
WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
@@ -93,6 +96,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
@Mock private lateinit var shellInterface: ShellInterface
@Mock private lateinit var navBarController: NavigationBarController
@Mock private lateinit var centralSurfaces: CentralSurfaces
+ @Mock private lateinit var shadeViewController: ShadeViewController
@Mock private lateinit var navModeController: NavigationModeController
@Mock private lateinit var statusBarWinController: NotificationShadeWindowController
@Mock private lateinit var userTracker: UserTracker
@@ -130,11 +134,13 @@ class OverviewProxyServiceTest : SysuiTestCase() {
executor,
commandQueue,
shellInterface,
- Lazy { navBarController },
- Lazy { Optional.of(centralSurfaces) },
+ { navBarController },
+ { Optional.of(centralSurfaces) },
+ { shadeViewController },
navModeController,
statusBarWinController,
sysUiState,
+ mock(),
userTracker,
screenLifecycle,
wakefulnessLifecycle,
@@ -142,6 +148,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
displayTracker,
sysuiUnlockAnimationController,
assistUtils,
+ featureFlags,
dumpManager,
unfoldTransitionProgressForwarder
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 3050c4edd24f..d2bbfa85604b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -100,4 +100,15 @@ class SceneInteractorTest : SysuiTestCase() {
)
)
}
+
+ @Test
+ fun remoteUserInput() = runTest {
+ val remoteUserInput by collectLastValue(underTest.remoteUserInput)
+ assertThat(remoteUserInput).isNull()
+
+ for (input in SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE) {
+ underTest.onRemoteUserInput(input)
+ assertThat(remoteUserInput).isEqualTo(input)
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index 5638d708cf30..6f6c5a589f44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.scene.domain.startable
+import android.view.Display
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -23,17 +26,24 @@ import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(JUnit4::class)
@@ -53,6 +63,7 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
utils.keyguardInteractor(
repository = keyguardRepository,
)
+ private val sysUiState: SysUiState = mock()
private val underTest =
SystemUiDefaultSceneContainerStartable(
@@ -61,6 +72,8 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
authenticationInteractor = authenticationInteractor,
keyguardInteractor = keyguardInteractor,
featureFlags = featureFlags,
+ sysUiState = sysUiState,
+ displayId = Display.DEFAULT_DISPLAY,
)
@Before
@@ -375,6 +388,31 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
}
+ @Test
+ fun hydrateSystemUiState() =
+ testScope.runTest {
+ underTest.start()
+ runCurrent()
+ clearInvocations(sysUiState)
+
+ listOf(
+ SceneKey.Gone,
+ SceneKey.Lockscreen,
+ SceneKey.Bouncer,
+ SceneKey.Shade,
+ SceneKey.QuickSettings,
+ )
+ .forEachIndexed { index, sceneKey ->
+ sceneInteractor.setCurrentScene(
+ SceneContainerNames.SYSTEM_UI_DEFAULT,
+ SceneModel(sceneKey),
+ )
+ runCurrent()
+
+ verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
+ }
+ }
+
private fun prepareState(
isFeatureEnabled: Boolean = true,
isDeviceUnlocked: Boolean = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 6882be7fe184..63ea918c904a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,14 +18,19 @@
package com.android.systemui.scene.ui.viewmodel
+import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,4 +73,35 @@ class SceneContainerViewModelTest : SysuiTestCase() {
underTest.setCurrentScene(SceneModel(SceneKey.Shade))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade))
}
+
+ @Test
+ fun onRemoteUserInput() = runTest {
+ val remoteUserInput by collectLastValue(underTest.remoteUserInput)
+ assertThat(remoteUserInput).isNull()
+
+ val inputs =
+ SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE.map { remoteUserInputToMotionEvent(it) }
+
+ inputs.forEachIndexed { index, input ->
+ underTest.onRemoteUserInput(input)
+ assertThat(remoteUserInput).isEqualTo(SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE[index])
+ }
+ }
+
+ private fun TestScope.remoteUserInputToMotionEvent(input: RemoteUserInput): MotionEvent {
+ return MotionEvent.obtain(
+ currentTime,
+ currentTime,
+ when (input.action) {
+ RemoteUserInputAction.DOWN -> MotionEvent.ACTION_DOWN
+ RemoteUserInputAction.MOVE -> MotionEvent.ACTION_MOVE
+ RemoteUserInputAction.UP -> MotionEvent.ACTION_UP
+ RemoteUserInputAction.CANCEL -> MotionEvent.ACTION_CANCEL
+ RemoteUserInputAction.UNKNOWN -> MotionEvent.ACTION_OUTSIDE
+ },
+ input.x,
+ input.y,
+ 0
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index ecd3308d48d9..202c7bf0d5fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -108,7 +108,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.ActivityStarter;
@@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
@Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
@Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
- @Mock protected MultiShadeInteractor mMultiShadeInteractor;
@Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
@Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock protected MotionEvent mDownMotionEvent;
@@ -615,7 +613,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mLockscreenToOccludedTransitionViewModel,
mMainDispatcher,
mKeyguardTransitionInteractor,
- () -> mMultiShadeInteractor,
mDumpManager,
mKeyuardLongPressViewModel,
mKeyguardInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 0c046e93ee20..c68095ca65a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -16,17 +16,23 @@
package com.android.systemui.shade
+import android.os.VibrationEffect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewStub
import androidx.test.filters.SmallTest
import com.android.internal.util.CollectionUtils
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.systemui.R
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -55,6 +61,9 @@ class NotificationPanelViewControllerWithCoroutinesTest :
override fun getMainDispatcher() = Dispatchers.Main.immediate
+ private val ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
+ VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false)
+
@Test
fun testDisableUserSwitcherAfterEnabling_returnsViewStubToTheViewHierarchy() = runTest {
launch(Dispatchers.Main.immediate) { givenViewAttached() }
@@ -148,6 +157,43 @@ class NotificationPanelViewControllerWithCoroutinesTest :
}
@Test
+ fun doubleTapRequired_onKeyguard_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(false)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(KEYGUARD)
+
+ listener.onAdditionalTapRequired()
+ val packageName = mView.context.packageName
+ verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+ verify(mVibratorHelper)
+ .vibrate(
+ any(),
+ eq(packageName),
+ eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
+ eq("falsing-additional-tap-required"),
+ eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
+ )
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun doubleTapRequired_onKeyguard_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(KEYGUARD)
+
+ listener.onAdditionalTapRequired()
+ verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+ verify(mVibratorHelper)
+ .performHapticFeedback(eq(mView), eq(HapticFeedbackConstants.REJECT))
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
fun testDoubleTapRequired_ShadeLocked() = runTest {
launch(Dispatchers.Main.immediate) {
val listener = getFalsingTapListener()
@@ -161,6 +207,45 @@ class NotificationPanelViewControllerWithCoroutinesTest :
}
@Test
+ fun doubleTapRequired_shadeLocked_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(false)
+ val listener = getFalsingTapListener()
+ val packageName = mView.context.packageName
+ mStatusBarStateController.setState(SHADE_LOCKED)
+
+ listener.onAdditionalTapRequired()
+ verify(mVibratorHelper)
+ .vibrate(
+ any(),
+ eq(packageName),
+ eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
+ eq("falsing-additional-tap-required"),
+ eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
+ )
+
+ verify(mTapAgainViewController).show()
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun doubleTapRequired_shadeLocked_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(SHADE_LOCKED)
+
+ listener.onAdditionalTapRequired()
+ verify(mVibratorHelper)
+ .performHapticFeedback(eq(mView), eq(HapticFeedbackConstants.REJECT))
+
+ verify(mTapAgainViewController).show()
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
fun testOnAttachRefreshStatusBarState() = runTest {
launch(Dispatchers.Main.immediate) {
mStatusBarStateController.setState(KEYGUARD)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 2a398c55560c..893123d57c99 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -34,21 +34,15 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.log.BouncerLogger
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -67,6 +61,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
@@ -80,9 +75,8 @@ import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.Optional
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -117,8 +111,9 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
- @Mock private lateinit var unfoldTransitionProgressProvider:
- Optional<UnfoldTransitionProgressProvider>
+ @Mock
+ private lateinit var unfoldTransitionProgressProvider:
+ Optional<UnfoldTransitionProgressProvider>
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
@@ -146,23 +141,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
- featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
- val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
- val multiShadeInteractor =
- MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository =
- MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- ),
- inputProxy = inputProxy,
- )
underTest =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -193,25 +176,14 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
- { multiShadeInteractor },
FakeSystemClock(),
- {
- MultiShadeMotionEventInteractor(
- applicationContext = context,
- applicationScope = testScope.backgroundScope,
- multiShadeInteractor = multiShadeInteractor,
- featureFlags = featureFlags,
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- ).keyguardTransitionInteractor,
- falsingManager = FalsingManagerFake(),
- shadeController = shadeController,
- )
- },
- BouncerMessageInteractor(FakeBouncerMessageRepository(),
- mock(BouncerMessageFactory::class.java),
- FakeUserRepository(), CountDownTimerUtil(), featureFlags),
+ BouncerMessageInteractor(
+ FakeBouncerMessageRepository(),
+ mock(BouncerMessageFactory::class.java),
+ FakeUserRepository(),
+ CountDownTimerUtil(),
+ featureFlags
+ ),
BouncerLogger(logcatLogBuffer("BouncerLog"))
)
underTest.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index d9eb9b9166b3..ed4ac35c7272 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -34,20 +34,14 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.log.BouncerLogger
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.DragDownHelper
@@ -70,7 +64,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -85,7 +78,6 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -160,22 +152,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
- featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
- val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
- val multiShadeInteractor =
- MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository =
- MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- ),
- inputProxy = inputProxy,
- )
controller =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -206,23 +186,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
- { multiShadeInteractor },
FakeSystemClock(),
- {
- MultiShadeMotionEventInteractor(
- applicationContext = context,
- applicationScope = testScope.backgroundScope,
- multiShadeInteractor = multiShadeInteractor,
- featureFlags = featureFlags,
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- )
- .keyguardTransitionInteractor,
- falsingManager = FalsingManagerFake(),
- shadeController = shadeController,
- )
- },
BouncerMessageInteractor(
FakeBouncerMessageRepository(),
Mockito.mock(BouncerMessageFactory::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 77a22ac9b092..29bc64e6249d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -29,6 +29,7 @@ import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.DozeInteractor
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.power.data.repository.FakePowerRepository
@@ -73,6 +74,8 @@ class PulsingGestureListenerTest : SysuiTestCase() {
@Mock
private lateinit var userTracker: UserTracker
@Mock
+ private lateinit var dozeInteractor: DozeInteractor
+ @Mock
private lateinit var screenOffAnimationController: ScreenOffAnimationController
private lateinit var powerRepository: FakePowerRepository
@@ -98,6 +101,7 @@ class PulsingGestureListenerTest : SysuiTestCase() {
ambientDisplayConfiguration,
statusBarStateController,
shadeLogger,
+ dozeInteractor,
userTracker,
tunerService,
dumpManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 1643e174ee13..b04d5d3d44e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -517,6 +517,21 @@ public class CommandQueueTest extends SysuiTestCase {
}
@Test
+ public void testConfirmImmersivePrompt() {
+ mCommandQueue.confirmImmersivePrompt();
+ waitForIdleSync();
+ verify(mCallbacks).confirmImmersivePrompt();
+ }
+
+ @Test
+ public void testImmersiveModeChanged() {
+ final int displayAreaId = 10;
+ mCommandQueue.immersiveModeChanged(displayAreaId, true);
+ waitForIdleSync();
+ verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+ }
+
+ @Test
public void testShowRearDisplayDialog() {
final int currentBaseState = 1;
mCommandQueue.showRearDisplayDialog(currentBaseState);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
index ad908e7f8000..aab4bc361d7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
@@ -6,6 +6,8 @@ import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.Vibrator
import android.testing.AndroidTestingRunner
+import android.view.HapticFeedbackConstants
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.eq
@@ -33,6 +35,7 @@ class VibratorHelperTest : SysuiTestCase() {
@Mock lateinit var vibrator: Vibrator
@Mock lateinit var executor: Executor
+ @Mock lateinit var view: View
@Captor lateinit var backgroundTaskCaptor: ArgumentCaptor<Runnable>
lateinit var vibratorHelper: VibratorHelper
@@ -72,6 +75,21 @@ class VibratorHelperTest : SysuiTestCase() {
}
@Test
+ fun testPerformHapticFeedback() {
+ val constant = HapticFeedbackConstants.CONFIRM
+ vibratorHelper.performHapticFeedback(view, constant)
+ verify(view).performHapticFeedback(eq(constant))
+ }
+
+ @Test
+ fun testPerformHapticFeedback_withFlags() {
+ val constant = HapticFeedbackConstants.CONFIRM
+ val flag = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+ vibratorHelper.performHapticFeedback(view, constant, flag)
+ verify(view).performHapticFeedback(eq(constant), eq(flag))
+ }
+
+ @Test
fun testHasVibrator() {
assertThat(vibratorHelper.hasVibrator()).isTrue()
verify(vibrator).hasVibrator()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 55b6be9679f2..0b2da8bfa649 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.events
import android.content.Context
import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
import android.util.Pair
import android.view.Gravity
import android.view.View
@@ -37,11 +39,14 @@ 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.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
private lateinit var controller: SystemEventChipAnimationController
@@ -159,7 +164,7 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {
assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
}
- class TestView(context: Context) : View(context), BackgroundAnimatableView {
+ private class TestView(context: Context) : View(context), BackgroundAnimatableView {
override val view: View
get() = this
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
new file mode 100644
index 000000000000..4a94dc819a9e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class GroupExpansionManagerTest : SysuiTestCase() {
+ private lateinit var gem: GroupExpansionManagerImpl
+
+ private val dumpManager: DumpManager = mock()
+ private val groupMembershipManager: GroupMembershipManager = mock()
+ private val featureFlags = FakeFeatureFlags()
+
+ private val entry1 = NotificationEntryBuilder().build()
+ private val entry2 = NotificationEntryBuilder().build()
+
+ @Before
+ fun setUp() {
+ whenever(groupMembershipManager.getGroupSummary(entry1)).thenReturn(entry1)
+ whenever(groupMembershipManager.getGroupSummary(entry2)).thenReturn(entry2)
+
+ gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags)
+ }
+
+ @Test
+ fun testNotifyOnlyOnChange_enabled() {
+ featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+ var listenerCalledCount = 0
+ gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ gem.setGroupExpanded(entry1, false)
+ Assert.assertEquals(0, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(1, listenerCalledCount)
+ gem.setGroupExpanded(entry2, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry2, false)
+ Assert.assertEquals(3, listenerCalledCount)
+ }
+
+ @Test
+ fun testNotifyOnlyOnChange_disabled() {
+ featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
+
+ var listenerCalledCount = 0
+ gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ gem.setGroupExpanded(entry1, false)
+ Assert.assertEquals(1, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry2, true)
+ Assert.assertEquals(3, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(4, listenerCalledCount)
+ gem.setGroupExpanded(entry2, false)
+ Assert.assertEquals(5, listenerCalledCount)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
index 2e68cec1fe63..4d4d319a3540 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
@@ -17,6 +17,10 @@
package com.android.systemui.statusbar.notification.row
+import android.app.Notification
+import android.net.Uri
+import android.os.UserHandle
+import android.os.UserHandle.USER_ALL
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
@@ -28,13 +32,17 @@ import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.PluginManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider
import com.android.systemui.statusbar.notification.collection.render.FakeNodeController
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
@@ -45,9 +53,9 @@ import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.SystemClock
import com.android.systemui.wmshell.BubblesManager
-import java.util.Optional
import junit.framework.Assert
import org.junit.After
import org.junit.Before
@@ -55,7 +63,10 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.util.*
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -92,10 +103,10 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
private val featureFlags: FeatureFlags = mock()
private val peopleNotificationIdentifier: PeopleNotificationIdentifier = mock()
private val bubblesManager: BubblesManager = mock()
+ private val settingsController: NotificationSettingsController = mock()
private val dragController: ExpandableNotificationRowDragController = mock()
private val dismissibilityProvider: NotificationDismissibilityProvider = mock()
private val statusBarService: IStatusBarService = mock()
-
private lateinit var controller: ExpandableNotificationRowController
@Before
@@ -132,11 +143,16 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
featureFlags,
peopleNotificationIdentifier,
Optional.of(bubblesManager),
+ settingsController,
dragController,
dismissibilityProvider,
statusBarService
)
whenever(view.childrenContainer).thenReturn(childrenContainer)
+
+ val notification = Notification.Builder(mContext).build()
+ val sbn = SbnBuilder().setNotification(notification).build()
+ whenever(view.entry).thenReturn(NotificationEntryBuilder().setSbn(sbn).build())
}
@After
@@ -204,4 +220,74 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
Mockito.verify(view).removeChildNotification(eq(childView))
Mockito.verify(listContainer).notifyGroupChildRemoved(eq(childView), eq(childrenContainer))
}
+
+ @Test
+ fun registerSettingsListener_forBubbles() {
+ controller.init(mock(NotificationEntry::class.java))
+ val viewStateObserver = withArgCaptor {
+ verify(view).addOnAttachStateChangeListener(capture());
+ }
+ viewStateObserver.onViewAttachedToWindow(view);
+ verify(settingsController).addCallback(any(), any());
+ }
+
+ @Test
+ fun unregisterSettingsListener_forBubbles() {
+ controller.init(mock(NotificationEntry::class.java))
+ val viewStateObserver = withArgCaptor {
+ verify(view).addOnAttachStateChangeListener(capture());
+ }
+ viewStateObserver.onViewDetachedFromWindow(view);
+ verify(settingsController).removeCallback(any(), any());
+ }
+
+ @Test
+ fun settingsListener_invalidUri() {
+ controller.mSettingsListener.onSettingChanged(Uri.EMPTY, view.entry.sbn.userId, "1")
+
+ verify(view, never()).getPrivateLayout()
+ }
+
+ @Test
+ fun settingsListener_invalidUserId() {
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, "1")
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, null)
+
+ verify(view, never()).getPrivateLayout()
+ }
+
+ @Test
+ fun settingsListener_validUserId() {
+ val childView: NotificationContentView = mock()
+ whenever(view.privateLayout).thenReturn(childView)
+
+ controller.mSettingsListener.onSettingChanged(
+ BUBBLES_SETTING_URI, view.entry.sbn.userId, "1")
+ verify(childView).setBubblesEnabledForUser(true)
+
+ controller.mSettingsListener.onSettingChanged(
+ BUBBLES_SETTING_URI, view.entry.sbn.userId, "9")
+ verify(childView).setBubblesEnabledForUser(false)
+ }
+
+ @Test
+ fun settingsListener_userAll() {
+ val childView: NotificationContentView = mock()
+ whenever(view.privateLayout).thenReturn(childView)
+
+ val notification = Notification.Builder(mContext).build()
+ val sbn = SbnBuilder().setNotification(notification)
+ .setUser(UserHandle.of(USER_ALL))
+ .build()
+ whenever(view.entry).thenReturn(NotificationEntryBuilder()
+ .setSbn(sbn)
+ .setUser(UserHandle.of(USER_ALL))
+ .build())
+
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 9, "1")
+ verify(childView).setBubblesEnabledForUser(true)
+
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 1, "0")
+ verify(childView).setBubblesEnabledForUser(false)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 0b90ebec3ec6..ba6c7fd50bc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -250,6 +250,9 @@ class NotificationContentViewTest : SysuiTestCase() {
.thenReturn(actionListMarginTarget)
view.setContainingNotification(mockContainingNotification)
+ // Given: controller says bubbles are enabled for the user
+ view.setBubblesEnabledForUser(true);
+
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -301,6 +304,9 @@ class NotificationContentViewTest : SysuiTestCase() {
view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+ // Given: controller says bubbles are enabled for the user
+ view.setBubblesEnabledForUser(true);
+
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should show bubble button
view.onNotificationUpdated(createMockNotificationEntry(true))
@@ -405,7 +411,6 @@ class NotificationContentViewTest : SysuiTestCase() {
val userMock: UserHandle = mock()
whenever(this.sbn).thenReturn(sbnMock)
whenever(sbnMock.user).thenReturn(userMock)
- doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
}
private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt
new file mode 100644
index 000000000000..2bccdcafbb6e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.ActivityManager
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.provider.Settings.Secure
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.notification.row.NotificationSettingsController.Listener
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.SecureSettings
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationSettingsControllerTest : SysuiTestCase() {
+
+ val setting1: String = Secure.NOTIFICATION_BUBBLES
+ val setting2: String = Secure.ACCESSIBILITY_ENABLED
+ val settingUri1: Uri = Secure.getUriFor(setting1)
+ val settingUri2: Uri = Secure.getUriFor(setting2)
+
+ @Mock
+ private lateinit var userTracker: UserTracker
+ private lateinit var handler: Handler
+ private lateinit var testableLooper: TestableLooper
+ @Mock
+ private lateinit var secureSettings: SecureSettings
+ @Mock
+ private lateinit var dumpManager: DumpManager
+
+ @Captor
+ private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
+ @Captor
+ private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
+
+ private lateinit var controller: NotificationSettingsController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+ handler = Handler(testableLooper.looper)
+ allowTestableLooperAsMainThread()
+ controller =
+ NotificationSettingsController(
+ userTracker,
+ handler,
+ secureSettings,
+ dumpManager
+ )
+ }
+
+ @After
+ fun tearDown() {
+ disallowTestableLooperAsMainThread()
+ }
+
+ @Test
+ fun creationRegistersCallbacks() {
+ verify(userTracker).addCallback(any(), any())
+ verify(dumpManager).registerNormalDumpable(anyString(), eq(controller))
+ }
+ @Test
+ fun updateContentObserverRegistration_onUserChange_noSettingsListeners() {
+ verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
+ val userCallback = userTrackerCallbackCaptor.value
+ val userId = 9
+
+ // When: User is changed
+ userCallback.onUserChanged(userId, context)
+
+ // Validate: Nothing to do, since we aren't monitoring settings
+ verify(secureSettings, never()).unregisterContentObserver(any())
+ verify(secureSettings, never()).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), any(), anyInt())
+ }
+ @Test
+ fun updateContentObserverRegistration_onUserChange_withSettingsListeners() {
+ // When: someone is listening to a setting
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+
+ verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
+ val userCallback = userTrackerCallbackCaptor.value
+ val userId = 9
+
+ // Then: User is changed
+ userCallback.onUserChanged(userId, context)
+
+ // Validate: The tracker is unregistered and re-registered with the new user
+ verify(secureSettings).unregisterContentObserver(any())
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(userId))
+ }
+
+ @Test
+ fun addCallback_onlyFirstForUriRegistersObserver() {
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), any(), anyInt())
+ }
+
+ @Test
+ fun addCallback_secondUriRegistersObserver() {
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri2,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri2), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), anyBoolean(), any(), anyInt())
+ }
+
+ @Test
+ fun removeCallback_lastUnregistersObserver() {
+ val listenerSetting1 : Listener = mock()
+ val listenerSetting2 : Listener = mock()
+ controller.addCallback(settingUri1, listenerSetting1)
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri2, listenerSetting2)
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri2), anyBoolean(), any(), anyInt())
+
+ controller.removeCallback(settingUri2, listenerSetting2)
+ verify(secureSettings, never()).unregisterContentObserver(any())
+
+ controller.removeCallback(settingUri1, listenerSetting1)
+ verify(secureSettings).unregisterContentObserver(any())
+ }
+
+ @Test
+ fun addCallback_updatesCurrentValue() {
+ whenever(secureSettings.getStringForUser(
+ setting1, ActivityManager.getCurrentUser())).thenReturn("9")
+ whenever(secureSettings.getStringForUser(
+ setting2, ActivityManager.getCurrentUser())).thenReturn("5")
+
+ val listenerSetting1a : Listener = mock()
+ val listenerSetting1b : Listener = mock()
+ val listenerSetting2 : Listener = mock()
+
+ controller.addCallback(settingUri1, listenerSetting1a)
+ controller.addCallback(settingUri1, listenerSetting1b)
+ controller.addCallback(settingUri2, listenerSetting2)
+
+ testableLooper.processAllMessages()
+
+ verify(listenerSetting1a).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting2).onSettingChanged(
+ settingUri2, ActivityManager.getCurrentUser(), "5")
+ }
+
+ @Test
+ fun removeCallback_noMoreUpdates() {
+ whenever(secureSettings.getStringForUser(
+ setting1, ActivityManager.getCurrentUser())).thenReturn("9")
+
+ val listenerSetting1a : Listener = mock()
+ val listenerSetting1b : Listener = mock()
+
+ // First, register
+ controller.addCallback(settingUri1, listenerSetting1a)
+ controller.addCallback(settingUri1, listenerSetting1b)
+ testableLooper.processAllMessages()
+
+ verify(secureSettings).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), capture(settingsObserverCaptor), anyInt())
+ verify(listenerSetting1a).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ Mockito.clearInvocations(listenerSetting1b)
+ Mockito.clearInvocations(listenerSetting1a)
+
+ // Remove one of them
+ controller.removeCallback(settingUri1, listenerSetting1a)
+
+ // On update, only remaining listener should get the callback
+ settingsObserverCaptor.value.onChange(false, settingUri1)
+ testableLooper.processAllMessages()
+
+ verify(listenerSetting1a, never()).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ }
+
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 5e0e140563cd..68f2728c9ace 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -65,6 +66,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@Mock private lateinit var keyguardViewMediator: KeyguardViewMediator
@Mock private lateinit var shadeController: ShadeController
+ @Mock private lateinit var shadeViewController: ShadeViewController
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@Mock private lateinit var lockScreenUserManager: NotificationLockscreenUserManager
@@ -91,6 +93,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
Lazy { biometricUnlockController },
Lazy { keyguardViewMediator },
Lazy { shadeController },
+ Lazy { shadeViewController },
Lazy { statusBarKeyguardViewManager },
Lazy { notifShadeWindowController },
activityLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 479803e1dfac..045a63cd44a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +40,8 @@ import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.testing.TestableResources;
+import android.view.HapticFeedbackConstants;
+import android.view.ViewRootImpl;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.util.LatencyTracker;
@@ -47,6 +50,7 @@ import com.android.keyguard.logging.BiometricUnlockLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -118,8 +122,11 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
private VibratorHelper mVibratorHelper;
@Mock
private BiometricUnlockLogger mLogger;
+ @Mock
+ private ViewRootImpl mViewRootImpl;
private final FakeSystemClock mSystemClock = new FakeSystemClock();
private BiometricUnlockController mBiometricUnlockController;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Before
public void setUp() {
@@ -143,11 +150,13 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mAuthController, mStatusBarStateController,
mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
mSystemClock,
- mStatusBarKeyguardViewManager
+ mFeatureFlags
);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mBiometricUnlockController.addListener(mBiometricUnlockEventsListener);
when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
+ when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(mViewRootImpl);
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
}
@Test
@@ -465,78 +474,59 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
- public void onSideFingerprintSuccess_dreaming_unlockThenWake() {
+ public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
+ // GIVEN side fingerprint enrolled, last wake reason was power button
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
when(mWakefulnessLifecycle.getLastWakeReason())
.thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
- final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor =
- ArgumentCaptor.forClass(Runnable.class);
- givenDreamingLocked();
- mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
- // Make sure the BiometricUnlockController has registered a callback for when the keyguard
- // is gone
- verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(
- afterKeyguardGoneRunnableCaptor.capture());
- // Ensure that the power hasn't been told to wake up yet.
- verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
- // Check that the keyguard has been told to unlock.
- verify(mKeyguardViewMediator).onWakeAndUnlocking();
+ // GIVEN last wake time was 500ms ago
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+ mSystemClock.advanceTime(500);
- // Simulate the keyguard disappearing.
- afterKeyguardGoneRunnableCaptor.getValue().run();
- // Verify that the power manager has been told to wake up now.
- verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).vibrateAuthSuccess(anyString());
}
@Test
- public void onSideFingerprintSuccess_dreaming_unlockIfStillLockedNotDreaming() {
+ public void onSideFingerprintSuccess_oldPowerButtonPress_playOneWayHaptic() {
+ // GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, last wake reason was power button
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
when(mWakefulnessLifecycle.getLastWakeReason())
.thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
- final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor =
- ArgumentCaptor.forClass(Runnable.class);
- givenDreamingLocked();
- mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
-
- // Make sure the BiometricUnlockController has registered a callback for when the keyguard
- // is gone
- verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(
- afterKeyguardGoneRunnableCaptor.capture());
- // Ensure that the power hasn't been told to wake up yet.
- verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
- // Check that the keyguard has been told to unlock.
- verify(mKeyguardViewMediator).onWakeAndUnlocking();
-
- when(mUpdateMonitor.isDreaming()).thenReturn(false);
- when(mKeyguardStateController.isUnlocked()).thenReturn(false);
- // Simulate the keyguard disappearing.
- afterKeyguardGoneRunnableCaptor.getValue().run();
-
- final ArgumentCaptor<Runnable> dismissKeyguardRunnableCaptor =
- ArgumentCaptor.forClass(Runnable.class);
- verify(mHandler).post(dismissKeyguardRunnableCaptor.capture());
+ // GIVEN last wake time was 500ms ago
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+ mSystemClock.advanceTime(500);
- // Verify that the power manager was not told to wake up.
- verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
- dismissKeyguardRunnableCaptor.getValue().run();
- // Verify that the keyguard controller is told to unlock.
- verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
+ // THEN vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.CONFIRM)
+ );
}
-
@Test
- public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
- // GIVEN side fingerprint enrolled, last wake reason was power button
+ public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() {
+ // GIVEN side fingerprint enrolled, wakeup just happened
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
- when(mWakefulnessLifecycle.getLastWakeReason())
- .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
-
- // GIVEN last wake time was 500ms ago
when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
- mSystemClock.advanceTime(500);
+
+ // GIVEN last wake reason was from a gesture
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_GESTURE);
// WHEN biometric fingerprint succeeds
givenFingerprintModeUnlockCollapsing();
@@ -548,7 +538,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
- public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() {
+ public void onSideFingerprintSuccess_recentGestureWakeUp_playOnewayHaptic() {
+ //GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
// GIVEN side fingerprint enrolled, wakeup just happened
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
@@ -563,7 +555,10 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
true);
// THEN vibrate the device
- verify(mVibratorHelper).vibrateAuthSuccess(anyString());
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.CONFIRM)
+ );
}
@Test
@@ -582,6 +577,26 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintFail_alwaysPlaysOneWayHaptic() {
+ // GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, last wake reason was recent power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // WHEN biometric fingerprint fails
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+
+ // THEN always vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.REJECT)
+ );
+ }
+
+ @Test
public void onFingerprintDetect_showBouncer() {
// WHEN fingerprint detect occurs
mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT,
@@ -601,14 +616,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean());
}
- private void givenDreamingLocked() {
- when(mUpdateMonitor.isDreaming()).thenReturn(true);
- when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
- }
-
private void givenFingerprintModeUnlockCollapsing() {
when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
when(mKeyguardStateController.isShowing()).thenReturn(true);
}
+
+ private void givenDreamingLocked() {
+ when(mUpdateMonitor.isDreaming()).thenReturn(true);
+ when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+ }
+ @Test
+ public void onSideFingerprintSuccess_dreaming_unlockNoWake() {
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+ givenDreamingLocked();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
+ verify(mKeyguardViewMediator).onWakeAndUnlocking();
+ // Ensure that the power hasn't been told to wake up yet.
+ verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index c8ec1bf4af9f..7de0075c45ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.shade.ShadeControllerImpl
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -50,6 +51,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.util.Optional
+import javax.inject.Provider
@SmallTest
class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@@ -73,6 +75,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Mock
private lateinit var shadeControllerImpl: ShadeControllerImpl
@Mock
+ private lateinit var sceneInteractor: Provider<SceneInteractor>
+ @Mock
private lateinit var shadeLogger: ShadeLogger
@Mock
private lateinit var viewUtil: ViewUtil
@@ -140,8 +144,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isViewEnabled).thenReturn(false)
val returnVal = view.onTouchEvent(
MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
@@ -152,8 +154,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isViewEnabled).thenReturn(false)
val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
@@ -165,8 +165,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isViewEnabled).thenReturn(true)
val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
@@ -178,8 +176,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isFullyCollapsed).thenReturn(true)
val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
@@ -205,6 +201,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
centralSurfacesImpl,
shadeControllerImpl,
shadeViewController,
+ sceneInteractor,
shadeLogger,
viewUtil,
configurationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index 2e9a6909e402..e76f26d8128e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
@@ -97,9 +97,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() {
powerManager,
handler = handler
)
- controller.initialize(centralSurfaces, lightRevealScrim)
- `when`(centralSurfaces.shadeViewController).thenReturn(
- shadeViewController)
+ controller.initialize(centralSurfaces, shadeViewController, lightRevealScrim)
// Screen off does not run if the panel is expanded, so we should say it's collapsed to test
// screen off.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
index 5bc98e0d19af..dbaa29bb3688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -18,7 +18,9 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
@@ -43,6 +45,7 @@ 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.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@@ -50,6 +53,8 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class WifiRepositorySwitcherTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositorySwitcher
private lateinit var realImpl: WifiRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index 9cf08c03b5d1..206ac1d37074 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -16,15 +16,20 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class DisabledWifiRepositoryTest : SysuiTestCase() {
private lateinit var underTest: DisabledWifiRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 7007345c175c..3cf5f5249f1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -32,7 +32,9 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
import android.net.wifi.WifiManager.UNKNOWN_SSID
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
@@ -57,6 +59,7 @@ 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.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -65,6 +68,8 @@ import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class WifiRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index 813597a8b576..7f990a446aaf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -101,7 +101,6 @@ class FoldAodAnimationControllerTest : SysuiTestCase() {
whenever(viewGroup.viewTreeObserver).thenReturn(viewTreeObserver)
whenever(wakefulnessLifecycle.lastSleepReason)
.thenReturn(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD)
- whenever(centralSurfaces.shadeViewController).thenReturn(shadeViewController)
whenever(shadeFoldAnimator.startFoldToAodAnimation(any(), any(), any())).then {
val onActionStarted = it.arguments[0] as Runnable
onActionStarted.run()
@@ -124,7 +123,7 @@ class FoldAodAnimationControllerTest : SysuiTestCase() {
latencyTracker,
{ keyguardInteractor },
)
- .apply { initialize(centralSurfaces, lightRevealScrim) }
+ .apply { initialize(centralSurfaces, shadeViewController, lightRevealScrim) }
verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 820e2a031a0e..2f228a8da0c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -158,6 +158,7 @@ import com.android.wm.shell.transition.Transitions;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -173,6 +174,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Optional;
+@Ignore("b/292153259")
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
index 49ac64c58cb8..2ef1be70000f 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
@@ -15,12 +15,17 @@
*
*/
-package com.android.systemui.multishade.shared.model
+package com.android.systemui.biometrics.data.repository
-import androidx.annotation.FloatRange
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
-/** Models the current state of a shade. */
-data class ShadeModel(
- val id: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
-)
+class FakeFacePropertyRepository : FacePropertyRepository {
+ private val faceSensorInfo = MutableStateFlow<FaceSensorInfo?>(null)
+ override val sensorInfo: Flow<FaceSensorInfo?>
+ get() = faceSensorInfo
+
+ fun setSensorInfo(value: FaceSensorInfo?) {
+ faceSensorInfo.value = value
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
index 0c5e43809fab..2362a5241244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
@@ -20,12 +20,16 @@ import android.hardware.biometrics.SensorLocationInternal
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
+ private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val isInitialized = _isInitialized.asStateFlow()
+
private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
- override val sensorId = _sensorId.asStateFlow()
+ override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
private val _strength: MutableStateFlow<SensorStrength> =
MutableStateFlow(SensorStrength.CONVENIENCE)
@@ -33,11 +37,12 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
private val _sensorType: MutableStateFlow<FingerprintSensorType> =
MutableStateFlow(FingerprintSensorType.UNKNOWN)
- override val sensorType = _sensorType.asStateFlow()
+ override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
- override val sensorLocations = _sensorLocations.asStateFlow()
+ override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+ _sensorLocations.asStateFlow()
fun setProperties(
sensorId: Int,
@@ -49,5 +54,6 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
_strength.value = strength
_sensorType.value = sensorType
_sensorLocations.value = sensorLocations
+ _isInitialized.value = true
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index 4aaf3478a31d..8c98aea6a990 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -59,7 +59,6 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0))
override val authenticationFlags: Flow<AuthenticationFlags>
get() = _authFlags
-
fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
_isFingerprintEnrolled.value = isFingerprintEnrolled
}
@@ -110,4 +109,8 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
fun setIsNonStrongBiometricAllowed(value: Boolean) {
_isNonStrongBiometricAllowed.value = value
}
+
+ fun setIsStrongBiometricAllowed(value: Boolean) {
+ _isStrongBiometricAllowed.value = value
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 548169e6cccd..f4c2db1b944e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -27,6 +27,11 @@ import kotlinx.coroutines.flow.filterNotNull
class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
+ private var _wasDisabled: Boolean = false
+
+ val wasDisabled: Boolean
+ get() = _wasDisabled
+
override val isAuthenticated = MutableStateFlow(false)
override val canRunFaceAuth = MutableStateFlow(false)
private val _authenticationStatus = MutableStateFlow<FaceAuthenticationStatus?>(null)
@@ -52,6 +57,9 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning
override val isBypassEnabled = MutableStateFlow(false)
+ override fun lockoutFaceAuth() {
+ _wasDisabled = true
+ }
override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) {
_runningAuthRequest.value = uiEvent to fallbackToDetection
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
index 7c22604dc546..b24b95e0d3d7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository
import com.android.systemui.statusbar.LightRevealEffect
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
/** Fake implementation of [LightRevealScrimRepository] */
@@ -30,4 +31,15 @@ class FakeLightRevealScrimRepository : LightRevealScrimRepository {
fun setRevealEffect(effect: LightRevealEffect) {
_revealEffect.tryEmit(effect)
}
+
+ private val _revealAmount: MutableStateFlow<Float> = MutableStateFlow(0.0f)
+ override val revealAmount: Flow<Float> = _revealAmount
+
+ override fun startRevealAmountAnimator(reveal: Boolean) {
+ if (reveal) {
+ _revealAmount.value = 1.0f
+ } else {
+ _revealAmount.value = 0.0f
+ }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 931798130499..f39982f54441 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -38,6 +38,8 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
@@ -217,5 +219,14 @@ class SceneTestUtils(
companion object {
const val CONTAINER_1 = SceneContainerNames.SYSTEM_UI_DEFAULT
const val CONTAINER_2 = "container2"
+
+ val REMOTE_INPUT_DOWN_GESTURE =
+ listOf(
+ RemoteUserInput(10f, 10f, RemoteUserInputAction.DOWN),
+ RemoteUserInput(10f, 20f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 30f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 40f, RemoteUserInputAction.UP),
+ )
}
}
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 6fd6afed49b9..754a7ede8006 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -159,9 +159,10 @@ public class PersistentDataBlockService extends SystemService {
private int getAllowedUid() {
final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
- final int mainUserId = umInternal.getMainUserId();
+ int mainUserId = umInternal.getMainUserId();
if (mainUserId < 0) {
- return -1;
+ // If main user is not defined. Use the SYSTEM user instead.
+ mainUserId = UserHandle.USER_SYSTEM;
}
String allowedPackage = mContext.getResources()
.getString(R.string.config_persistentDataPackageName);
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 7d46c0cd075f..d959de33d3e9 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8166,7 +8166,13 @@ public final class ActiveServices {
mAm.getUidStateLocked(r.mRecentCallingUid),
mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid),
0,
- 0);
+ 0,
+ r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ r.mAllowWIUInBindService,
+ r.mAllowWIUByBindings,
+ r.mAllowStartForegroundNoBinding,
+ r.mAllowStartInBindService,
+ r.mAllowStartByBindings);
int event = 0;
if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) {
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index 4f5b5e1fbd68..786e1cc7075f 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -506,7 +506,13 @@ public class ForegroundServiceTypeLoggerModule {
ActivityManager.PROCESS_STATE_UNKNOWN,
ActivityManager.PROCESS_CAPABILITY_NONE,
apiDurationBeforeFgsStart,
- apiDurationAfterFgsEnd);
+ apiDurationAfterFgsEnd,
+ r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ r.mAllowWIUInBindService,
+ r.mAllowWIUByBindings,
+ r.mAllowStartForegroundNoBinding,
+ r.mAllowStartInBindService,
+ r.mAllowStartByBindings);
}
/**
@@ -557,7 +563,13 @@ public class ForegroundServiceTypeLoggerModule {
ActivityManager.PROCESS_STATE_UNKNOWN,
ActivityManager.PROCESS_CAPABILITY_NONE,
0,
- apiDurationAfterFgsEnd);
+ apiDurationAfterFgsEnd,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0);
}
/**
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 54d1faa39be0..3d0ea9d8bef6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -260,14 +260,6 @@ class FingerprintAuthenticationClient
final AidlSession session = getFreshDaemon();
final OperationContextExt opContext = getOperationContext();
- final ICancellationSignal cancel;
- if (session.hasContextMethods()) {
- cancel = session.getSession().authenticateWithContext(
- mOperationId, opContext.toAidlContext(getOptions()));
- } else {
- cancel = session.getSession().authenticate(mOperationId);
- }
-
getBiometricContext().subscribe(opContext, ctx -> {
if (session.hasContextMethods()) {
try {
@@ -289,7 +281,12 @@ class FingerprintAuthenticationClient
mALSProbeCallback.getProbe().enable();
}
- return cancel;
+ if (session.hasContextMethods()) {
+ return session.getSession().authenticateWithContext(
+ mOperationId, opContext.toAidlContext(getOptions()));
+ } else {
+ return session.getSession().authenticate(mOperationId);
+ }
}
@Override
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3b779ecf77e5..626502ef07b4 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3151,11 +3151,6 @@ public final class DisplayManagerService extends SystemService {
// with the corresponding displaydevice.
HighBrightnessModeMetadata hbmMetadata =
mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
- if (hbmMetadata == null) {
- Slog.wtf(TAG, "High Brightness Mode Metadata is null in DisplayManagerService for "
- + "display: " + display.getDisplayIdLocked());
- return null;
- }
if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) {
displayPowerController = new DisplayPowerController2(
mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 5b9bed7aa4b4..59b887171506 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -450,6 +450,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
private float[] mNitsRange;
private final BrightnessRangeController mBrightnessRangeController;
+
+ @Nullable
private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
private final BrightnessThrottler mBrightnessThrottler;
@@ -722,7 +724,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
setUpAutoBrightness(resources, handler);
- mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic();
+ mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic()
+ && !resources.getBoolean(
+ com.android.internal.R.bool.config_displayColorFadeDisabled);
mColorFadeFadesConfig = resources.getBoolean(
com.android.internal.R.bool.config_animateScreenLights);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 2d5cd0b633c4..88fc1fb97dc8 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -611,7 +611,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
setUpAutoBrightness(resources, handler);
- mColorFadeEnabled = mInjector.isColorFadeEnabled();
+ mColorFadeEnabled = mInjector.isColorFadeEnabled()
+ && !resources.getBoolean(
+ com.android.internal.R.bool.config_displayColorFadeDisabled);
mColorFadeFadesConfig = resources.getBoolean(
R.bool.config_animateScreenLights);
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 11160a532609..c04c2793b3c5 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import android.annotation.Nullable;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
@@ -75,6 +76,8 @@ class HighBrightnessModeController {
private final Injector mInjector;
private HdrListener mHdrListener;
+
+ @Nullable
private HighBrightnessModeData mHbmData;
private HdrBrightnessDeviceConfig mHdrBrightnessCfg;
private IBinder mRegisteredDisplayToken;
@@ -107,7 +110,9 @@ class HighBrightnessModeController {
* If HBM is currently running, this is the start time and set of all events,
* for the current HBM session.
*/
- private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null;
+ @Nullable
+ private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+
HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
String displayUniqueId, float brightnessMin, float brightnessMax,
HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
@@ -310,23 +315,29 @@ class HighBrightnessModeController {
pw.println(" mBrightnessMax=" + mBrightnessMax);
pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis()));
pw.println(" mIsTimeAvailable= " + mIsTimeAvailable);
- pw.println(" mRunningStartTimeMillis="
- + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
pw.println(" width*height=" + mWidth + "*" + mHeight);
- pw.println(" mEvents=");
- final long currentTime = mClock.uptimeMillis();
- long lastStartTime = currentTime;
- long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
- if (runningStartTimeMillis != -1) {
- lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
- }
- for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
- if (lastStartTime > event.getEndTimeMillis()) {
- pw.println(" event: [normal brightness]: "
- + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
+
+ if (mHighBrightnessModeMetadata != null) {
+ pw.println(" mRunningStartTimeMillis="
+ + TimeUtils.formatUptime(
+ mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
+ pw.println(" mEvents=");
+ final long currentTime = mClock.uptimeMillis();
+ long lastStartTime = currentTime;
+ long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+ if (runningStartTimeMillis != -1) {
+ lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
}
- lastStartTime = dumpHbmEvent(pw, event);
+ for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
+ if (lastStartTime > event.getEndTimeMillis()) {
+ pw.println(" event: [normal brightness]: "
+ + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
+ }
+ lastStartTime = dumpHbmEvent(pw, event);
+ }
+ } else {
+ pw.println(" mHighBrightnessModeMetadata=null");
}
}
@@ -353,7 +364,7 @@ class HighBrightnessModeController {
}
private boolean deviceSupportsHbm() {
- return mHbmData != null;
+ return mHbmData != null && mHighBrightnessModeMetadata != null;
}
private long calculateRemainingTime(long currentTime) {
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
index 76702d3f6f8c..9e6f0eb93831 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
@@ -41,6 +41,9 @@ class HighBrightnessModeMetadataMapper {
+ display.getDisplayIdLocked());
return null;
}
+ if (device.getDisplayDeviceConfig().getHighBrightnessModeData() == null) {
+ return null;
+ }
final String uniqueId = device.getUniqueId();
diff --git a/services/core/java/com/android/server/display/TEST_MAPPING b/services/core/java/com/android/server/display/TEST_MAPPING
index c4a566fd7b62..5e4e27069fb1 100644
--- a/services/core/java/com/android/server/display/TEST_MAPPING
+++ b/services/core/java/com/android/server/display/TEST_MAPPING
@@ -1,15 +1,7 @@
{
"presubmit": [
{
- "name": "FrameworksMockingServicesTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"}
- ]
- },
- {
- "name": "FrameworksServicesTests",
+ "name": "DisplayServiceTests",
"options": [
{"include-filter": "com.android.server.display"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
@@ -31,5 +23,14 @@
}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "DisplayServiceTests",
+ "options": [
+ {"include-filter": "com.android.server.display"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
]
-} \ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 3a8f9d5c35e6..5c80291f7f46 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -116,6 +116,7 @@ import com.android.server.DisplayThread;
import com.android.server.LocalServices;
import com.android.server.Watchdog;
import com.android.server.input.InputManagerInternal.LidSwitchCallback;
+import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
import libcore.io.IoUtils;
@@ -165,6 +166,8 @@ public class InputManagerService extends IInputManager.Stub
private final InputManagerHandler mHandler;
private DisplayManagerInternal mDisplayManagerInternal;
+ private InputMethodManagerInternal mInputMethodManagerInternal;
+
// Context cache used for loading pointer resources.
private Context mPointerIconDisplayContext;
@@ -510,6 +513,8 @@ public class InputManagerService extends IInputManager.Stub
}
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+ mInputMethodManagerInternal =
+ LocalServices.getService(InputMethodManagerInternal.class);
mSettingsObserver.registerAndUpdate();
@@ -2790,6 +2795,13 @@ public class InputManagerService extends IInputManager.Stub
yPosition)).sendToTarget();
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ boolean isInputMethodConnectionActive() {
+ return mInputMethodManagerInternal != null
+ && mInputMethodManagerInternal.isAnyInputConnectionActive();
+ }
+
/**
* Callback interface implemented by the Window Manager.
*/
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 8c7658e53dcd..08503cb2e9f8 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -186,6 +186,12 @@ public abstract class InputMethodManagerInternal {
public abstract void switchKeyboardLayout(int direction);
/**
+ * Returns true if any InputConnection is currently active.
+ * {@hide}
+ */
+ public abstract boolean isAnyInputConnectionActive();
+
+ /**
* Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing.
*/
private static final InputMethodManagerInternal NOP =
@@ -268,6 +274,11 @@ public abstract class InputMethodManagerInternal {
@Override
public void switchKeyboardLayout(int direction) {
}
+
+ @Override
+ public boolean isAnyInputConnectionActive() {
+ return false;
+ }
};
/**
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index c5fbcb968ab6..cfcb4620bf25 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5937,6 +5937,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
}
}
}
+
+ /**
+ * Returns true if any InputConnection is currently active.
+ */
+ @Override
+ public boolean isAnyInputConnectionActive() {
+ return mCurInputConnection != null;
+ }
}
@BinderThread
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 398e470d9fda..61d0afeb7717 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -117,6 +117,10 @@ public final class MediaProjectionManagerService extends SystemService
// WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService
// See mediaprojection.md
private final Object mLock = new Object();
+ // A handler for posting tasks that must interact with a service holding another lock,
+ // especially for services that will eventually acquire the WindowManager lock.
+ @NonNull private final Handler mHandler;
+
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
@@ -145,6 +149,8 @@ public final class MediaProjectionManagerService extends SystemService
super(context);
mContext = context;
mInjector = injector;
+ // Post messages on the main thread; no need for a separate thread.
+ mHandler = new Handler(Looper.getMainLooper());
mClock = injector.createClock();
mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
mCallbackDelegate = new CallbackDelegate(injector.createCallbackLooper());
@@ -243,14 +249,17 @@ public final class MediaProjectionManagerService extends SystemService
if (!mProjectionGrant.requiresForegroundService()) {
return;
}
+ }
- if (mActivityManagerInternal.hasRunningForegroundService(
- uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
- // If there is any process within this UID running a FGS
- // with the mediaProjection type, that's Okay.
- return;
- }
+ // Run outside the lock when calling into ActivityManagerService.
+ if (mActivityManagerInternal.hasRunningForegroundService(
+ uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
+ // If there is any process within this UID running a FGS
+ // with the mediaProjection type, that's Okay.
+ return;
+ }
+ synchronized (mLock) {
mProjectionGrant.stop();
}
}
@@ -867,7 +876,6 @@ public final class MediaProjectionManagerService extends SystemService
mTargetSdkVersion = targetSdkVersion;
mIsPrivileged = isPrivileged;
mCreateTimeMs = mClock.uptimeMillis();
- // TODO(b/267740338): Add unit test.
mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
MEDIA_PROJECTION_TOKEN_EVENT_CREATED);
}
@@ -924,6 +932,10 @@ public final class MediaProjectionManagerService extends SystemService
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
+ // Cache result of calling into ActivityManagerService outside of the lock, to prevent
+ // deadlock with WindowManagerService.
+ final boolean hasFGS = mActivityManagerInternal.hasRunningForegroundService(
+ uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
synchronized (mLock) {
if (isCurrentProjection(asBinder())) {
Slog.w(TAG, "UID " + Binder.getCallingUid()
@@ -935,9 +947,7 @@ public final class MediaProjectionManagerService extends SystemService
}
if (REQUIRE_FG_SERVICE_FOR_PROJECTION
- && requiresForegroundService()
- && !mActivityManagerInternal.hasRunningForegroundService(
- uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
+ && requiresForegroundService() && !hasFGS) {
throw new SecurityException("Media projections require a foreground service"
+ " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION");
}
@@ -1026,10 +1036,11 @@ public final class MediaProjectionManagerService extends SystemService
mToken = null;
unregisterCallback(mCallback);
mCallback = null;
- // TODO(b/267740338): Add unit test.
- mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
- MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED);
}
+ // Run on a separate thread, to ensure no lock is held when calling into
+ // ActivityManagerService.
+ mHandler.post(() -> mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
+ MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED));
}
@Override // Binder call
diff --git a/services/core/java/com/android/server/media/projection/mediaprojection.md b/services/core/java/com/android/server/media/projection/mediaprojection.md
index bccdf3411903..34e7ecc6c6c5 100644
--- a/services/core/java/com/android/server/media/projection/mediaprojection.md
+++ b/services/core/java/com/android/server/media/projection/mediaprojection.md
@@ -11,6 +11,11 @@ Calls must follow the below invocation order while holding locks:
`WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService`
+`MediaProjectionManagerService` should never lock when calling into a service that may acquire
+the `WindowManagerService` global lock (for example,
+`MediaProjectionManagerService -> ActivityManagerService` may result in deadlock, since
+`ActivityManagerService -> WindowManagerService`).
+
### Justification
`MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 37879effc41c..c2b21beb10df 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1020,6 +1020,7 @@ public class NotificationManagerService extends SystemService {
}
mAssistants.resetDefaultAssistantsIfNecessary();
+ mPreferencesHelper.syncChannelsBypassingDnd();
}
@VisibleForTesting
@@ -1859,6 +1860,7 @@ public class NotificationManagerService extends SystemService {
mConditionProviders.onUserSwitched(userId);
mListeners.onUserSwitched(userId);
mZenModeHelper.onUserSwitched(userId);
+ mPreferencesHelper.syncChannelsBypassingDnd();
}
// assistant is the only thing that cares about managed profiles specifically
mAssistants.onUserSwitched(userId);
@@ -2489,6 +2491,16 @@ public class NotificationManagerService extends SystemService {
getContext().registerReceiver(mReviewNotificationPermissionsReceiver,
ReviewNotificationPermissionsReceiver.getFilter(),
Context.RECEIVER_NOT_EXPORTED);
+
+ mAppOps.startWatchingMode(AppOpsManager.OP_POST_NOTIFICATION, null,
+ new AppOpsManager.OnOpChangedInternalListener() {
+ @Override
+ public void onOpChanged(@NonNull String op, @NonNull String packageName,
+ int userId) {
+ mHandler.post(
+ () -> handleNotificationPermissionChange(packageName, userId));
+ }
+ });
}
/**
@@ -3279,6 +3291,11 @@ public class NotificationManagerService extends SystemService {
return new MultiRateLimiter.Builder(getContext()).addRateLimits(TOAST_RATE_LIMITS).build();
}
+ protected int checkComponentPermission(String permission, int uid, int owningUid,
+ boolean exported) {
+ return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+ }
+
@VisibleForTesting
final IBinder mService = new INotificationManager.Stub() {
// Toasts
@@ -3555,13 +3572,9 @@ public class NotificationManagerService extends SystemService {
.setPackageName(pkg)
.setSubtype(enabled ? 1 : 0));
mNotificationChannelLogger.logAppNotificationsAllowed(uid, pkg, enabled);
- // Now, cancel any outstanding notifications that are part of a just-disabled app
- if (!enabled) {
- cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0,
- UserHandle.getUserId(uid), REASON_PACKAGE_BANNED);
- }
- handleSavePolicyFile();
+ // Outstanding notifications from this package will be cancelled as soon as we get the
+ // callback from AppOpsManager.
}
/**
@@ -5236,10 +5249,11 @@ public class NotificationManagerService extends SystemService {
}
private boolean checkPolicyAccess(String pkg) {
+ final int uid;
try {
- int uid = getContext().getPackageManager().getPackageUidAsUser(pkg,
+ uid = getContext().getPackageManager().getPackageUidAsUser(pkg,
UserHandle.getCallingUserId());
- if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
+ if (PackageManager.PERMISSION_GRANTED == checkComponentPermission(
android.Manifest.permission.MANAGE_NOTIFICATIONS, uid,
-1, true)) {
return true;
@@ -5250,8 +5264,8 @@ public class NotificationManagerService extends SystemService {
//TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
return checkPackagePolicyAccess(pkg)
|| mListeners.isComponentEnabledForPackage(pkg)
- || (mDpm != null && (mDpm.isActiveProfileOwner(Binder.getCallingUid())
- || mDpm.isActiveDeviceOwner(Binder.getCallingUid())));
+ || (mDpm != null && (mDpm.isActiveProfileOwner(uid)
+ || mDpm.isActiveDeviceOwner(uid)));
}
@Override
@@ -5884,6 +5898,23 @@ public class NotificationManagerService extends SystemService {
}
};
+ private void handleNotificationPermissionChange(String pkg, @UserIdInt int userId) {
+ if (!mUmInternal.isUserInitialized(userId)) {
+ return; // App-op "updates" are sent when starting a new user the first time.
+ }
+ int uid = mPackageManagerInternal.getPackageUid(pkg, 0, userId);
+ if (uid == INVALID_UID) {
+ Log.e(TAG, String.format("No uid found for %s, %s!", pkg, userId));
+ return;
+ }
+ boolean hasPermission = mPermissionHelper.hasPermission(uid);
+ if (!hasPermission) {
+ cancelAllNotificationsInt(MY_UID, MY_PID, pkg, /* channelId= */ null,
+ /* mustHaveFlags= */ 0, /* mustNotHaveFlags= */ 0, userId,
+ REASON_PACKAGE_BANNED);
+ }
+ }
+
protected void checkNotificationListenerAccess() {
if (!isCallerSystemOrPhone()) {
getContext().enforceCallingPermission(
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 1818d25ae59e..1bb10929135c 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -202,7 +202,7 @@ public class PreferencesHelper implements RankingConfig {
private SparseBooleanArray mLockScreenShowNotifications;
private SparseBooleanArray mLockScreenPrivateNotifications;
private boolean mIsMediaNotificationFilteringEnabled = DEFAULT_MEDIA_NOTIFICATION_FILTERING;
- private boolean mAreChannelsBypassingDnd;
+ private boolean mCurrentUserHasChannelsBypassingDnd;
private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
private boolean mShowReviewPermissionsNotification;
@@ -230,7 +230,6 @@ public class PreferencesHelper implements RankingConfig {
updateBadgingEnabled();
updateBubblesEnabled();
updateMediaNotificationFilteringEnabled();
- syncChannelsBypassingDnd(Process.SYSTEM_UID, true); // init comes from system
}
public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
@@ -893,7 +892,7 @@ public class PreferencesHelper implements RankingConfig {
r.groups.put(group.getId(), group);
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
}
@@ -972,7 +971,7 @@ public class PreferencesHelper implements RankingConfig {
existing.setBypassDnd(bypassDnd);
needsPolicyFileChange = true;
- if (bypassDnd != mAreChannelsBypassingDnd
+ if (bypassDnd != mCurrentUserHasChannelsBypassingDnd
|| previousExistingImportance != existing.getImportance()) {
needsDndChange = true;
}
@@ -1031,7 +1030,7 @@ public class PreferencesHelper implements RankingConfig {
}
r.channels.put(channel.getId(), channel);
- if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
+ if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) {
needsDndChange = true;
}
MetricsLogger.action(getChannelLog(channel, pkg).setType(
@@ -1041,7 +1040,7 @@ public class PreferencesHelper implements RankingConfig {
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return needsPolicyFileChange;
@@ -1127,14 +1126,14 @@ public class PreferencesHelper implements RankingConfig {
// relevantly affected without the parent channel already having been.
}
- if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd
+ if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd
|| channel.getImportance() != updatedChannel.getImportance()) {
needsDndChange = true;
changed = true;
}
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
if (changed) {
updateConfig();
@@ -1321,7 +1320,7 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (channelBypassedDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannel;
}
@@ -1538,7 +1537,7 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (groupBypassedDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannels;
}
@@ -1685,8 +1684,8 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
- if (!deletedChannelIds.isEmpty() && mAreChannelsBypassingDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannelIds;
}
@@ -1788,21 +1787,28 @@ public class PreferencesHelper implements RankingConfig {
}
/**
- * Syncs {@link #mAreChannelsBypassingDnd} with the current user's notification policy before
- * updating
+ * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification
+ * policy before updating. Must be called:
+ * <ul>
+ * <li>On system init, after channels and DND configurations are loaded.</li>
+ * <li>When the current user changes, after the corresponding DND config is loaded.</li>
+ * </ul>
*/
- private void syncChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
- mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
- & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1;
+ void syncChannelsBypassingDnd() {
+ mCurrentUserHasChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
+ & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID,
+ /* fromSystemOrSystemUi= */ true);
}
/**
- * Updates the user's NotificationPolicy based on whether the current userId
- * has channels bypassing DND
+ * Updates the user's NotificationPolicy based on whether the current userId has channels
+ * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
+ * when the current user is switched.
*/
- private void updateChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
+ private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
+ boolean fromSystemOrSystemUi) {
ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
final int currentUserId = getCurrentUser();
@@ -1817,7 +1823,7 @@ public class PreferencesHelper implements RankingConfig {
for (NotificationChannel channel : r.channels.values()) {
if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) {
- candidatePkgs.add(new Pair(r.pkg, r.uid));
+ candidatePkgs.add(new Pair<>(r.pkg, r.uid));
break;
}
}
@@ -1830,9 +1836,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
boolean haveBypassingApps = candidatePkgs.size() > 0;
- if (mAreChannelsBypassingDnd != haveBypassingApps) {
- mAreChannelsBypassingDnd = haveBypassingApps;
- updateZenPolicy(mAreChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
+ if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
+ mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
+ updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
}
}
@@ -1869,7 +1875,7 @@ public class PreferencesHelper implements RankingConfig {
}
public boolean areChannelsBypassingDnd() {
- return mAreChannelsBypassingDnd;
+ return mCurrentUserHasChannelsBypassingDnd;
}
/**
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 50f1673cae44..fbd54555dbbf 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -110,6 +110,7 @@ import android.app.backup.IBackupManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.DataLoaderType;
@@ -119,7 +120,6 @@ import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
-import android.content.pm.ResolveInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.Signature;
import android.content.pm.SigningDetails;
@@ -184,7 +184,9 @@ import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.SharedLibraryWrapper;
import com.android.server.pm.pkg.component.ComponentMutateUtils;
+import com.android.server.pm.pkg.component.ParsedActivity;
import com.android.server.pm.pkg.component.ParsedInstrumentation;
+import com.android.server.pm.pkg.component.ParsedIntentInfo;
import com.android.server.pm.pkg.component.ParsedPermission;
import com.android.server.pm.pkg.component.ParsedPermissionGroup;
import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
@@ -207,6 +209,7 @@ import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -3925,23 +3928,6 @@ final class InstallPackageHelper {
}
}
- // If this is a system app we hadn't seen before, and this is a first boot or OTA,
- // we need to unstop it if it doesn't have a launcher entry.
- if (mPm.mShouldStopSystemPackagesByDefault && scanResult.mRequest.mPkgSetting == null
- && ((scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0)
- && ((scanFlags & SCAN_AS_SYSTEM) != 0)) {
- final Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
- launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- launcherIntent.setPackage(parsedPackage.getPackageName());
- final List<ResolveInfo> launcherActivities =
- mPm.snapshotComputer().queryIntentActivitiesInternal(launcherIntent, null,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
- if (launcherActivities.isEmpty()) {
- scanResult.mPkgSetting.setStopped(false, 0);
- }
- }
-
if (mIncrementalManager != null && isIncrementalPath(parsedPackage.getPath())) {
if (scanResult.mPkgSetting != null && scanResult.mPkgSetting.isLoading()) {
// Continue monitoring loading progress of active incremental packages
@@ -4314,6 +4300,8 @@ final class InstallPackageHelper {
// - It's an APEX or overlay package since stopped state does not affect them.
// - It is enumerated with a <initial-package-state> tag having the stopped attribute
// set to false
+ // - It doesn't have an enabled and exported launcher activity, which means the user
+ // wouldn't have a way to un-stop it
final boolean isApexPkg = (scanFlags & SCAN_AS_APEX) != 0;
if (mPm.mShouldStopSystemPackagesByDefault
&& scanSystemPartition
@@ -4322,8 +4310,9 @@ final class InstallPackageHelper {
&& !parsedPackage.isOverlayIsStatic()
) {
String packageName = parsedPackage.getPackageName();
- if (!mPm.mInitialNonStoppedSystemPackages.contains(packageName)
- && !"android".contentEquals(packageName)) {
+ if (!"android".contentEquals(packageName)
+ && !mPm.mInitialNonStoppedSystemPackages.contains(packageName)
+ && hasLauncherEntry(parsedPackage)) {
scanFlags |= SCAN_AS_STOPPED_SYSTEM_APP;
}
}
@@ -4333,6 +4322,26 @@ final class InstallPackageHelper {
return new Pair<>(scanResult, shouldHideSystemApp);
}
+ private static boolean hasLauncherEntry(ParsedPackage parsedPackage) {
+ final HashSet<String> categories = new HashSet<>();
+ categories.add(Intent.CATEGORY_LAUNCHER);
+ final List<ParsedActivity> activities = parsedPackage.getActivities();
+ for (int indexActivity = 0; indexActivity < activities.size(); indexActivity++) {
+ final ParsedActivity activity = activities.get(indexActivity);
+ if (!activity.isEnabled() || !activity.isExported()) {
+ continue;
+ }
+ final List<ParsedIntentInfo> intents = activity.getIntents();
+ for (int indexIntent = 0; indexIntent < intents.size(); indexIntent++) {
+ final IntentFilter intentFilter = intents.get(indexIntent).getIntentFilter();
+ if (intentFilter != null && intentFilter.matchCategories(categories) == null) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Returns if forced apk verification can be skipped for the whole package, including splits.
*/
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index f8bd3289a0b3..cab90d24ca39 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2650,15 +2650,15 @@ public class UserManagerService extends IUserManager.Stub {
@Override
public void setDefaultGuestRestrictions(Bundle restrictions) {
checkManageUsersPermission("setDefaultGuestRestrictions");
+ final List<UserInfo> guests = getGuestUsers();
+ synchronized (mRestrictionsLock) {
+ for (int i = 0; i < guests.size(); i++) {
+ updateUserRestrictionsInternalLR(restrictions, guests.get(i).id);
+ }
+ }
synchronized (mGuestRestrictions) {
mGuestRestrictions.clear();
mGuestRestrictions.putAll(restrictions);
- final List<UserInfo> guests = getGuestUsers();
- for (int i = 0; i < guests.size(); i++) {
- synchronized (mRestrictionsLock) {
- updateUserRestrictionsInternalLR(mGuestRestrictions, guests.get(i).id);
- }
- }
}
synchronized (mPackagesLock) {
writeUserListLP();
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
index d8e6c262359d..d770792dffec 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
@@ -17,6 +17,7 @@
package com.android.server.powerstats;
import android.content.Context;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.util.FileRotator;
@@ -27,6 +28,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
+import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -266,4 +268,51 @@ public class PowerStatsDataStorage {
mLock.unlock();
}
}
+
+ /**
+ * Dump stats about stored data.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ mLock.lock();
+ try {
+ final int versionDot = mDataStorageFilename.lastIndexOf('.');
+ final String beforeVersionDot = mDataStorageFilename.substring(0, versionDot);
+ final File[] files = mDataStorageDir.listFiles();
+
+ int number = 0;
+ int dataSize = 0;
+ long earliestLogEpochTime = Long.MAX_VALUE;
+ for (int i = 0; i < files.length; i++) {
+ // Check that the stems before the version match.
+ final File file = files[i];
+ final String fileName = file.getName();
+ if (files[i].getName().startsWith(beforeVersionDot)) {
+ number++;
+ dataSize += file.length();
+ final int firstTimeChar = fileName.lastIndexOf('.') + 1;
+ final int endChar = fileName.lastIndexOf('-');
+ try {
+ final Long startTime =
+ Long.parseLong(fileName.substring(firstTimeChar, endChar));
+ if (startTime != null && startTime < earliestLogEpochTime) {
+ earliestLogEpochTime = startTime;
+ }
+ } catch (NumberFormatException nfe) {
+ Slog.e(TAG,
+ "Failed to extract start time from file : " + fileName, nfe);
+ }
+ }
+ }
+
+ if (earliestLogEpochTime != Long.MAX_VALUE) {
+ ipw.println("Earliest data time : " + new Date(earliestLogEpochTime));
+ } else {
+ ipw.println("Failed to parse earliest data time!!!");
+ }
+ ipw.println("# files : " + number);
+ ipw.println("Total data size (B) : " + dataSize);
+ } finally {
+ mLock.unlock();
+ }
+ }
}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
index 39ead13b03fe..e80a86d73f90 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
@@ -30,6 +30,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
@@ -354,4 +355,23 @@ public final class PowerStatsLogger extends Handler {
updateCacheFile(residencyCacheFilename, powerEntityBytes);
}
}
+
+ /**
+ * Dump stats about stored data.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ ipw.println("PowerStats Meter Data:");
+ ipw.increaseIndent();
+ mPowerStatsMeterStorage.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.println("PowerStats Model Data:");
+ ipw.increaseIndent();
+ mPowerStatsModelStorage.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.println("PowerStats State Residency Data:");
+ ipw.increaseIndent();
+ mPowerStatsResidencyStorage.dump(ipw);
+ ipw.decreaseIndent();
+ }
+
}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index 2638f34fe7df..ffc9a0178971 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -31,6 +31,7 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.UserHandle;
import android.power.PowerStatsInternal;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
@@ -176,18 +177,31 @@ public class PowerStatsService extends SystemService {
} else if ("residency".equals(args[1])) {
mPowerStatsLogger.writeResidencyDataToFile(fd);
}
- } else if (args.length == 0) {
- pw.println("PowerStatsService dumpsys: available PowerEntities");
+ } else {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+ ipw.println("PowerStatsService dumpsys: available PowerEntities");
PowerEntity[] powerEntity = getPowerStatsHal().getPowerEntityInfo();
- PowerEntityUtils.dumpsys(powerEntity, pw);
+ ipw.increaseIndent();
+ PowerEntityUtils.dumpsys(powerEntity, ipw);
+ ipw.decreaseIndent();
- pw.println("PowerStatsService dumpsys: available Channels");
+ ipw.println("PowerStatsService dumpsys: available Channels");
Channel[] channel = getPowerStatsHal().getEnergyMeterInfo();
- ChannelUtils.dumpsys(channel, pw);
+ ipw.increaseIndent();
+ ChannelUtils.dumpsys(channel, ipw);
+ ipw.decreaseIndent();
- pw.println("PowerStatsService dumpsys: available EnergyConsumers");
+ ipw.println("PowerStatsService dumpsys: available EnergyConsumers");
EnergyConsumer[] energyConsumer = getPowerStatsHal().getEnergyConsumerInfo();
- EnergyConsumerUtils.dumpsys(energyConsumer, pw);
+ ipw.increaseIndent();
+ EnergyConsumerUtils.dumpsys(energyConsumer, ipw);
+ ipw.decreaseIndent();
+
+ ipw.println("PowerStatsService dumpsys: PowerStatsLogger stats");
+ ipw.increaseIndent();
+ mPowerStatsLogger.dump(ipw);
+ ipw.decreaseIndent();
+
}
}
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index efd8b6d9a943..a5123311d499 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -140,6 +140,20 @@ public interface StatusBarManagerInternal {
boolean showShutdownUi(boolean isReboot, String requestString);
/**
+ * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed
+ * status should be saved without user clicking on the button. This could happen when a user
+ * swipe on the edge with the confirmation prompt showing.
+ */
+ void confirmImmersivePrompt();
+
+ /**
+ * Notify System UI that the system get into or exit immersive mode.
+ * @param rootDisplayAreaId The changed display area Id.
+ * @param isImmersiveMode {@code true} if the display area get into immersive mode.
+ */
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+
+ /**
* Show a rotation suggestion that a user may approve to rotate the screen.
*
* @param rotation rotation suggestion
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index cc849b6fbf91..40e9c1305f01 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -27,6 +27,7 @@ import static android.app.StatusBarManager.NavBarMode;
import static android.app.StatusBarManager.SessionFlags;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
import android.Manifest;
@@ -638,6 +639,31 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
return false;
}
+ @Override
+ public void confirmImmersivePrompt() {
+ if (mBar == null) {
+ return;
+ }
+ try {
+ mBar.confirmImmersivePrompt();
+ } catch (RemoteException ex) {
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ if (mBar == null) {
+ return;
+ }
+ if (!CLIENT_TRANSIENT) {
+ // Only call from here when the client transient is not enabled.
+ try {
+ mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
// TODO(b/118592525): support it per display if necessary.
@Override
public void onProposedRotationChanged(int rotation, boolean isValid) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index ee7dc5007d97..309a9c0e0372 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2895,6 +2895,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
checkPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT);
final long ident = Binder.clearCallingIdentity();
try {
+ List<WallpaperData> pendingColorExtraction = new ArrayList<>();
synchronized (mLock) {
WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId);
WallpaperData lockWallpaper = mLockWallpaperMap.get(mCurrentUserId);
@@ -2930,7 +2931,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
// Need to extract colors again to re-calculate dark hints after
// applying dimming.
wp.mIsColorExtractedFromDim = true;
- notifyWallpaperColorsChanged(wp, wp.mWhich);
+ pendingColorExtraction.add(wp);
changed = true;
}
}
@@ -2962,6 +2963,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
}
+ for (WallpaperData wp: pendingColorExtraction) {
+ notifyWallpaperColorsChanged(wp, wp.mWhich);
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 884100c5da33..3c650e32beb9 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2032,6 +2032,13 @@ class ActivityStarter {
}
// ASM rules have failed. Log why
+ return logAsmFailureAndCheckFeatureEnabled(r, newTask, targetTask, shouldBlockActivityStart,
+ taskToFront);
+ }
+
+ private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord r, boolean newTask,
+ Task targetTask, boolean shouldBlockActivityStart, boolean taskToFront) {
+ // ASM rules have failed. Log why
ActivityRecord targetTopActivity = targetTask == null ? null
: targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop());
@@ -2041,6 +2048,13 @@ class ActivityStarter {
? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK
: FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK);
+ boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags
+ .shouldRestrictActivitySwitch(mCallingUid)
+ && shouldBlockActivityStart;
+
+ String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", r, targetTask,
+ targetTopActivity, blockActivityStartAndFeatureEnabled, /*taskToFront*/taskToFront);
+
FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
/* caller_uid */
mSourceRecord != null ? mSourceRecord.getUid() : mCallingUid,
@@ -2069,24 +2083,21 @@ class ActivityStarter {
targetTask != null && mSourceRecord != null
&& !targetTask.equals(mSourceRecord.getTask()) && targetTask.isVisible(),
/* bal_code */
- mBalCode
+ mBalCode,
+ /* task_stack */
+ asmDebugInfo
);
- boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags
- .shouldRestrictActivitySwitch(mCallingUid)
- && shouldBlockActivityStart;
-
String launchedFromPackageName = r.launchedFromPackage;
if (ActivitySecurityModelFeatureFlags.shouldShowToast(mCallingUid)) {
String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
+ (blockActivityStartAndFeatureEnabled ? " blocked " : " would block ")
+ getApplicationLabel(mService.mContext.getPackageManager(),
- launchedFromPackageName);
+ launchedFromPackageName);
UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
toastText, Toast.LENGTH_LONG).show());
- logDebugInfoForActivitySecurity("Launch", r, targetTask, targetTopActivity,
- blockActivityStartAndFeatureEnabled, /* taskToFront */ taskToFront);
+ Slog.i(TAG, asmDebugInfo);
}
if (blockActivityStartAndFeatureEnabled) {
@@ -2104,7 +2115,7 @@ class ActivityStarter {
}
/** Only called when an activity launch may be blocked, which should happen very rarely */
- private void logDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask,
+ private String getDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask,
ActivityRecord targetTopActivity, boolean blockActivityStartAndFeatureEnabled,
boolean taskToFront) {
final String prefix = "[ASM] ";
@@ -2165,7 +2176,7 @@ class ActivityStarter {
joiner.add(prefix + "BalCode: " + balCodeToString(mBalCode));
joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------");
- Slog.i(TAG, joiner.toString());
+ return joiner.toString();
}
/**
@@ -2339,7 +2350,7 @@ class ActivityStarter {
+ ActivitySecurityModelFeatureFlags.DOC_LINK,
Toast.LENGTH_LONG).show());
- logDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop,
+ getDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop,
shouldBlockActivityStart, /* taskToFront */ true);
}
}
@@ -3100,7 +3111,18 @@ class ActivityStarter {
} else {
TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
if (candidateTf == null) {
- final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */);
+ // Puts the activity on the top-most non-isolated navigation TF, unless the
+ // activity is launched from the same TF.
+ final TaskFragment sourceTaskFragment =
+ mSourceRecord != null ? mSourceRecord.getTaskFragment() : null;
+ final ActivityRecord top = task.getActivity(r -> {
+ if (!r.canBeTopRunning()) {
+ return false;
+ }
+ final TaskFragment taskFragment = r.getTaskFragment();
+ return !taskFragment.isIsolatedNav() || (sourceTaskFragment != null
+ && sourceTaskFragment == taskFragment);
+ });
if (top != null) {
candidateTf = top.getTaskFragment();
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 5553600b403f..cc130c407690 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1752,7 +1752,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
/* multi_window */
false,
/* bal_code */
- -1
+ -1,
+ /* task_stack */
+ null
);
boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index 89f044bdd163..d7667d8ce7a8 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -215,8 +215,7 @@ class Dimmer {
return mDimState;
}
- private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer,
- float alpha, int blurRadius) {
+ private void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) {
final DimState d = getDimState(container);
if (d == null) {
@@ -226,6 +225,7 @@ class Dimmer {
// The dim method is called from WindowState.prepareSurfaces(), which is always called
// in the correct Z from lowest Z to highest. This ensures that the dim layer is always
// relative to the highest Z layer with a dim.
+ SurfaceControl.Transaction t = mHost.getPendingTransaction();
t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
t.setAlpha(d.mDimLayer, alpha);
t.setBackgroundBlurRadius(d.mDimLayer, blurRadius);
@@ -238,26 +238,23 @@ class Dimmer {
* for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset
* and the child should call dimAbove again to request the Dim to continue.
*
- * @param t A transaction in which to apply the Dim.
* @param container The container which to dim above. Should be a child of our host.
* @param alpha The alpha at which to Dim.
*/
- void dimAbove(SurfaceControl.Transaction t, WindowContainer container, float alpha) {
- dim(t, container, 1, alpha, 0);
+ void dimAbove(WindowContainer container, float alpha) {
+ dim(container, 1, alpha, 0);
}
/**
* Like {@link #dimAbove} but places the dim below the given container.
*
- * @param t A transaction in which to apply the Dim.
* @param container The container which to dim below. Should be a child of our host.
* @param alpha The alpha at which to Dim.
* @param blurRadius The amount of blur added to the Dim.
*/
- void dimBelow(SurfaceControl.Transaction t, WindowContainer container, float alpha,
- int blurRadius) {
- dim(t, container, -1, alpha, blurRadius);
+ void dimBelow(WindowContainer container, float alpha, int blurRadius) {
+ dim(container, -1, alpha, blurRadius);
}
/**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2309e5891a30..64c2c5d9c228 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1935,7 +1935,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
} else if (mFixedRotationLaunchingApp != null && r == null) {
mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
// Keep async rotation controller if the next transition of display is requested.
- if (!mTransitionController.isCollecting(this)) {
+ if (!mTransitionController.hasCollectingRotationChange(this, getRotation())) {
finishAsyncRotationIfPossible();
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 2717a6a8ab04..354b0db77382 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -22,6 +22,7 @@ import static android.view.InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE;
import static android.view.InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS;
import static android.view.InsetsFrameProvider.SOURCE_DISPLAY;
import static android.view.InsetsFrameProvider.SOURCE_FRAME;
+import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -38,6 +39,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACK
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
@@ -194,6 +196,8 @@ public class DisplayPolicy {
private final ScreenshotHelper mScreenshotHelper;
private final Object mServiceAcquireLock = new Object();
+ private long mPanicTime;
+ private final long mPanicThresholdMs;
private StatusBarManagerInternal mStatusBarManagerInternal;
@Px
@@ -246,6 +250,8 @@ public class DisplayPolicy {
private volatile boolean mKeyguardDrawComplete;
private volatile boolean mWindowManagerDrawComplete;
+ private boolean mImmersiveConfirmationWindowExists;
+
private WindowState mStatusBar = null;
private volatile WindowState mNotificationShade;
private WindowState mNavigationBar = null;
@@ -402,6 +408,7 @@ public class DisplayPolicy {
mCanSystemBarsBeShownByUser = !r.getBoolean(
R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
+ mPanicThresholdMs = r.getInteger(R.integer.config_immersive_mode_confirmation_panic);
mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(
Context.ACCESSIBILITY_SERVICE);
@@ -623,8 +630,12 @@ public class DisplayPolicy {
};
displayContent.mAppTransition.registerListenerLocked(mAppTransitionListener);
displayContent.mTransitionController.registerLegacyListener(mAppTransitionListener);
- mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
- mService.mVrModeEnabled, mCanSystemBarsBeShownByUser);
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation = null;
+ } else {
+ mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
+ mService.mVrModeEnabled, mCanSystemBarsBeShownByUser);
+ }
// TODO: Make it can take screenshot on external display
mScreenshotHelper = displayContent.isDefaultDisplay
@@ -1075,6 +1086,9 @@ public class DisplayPolicy {
mNavigationBar = win;
break;
}
+ if ((attrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
+ mImmersiveConfirmationWindowExists = true;
+ }
if (attrs.providedInsets != null) {
for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
final InsetsFrameProvider provider = attrs.providedInsets[i];
@@ -1234,6 +1248,9 @@ public class DisplayPolicy {
}
}
mInsetsSourceWindowsExceptIme.remove(win);
+ if ((win.mAttrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
+ mImmersiveConfirmationWindowExists = false;
+ }
}
WindowState getStatusBar() {
@@ -2171,7 +2188,11 @@ public class DisplayPolicy {
}
}
}
- mImmersiveModeConfirmation.confirmCurrentPrompt();
+ if (CLIENT_IMMERSIVE_CONFIRMATION || CLIENT_TRANSIENT) {
+ mStatusBarManagerInternal.confirmImmersivePrompt();
+ } else {
+ mImmersiveModeConfirmation.confirmCurrentPrompt();
+ }
}
boolean isKeyguardShowing() {
@@ -2221,7 +2242,8 @@ public class DisplayPolicy {
// Immersive mode confirmation should never affect the system bar visibility, otherwise
// it will unhide the navigation bar and hide itself.
- if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) {
+ if ((winCandidate.getAttrs().privateFlags
+ & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
if (mNotificationShade != null && mNotificationShade.canReceiveKeys()) {
// Let notification shade control the system bar visibility.
winCandidate = mNotificationShade;
@@ -2389,9 +2411,16 @@ public class DisplayPolicy {
// The immersive confirmation window should be attached to the immersive window root.
final RootDisplayArea root = win.getRootDisplayArea();
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
- mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, isImmersiveMode,
- mService.mPolicy.isUserSetupComplete(),
- isNavBarEmpty(disableFlags));
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId,
+ isImmersiveMode,
+ mService.mPolicy.isUserSetupComplete(),
+ isNavBarEmpty(disableFlags));
+ } else {
+ // TODO (b/277290737): Move this to the client side, instead of using a proxy.
+ callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(rootDisplayAreaId,
+ isImmersiveMode));
+ }
}
// Show transient bars for panic if needed.
@@ -2604,16 +2633,39 @@ public class DisplayPolicy {
void onPowerKeyDown(boolean isScreenOn) {
// Detect user pressing the power button in panic when an application has
// taken over the whole screen.
- boolean panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn,
- SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow),
- isNavBarEmpty(mLastDisableFlags));
+ boolean panic = false;
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn,
+ SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow),
+ isNavBarEmpty(mLastDisableFlags));
+ } else {
+ panic = isPowerKeyDownPanic(isScreenOn, SystemClock.elapsedRealtime(),
+ isImmersiveMode(mSystemUiControllingWindow), isNavBarEmpty(mLastDisableFlags));
+ }
if (panic) {
mHandler.post(mHiddenNavPanic);
}
}
+ private boolean isPowerKeyDownPanic(boolean isScreenOn, long time, boolean inImmersiveMode,
+ boolean navBarEmpty) {
+ if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) {
+ // turning the screen back on within the panic threshold
+ return !mImmersiveConfirmationWindowExists;
+ }
+ if (isScreenOn && inImmersiveMode && !navBarEmpty) {
+ // turning the screen off, remember if we were in immersive mode
+ mPanicTime = time;
+ } else {
+ mPanicTime = 0;
+ }
+ return false;
+ }
+
void onVrStateChangedLw(boolean enabled) {
- mImmersiveModeConfirmation.onVrStateChangedLw(enabled);
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.onVrStateChangedLw(enabled);
+ }
}
/**
@@ -2626,7 +2678,9 @@ public class DisplayPolicy {
* {@link ActivityManager#LOCK_TASK_MODE_PINNED}.
*/
public void onLockTaskStateChangedLw(int lockTaskState) {
- mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
+ }
}
/** Called when a {@link android.os.PowerManager#USER_ACTIVITY_EVENT_TOUCH} is sent. */
@@ -2643,7 +2697,11 @@ public class DisplayPolicy {
}
boolean onSystemUiSettingsChanged() {
- return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ return false;
+ } else {
+ return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
+ }
}
/**
@@ -2857,7 +2915,9 @@ public class DisplayPolicy {
mDisplayContent.mTransitionController.unregisterLegacyListener(mAppTransitionListener);
mHandler.post(mGestureNavigationSettingsObserver::unregister);
mHandler.post(mForceShowNavBarSettingsObserver::unregister);
- mImmersiveModeConfirmation.release();
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.release();
+ }
if (mService.mPointerLocationEnabled) {
setPointerLocationEnabled(false);
}
diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
index 56edde09f747..bd08dff9481b 100644
--- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
@@ -229,7 +230,8 @@ public class ImmersiveModeConfirmation {
lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
// Trusted overlay so touches outside the touchable area are allowed to pass through
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
- | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+ | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
lp.setTitle("ImmersiveModeConfirmation");
lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
lp.token = getWindowToken();
@@ -469,6 +471,9 @@ public class ImmersiveModeConfirmation {
@Override
public void handleMessage(Message msg) {
+ if (CLIENT_TRANSIENT) {
+ return;
+ }
switch(msg.what) {
case SHOW:
handleShow(msg.arg1);
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index e945bc1babd9..9e3a611c0e70 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -85,6 +85,11 @@ final class LetterboxConfiguration {
// TODO(b/288142656): Enable user aspect ratio settings by default.
private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = false;
+ // Whether per-app fullscreen user aspect ratio override option is enabled
+ private static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
+ "enable_app_compat_user_aspect_ratio_fullscreen";
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
+
// Whether the letterbox wallpaper style is enabled by default
private static final String KEY_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER =
"enable_letterbox_background_wallpaper";
@@ -266,6 +271,9 @@ final class LetterboxConfiguration {
// Allows to enable user aspect ratio settings ignoring flags.
private boolean mUserAppAspectRatioSettingsOverrideEnabled;
+ // Allows to enable fullscreen option in user aspect ratio settings ignoring flags.
+ private boolean mUserAppAspectRatioFullscreenOverrideEnabled;
+
// The override for letterbox background type in case it's different from
// LETTERBOX_BACKGROUND_OVERRIDE_UNSET
@LetterboxBackgroundType
@@ -379,6 +387,10 @@ final class LetterboxConfiguration {
R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled))
.addDeviceConfigEntry(KEY_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER,
DEFAULT_VALUE_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER, /* enabled */ true)
+ .addDeviceConfigEntry(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ mContext.getResources().getBoolean(
+ R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled))
.build();
}
@@ -1275,4 +1287,21 @@ final class LetterboxConfiguration {
void resetUserAppAspectRatioSettingsEnabled() {
setUserAppAspectRatioSettingsOverrideEnabled(false);
}
+
+ /**
+ * Whether fullscreen option in per-app user aspect ratio settings is enabled
+ */
+ boolean isUserAppAspectRatioFullscreenEnabled() {
+ return isUserAppAspectRatioSettingsEnabled()
+ && (mUserAppAspectRatioFullscreenOverrideEnabled
+ || mDeviceConfig.getFlagValue(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN));
+ }
+
+ void setUserAppAspectRatioFullscreenOverrideEnabled(boolean enabled) {
+ mUserAppAspectRatioFullscreenOverrideEnabled = enabled;
+ }
+
+ void resetUserAppAspectRatioFullscreenEnabled() {
+ setUserAppAspectRatioFullscreenOverrideEnabled(false);
+ }
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 92c0987d5636..57ce368aae87 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -329,6 +329,15 @@ class TaskFragment extends WindowContainer<WindowContainer> {
*/
private boolean mDelayLastActivityRemoval;
+ /**
+ * Whether the activity navigation should be isolated. That is, Activities cannot be launched
+ * on an isolated TaskFragment, unless the activity is launched from an Activity in the same
+ * isolated TaskFragment, or explicitly requested to be launched to.
+ * <p>
+ * Note that only an embedded TaskFragment can be isolated.
+ */
+ private boolean mIsolatedNav;
+
final Point mLastSurfaceSize = new Point();
private final Rect mTmpBounds = new Rect();
@@ -481,6 +490,19 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return mAnimationParams;
}
+ /** @see #mIsolatedNav */
+ void setIsolatedNav(boolean isolatedNav) {
+ if (!isEmbedded()) {
+ return;
+ }
+ mIsolatedNav = isolatedNav;
+ }
+
+ /** @see #mIsolatedNav */
+ boolean isIsolatedNav() {
+ return isEmbedded() && mIsolatedNav;
+ }
+
TaskFragment getAdjacentTaskFragment() {
return mAdjacentTaskFragment;
}
@@ -3034,7 +3056,8 @@ class TaskFragment extends WindowContainer<WindowContainer> {
@Override
void dump(PrintWriter pw, String prefix, boolean dumpAll) {
super.dump(pw, prefix, dumpAll);
- pw.println(prefix + "bounds=" + getBounds().toShortString());
+ pw.println(prefix + "bounds=" + getBounds().toShortString()
+ + (mIsolatedNav ? ", isolatedNav" : ""));
final String doublePrefix = prefix + " ";
for (int i = mChildren.size() - 1; i >= 0; i--) {
final WindowContainer<?> child = mChildren.get(i);
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 37985ea0aa6a..1565341deb4c 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -578,6 +578,17 @@ class TransitionController {
}
/**
+ * Returns {@code true} if the window container is in the collecting transition, and its
+ * collected rotation is different from the target rotation.
+ */
+ boolean hasCollectingRotationChange(@NonNull WindowContainer<?> wc, int targetRotation) {
+ final Transition transition = mCollectingTransition;
+ if (transition == null || !transition.mParticipants.contains(wc)) return false;
+ final Transition.ChangeInfo changeInfo = transition.mChanges.get(wc);
+ return changeInfo != null && changeInfo.mRotation != targetRotation;
+ }
+
+ /**
* @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
*/
@Nullable
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index f4781f9bc9f0..ceebb27642ce 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1013,6 +1013,10 @@ public class WindowManagerShellCommand extends ShellCommand {
runSetBooleanFlag(pw, mLetterboxConfiguration
::setUserAppAspectRatioSettingsOverrideEnabled);
break;
+ case "--isUserAppAspectRatioFullscreenEnabled":
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setUserAppAspectRatioFullscreenOverrideEnabled);
+ break;
case "--isCameraCompatRefreshEnabled":
runSetBooleanFlag(pw, mLetterboxConfiguration::setCameraCompatRefreshEnabled);
break;
@@ -1093,6 +1097,9 @@ public class WindowManagerShellCommand extends ShellCommand {
case "isUserAppAspectRatioSettingsEnabled":
mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
break;
+ case "isUserAppAspectRatioFullscreenEnabled":
+ mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
+ break;
case "isCameraCompatRefreshEnabled":
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
break;
@@ -1204,6 +1211,7 @@ public class WindowManagerShellCommand extends ShellCommand {
mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
+ mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
}
@@ -1272,6 +1280,8 @@ public class WindowManagerShellCommand extends ShellCommand {
+ mLetterboxConfiguration.isTranslucentLetterboxingEnabled());
pw.println("Is the user aspect ratio settings enabled: "
+ mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled());
+ pw.println("Is the fullscreen option in user aspect ratio settings enabled: "
+ + mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled());
}
return 0;
}
@@ -1471,6 +1481,8 @@ public class WindowManagerShellCommand extends ShellCommand {
pw.println(" Whether letterboxing for translucent activities is enabled.");
pw.println(" --isUserAppAspectRatioSettingsEnabled [true|1|false|0]");
pw.println(" Whether user aspect ratio settings are enabled.");
+ pw.println(" --isUserAppAspectRatioFullscreenEnabled [true|1|false|0]");
+ pw.println(" Whether user aspect ratio fullscreen option is enabled.");
pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]");
pw.println(" Whether camera compatibility refresh is enabled.");
pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 5d239eb993af..be0f6db24923 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -30,6 +30,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS;
import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN;
@@ -1356,6 +1357,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
break;
}
+ case OP_TYPE_SET_ISOLATED_NAVIGATION: {
+ final boolean isolatedNav = operation.isIsolatedNav();
+ taskFragment.setIsolatedNav(isolatedNav);
+ break;
+ }
}
return effects;
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0d4c2d631b2c..140255b2f016 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5132,7 +5132,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private void applyDims() {
if (((mAttrs.flags & FLAG_DIM_BEHIND) != 0 || shouldDrawBlurBehind())
- && isVisibleNow() && !mHidden && mTransitionController.canApplyDim(getTask())) {
+ && mToken.isVisibleRequested() && isVisibleNow() && !mHidden
+ && mTransitionController.canApplyDim(getTask())) {
// Only show the Dimmer when the following is satisfied:
// 1. The window has the flag FLAG_DIM_BEHIND or blur behind is requested
// 2. The WindowToken is not hidden so dims aren't shown when the window is exiting.
@@ -5142,7 +5143,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mIsDimming = true;
final float dimAmount = (mAttrs.flags & FLAG_DIM_BEHIND) != 0 ? mAttrs.dimAmount : 0;
final int blurRadius = shouldDrawBlurBehind() ? mAttrs.getBlurBehindRadius() : 0;
- getDimmer().dimBelow(getSyncTransaction(), this, dimAmount, blurRadius);
+ getDimmer().dimBelow(this, dimAmount, blurRadius);
}
}
@@ -5702,8 +5703,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// window becomes visible while the sync group is still active.
return true;
}
- if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mWinAnimator.mDrawState == HAS_DRAWN
- && !mRedrawForSyncReported && !mWmService.mResizingWindows.contains(this)) {
+ if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mLastConfigReportedToClient && isDrawn()) {
// Complete the sync state immediately for a drawn window that doesn't need to redraw.
onSyncFinishedDrawing();
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index c065cb5f4ebe..9d391654063c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -108,6 +108,7 @@ static struct {
jmethodID notifySensorEvent;
jmethodID notifySensorAccuracy;
jmethodID notifyStylusGestureStarted;
+ jmethodID isInputMethodConnectionActive;
jmethodID notifyVibratorState;
jmethodID filterInputEvent;
jmethodID interceptKeyBeforeQueueing;
@@ -322,6 +323,7 @@ public:
TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr);
void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
+ bool isInputMethodConnectionActive() override;
/* --- InputDispatcherPolicyInterface implementation --- */
@@ -1306,6 +1308,14 @@ void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t ev
checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted");
}
+bool NativeInputManager::isInputMethodConnectionActive() {
+ JNIEnv* env = jniEnv();
+ const jboolean result =
+ env->CallBooleanMethod(mServiceObj, gServiceClassInfo.isInputMethodConnectionActive);
+ checkAndClearExceptionFromCallback(env, "isInputMethodConnectionActive");
+ return result;
+}
+
bool NativeInputManager::filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) {
ATRACE_CALL();
JNIEnv* env = jniEnv();
@@ -2743,6 +2753,9 @@ int register_android_server_InputManager(JNIEnv* env) {
GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted",
"(IJ)V");
+ GET_METHOD_ID(gServiceClassInfo.isInputMethodConnectionActive, clazz,
+ "isInputMethodConnectionActive", "()Z");
+
GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V");
GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr",
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index fe913b9807cf..f3c2de6f7af1 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -241,7 +241,6 @@ import static android.provider.Telephony.Carriers.ENFORCE_KEY;
import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
import static android.provider.Telephony.Carriers.INVALID_APN_ID;
import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION;
-
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
@@ -540,6 +539,8 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
@@ -18817,10 +18818,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
});
}
+ ThreadPoolExecutor calculateHasIncompatibleAccountsExecutor = new ThreadPoolExecutor(
+ 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+
@Override
public void calculateHasIncompatibleAccounts() {
+ if (calculateHasIncompatibleAccountsExecutor.getQueue().size() > 1) {
+ return;
+ }
new CalculateHasIncompatibleAccountsTask().executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, null);
+ calculateHasIncompatibleAccountsExecutor, null);
}
@Nullable
diff --git a/services/tests/displayservicetests/TEST_MAPPING b/services/tests/displayservicetests/TEST_MAPPING
index d8655194ab0f..477860d3a1c5 100644
--- a/services/tests/displayservicetests/TEST_MAPPING
+++ b/services/tests/displayservicetests/TEST_MAPPING
@@ -1,13 +1,7 @@
{
- "presubmit": [
+ "imports": [
{
- "name": "DisplayServiceTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
+ "path": "frameworks/base/services/core/java/com/android/server/display"
}
]
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 56f650ee9084..c39bb56e7ba1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -50,6 +50,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
@@ -144,11 +145,12 @@ public final class DisplayPowerController2Test {
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- // Put the system into manual brightness by default, just to minimize unexpected events and
- // have a consistent starting state
+ // Set some settings to minimize unexpected events and have a consistent starting state
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index e2aeea3bedba..0544376959ee 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -50,6 +50,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
@@ -144,12 +145,12 @@ public final class DisplayPowerControllerTest {
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- // Put the system into manual brightness by default, just to minimize unexpected events and
- // have a consistent starting state
+ // Set some settings to minimize unexpected events and have a consistent starting state
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
index d9fbba5b4274..7e7ccf733876 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
@@ -17,35 +17,69 @@
package com.android.server.display;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
public class HighBrightnessModeMetadataMapperTest {
+ @Mock
+ private LogicalDisplay mDisplayMock;
+
+ @Mock
+ private DisplayDevice mDeviceMock;
+
+ @Mock
+ private DisplayDeviceConfig mDdcMock;
+
+ @Mock
+ private DisplayDeviceConfig.HighBrightnessModeData mHbmDataMock;
+
private HighBrightnessModeMetadataMapper mHighBrightnessModeMetadataMapper;
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDeviceMock);
+ when(mDeviceMock.getDisplayDeviceConfig()).thenReturn(mDdcMock);
+ when(mDdcMock.getHighBrightnessModeData()).thenReturn(mHbmDataMock);
mHighBrightnessModeMetadataMapper = new HighBrightnessModeMetadataMapper();
}
@Test
- public void testGetHighBrightnessModeMetadata() {
- // Display device is null
- final LogicalDisplay display = mock(LogicalDisplay.class);
- when(display.getPrimaryDisplayDeviceLocked()).thenReturn(null);
- assertNull(mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display));
-
- // No HBM metadata stored for this display yet
- final DisplayDevice device = mock(DisplayDevice.class);
- when(display.getPrimaryDisplayDeviceLocked()).thenReturn(device);
+ public void testGetHighBrightnessModeMetadata_NoDisplayDevice() {
+ when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(null);
+ assertNull(mHighBrightnessModeMetadataMapper
+ .getHighBrightnessModeMetadataLocked(mDisplayMock));
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_NoHBMData() {
+ when(mDdcMock.getHighBrightnessModeData()).thenReturn(null);
+ assertNull(mHighBrightnessModeMetadataMapper
+ .getHighBrightnessModeMetadataLocked(mDisplayMock));
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_NewDisplay() {
HighBrightnessModeMetadata hbmMetadata =
- mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+ assertNotNull(hbmMetadata);
+ assertTrue(hbmMetadata.getHbmEventQueue().isEmpty());
+ assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0);
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_Modify() {
+ HighBrightnessModeMetadata hbmMetadata =
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+ assertNotNull(hbmMetadata);
assertTrue(hbmMetadata.getHbmEventQueue().isEmpty());
assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0);
@@ -55,8 +89,10 @@ public class HighBrightnessModeMetadataMapperTest {
long setTime = 300;
hbmMetadata.addHbmEvent(new HbmEvent(startTimeMillis, endTimeMillis));
hbmMetadata.setRunningStartTimeMillis(setTime);
+
hbmMetadata =
- mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+
assertEquals(1, hbmMetadata.getHbmEventQueue().size());
assertEquals(startTimeMillis,
hbmMetadata.getHbmEventQueue().getFirst().getStartTimeMillis());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 0292bca06813..6b225fc945d5 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -405,6 +405,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
UriGrantsManagerInternal mUgmInternal;
@Mock
AppOpsManager mAppOpsManager;
+ private AppOpsManager.OnOpChangedListener mOnPermissionChangeListener;
@Mock
private TestableNotificationManagerService.NotificationAssistantAccessGrantedCallback
mNotificationAssistantAccessGrantedCallback;
@@ -421,6 +422,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Mock
MultiRateLimiter mToastRateLimiter;
BroadcastReceiver mPackageIntentReceiver;
+ BroadcastReceiver mUserSwitchIntentReceiver;
NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
@@ -604,6 +606,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
tr.addOverride(com.android.internal.R.string.config_defaultSearchSelectorPackageName,
SEARCH_SELECTOR_PKG);
+ doAnswer(invocation -> {
+ mOnPermissionChangeListener = invocation.getArgument(2);
+ return null;
+ }).when(mAppOpsManager).startWatchingMode(eq(AppOpsManager.OP_POST_NOTIFICATION), any(),
+ any());
+ when(mUmInternal.isUserInitialized(anyInt())).thenReturn(true);
+
mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
@@ -651,8 +660,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
&& filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
mPackageIntentReceiver = broadcastReceivers.get(i);
}
+ if (filter.hasAction(Intent.ACTION_USER_SWITCHED)) {
+ mUserSwitchIntentReceiver = broadcastReceivers.get(i);
+ }
}
assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
+ assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver);
// Pretend the shortcut exists
List<ShortcutInfo> shortcutInfos = new ArrayList<>();
@@ -692,6 +705,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mTestFlagResolver.setFlagOverride(FSI_FORCE_DEMOTE, false);
mTestFlagResolver.setFlagOverride(SHOW_STICKY_HUN_FOR_DENIED_FSI, false);
+
+ var checker = mock(TestableNotificationManagerService.ComponentPermissionChecker.class);
+ mService.permissionChecker = checker;
+ when(checker.check(anyString(), anyInt(), anyInt(), anyBoolean()))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
}
@After
@@ -3216,6 +3234,108 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void onOpChanged_permissionRevoked_cancelsAllNotificationsFromPackage()
+ throws RemoteException {
+ // Have preexisting posted notifications from revoked package and other packages.
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("revoked", 1001, 1, 0), mTestNotificationChannel));
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
+ // Have preexisting enqueued notifications from revoked package and other packages.
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("revoked", 1001, 3, 0), mTestNotificationChannel));
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+
+ when(mPackageManagerInternal.getPackageUid("revoked", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "revoked", 0);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getSbn().getPackageName()).isEqualTo("other");
+ assertThat(mService.mEnqueuedNotifications).hasSize(1);
+ assertThat(mService.mEnqueuedNotifications.get(0).getSbn().getPackageName()).isEqualTo(
+ "other");
+ }
+
+ @Test
+ public void onOpChanged_permissionStillGranted_notificationsAreNotAffected()
+ throws RemoteException {
+ // NOTE: This combination (receiving the onOpChanged broadcast for a package, the permission
+ // being now granted, AND having previously posted notifications from said package) should
+ // never happen (if we trust the broadcasts are correct). So this test is for a what-if
+ // scenario, to verify we still handle it reasonably.
+
+ // Have preexisting posted notifications from specific package and other packages.
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("granted", 1001, 1, 0), mTestNotificationChannel));
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
+ // Have preexisting enqueued notifications from specific package and other packages.
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("granted", 1001, 3, 0), mTestNotificationChannel));
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+
+ when(mPackageManagerInternal.getPackageUid("granted", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "granted", 0);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+ }
+
+ @Test
+ public void onOpChanged_notInitializedUser_ignored() throws RemoteException {
+ when(mUmInternal.isUserInitialized(eq(0))).thenReturn(false);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
+ waitForIdle();
+
+ // We early-exited and didn't even query PM for package details.
+ verify(mPackageManagerInternal, never()).getPackageUid(any(), anyLong(), anyInt());
+ }
+
+ @Test
+ public void setNotificationsEnabledForPackage_disabling_clearsNotifications() throws Exception {
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("package", 1001, 1, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(1);
+ when(mPackageManagerInternal.getPackageUid("package", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasRequestedPermission(any(), eq("package"), anyInt())).thenReturn(
+ true);
+
+ // Start with granted permission and simulate effect of revoking it.
+ when(mPermissionHelper.hasPermission(1001)).thenReturn(true);
+ doAnswer(invocation -> {
+ when(mPermissionHelper.hasPermission(1001)).thenReturn(false);
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
+ return null;
+ }).when(mPermissionHelper).setNotificationPermission("package", 0, false, true);
+
+ mBinderService.setNotificationsEnabledForPackage("package", 1001, false);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(0);
+
+ mTestableLooper.moveTimeForward(500);
+ waitForIdle();
+ verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null));
+ }
+
+ @Test
public void testUpdateAppNotifyCreatorBlock() throws Exception {
when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
@@ -4235,6 +4355,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void testSetNASMigrationDoneAndResetDefault_enableNAS() throws Exception {
int userId = 10;
+ setNASMigrationDone(false, userId);
when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId});
mBinderService.setNASMigrationDoneAndResetDefault(userId, true);
@@ -4246,6 +4367,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void testSetNASMigrationDoneAndResetDefault_disableNAS() throws Exception {
int userId = 10;
+ setNASMigrationDone(false, userId);
when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId});
mBinderService.setNASMigrationDoneAndResetDefault(userId, false);
@@ -4258,6 +4380,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testSetNASMigrationDoneAndResetDefault_multiProfile() throws Exception {
int userId1 = 11;
int userId2 = 12; //work profile
+ setNASMigrationDone(false, userId1);
+ setNASMigrationDone(false, userId2);
setUsers(new int[]{userId1, userId2});
when(mUm.isManagedProfile(userId2)).thenReturn(true);
when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1, userId2});
@@ -4271,6 +4395,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testSetNASMigrationDoneAndResetDefault_multiUser() throws Exception {
int userId1 = 11;
int userId2 = 12;
+ setNASMigrationDone(false, userId1);
+ setNASMigrationDone(false, userId2);
setUsers(new int[]{userId1, userId2});
when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1});
when(mUm.getProfileIds(userId2, false)).thenReturn(new int[]{userId2});
@@ -12148,6 +12274,145 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
any(), eq(FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER));
}
+ @Test
+ public void onUserSwitched_updatesZenModeAndChannelsBypassingDnd() {
+ Intent intent = new Intent(Intent.ACTION_USER_SWITCHED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, 20);
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ mService.setPreferencesHelper(mPreferencesHelper);
+
+ mUserSwitchIntentReceiver.onReceive(mContext, intent);
+
+ InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
+ inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
+ inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception {
+ final String notReal = "NOT REAL";
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow(
+ PackageManager.NameNotFoundException.class);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt());
+ verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean());
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_hasPermission() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt()))
+ .thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mListeners.isComponentEnabledForPackage(packageName)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ }
+
+ /**
+ * b/292163859
+ */
+ @Test
+ public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final int callingUid = Binder.getCallingUid();
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
+ }
+
+ @Test
+ public void isNotificationPolicyAccessGranted_notGranted() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ }
+
private static <T extends Parcelable> T parcelAndUnparcel(T source,
Parcelable.Creator<T> creator) {
Parcel parcel = Parcel.obtain();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index ea670bd94d88..c242554e950b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -2478,7 +2478,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
-
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2509,6 +2509,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2533,6 +2534,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2587,6 +2589,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
assertFalse(mHelper.areChannelsBypassingDnd());
verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean());
resetZenModeHelper();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 9f4eee7e332f..27e8f3664a65 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -43,6 +43,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi
@Nullable
Boolean mIsVisibleToListenerReturnValue = null;
+ ComponentPermissionChecker permissionChecker;
+
TestableNotificationManagerService(Context context, NotificationRecordLogger logger,
InstanceIdSequence notificationInstanceIdSequence) {
super(context, logger, notificationInstanceIdSequence);
@@ -150,6 +152,12 @@ public class TestableNotificationManagerService extends NotificationManagerServi
return super.isVisibleToListener(sbn, notificationType, listener);
}
+ @Override
+ protected int checkComponentPermission(String permission, int uid, int owningUid,
+ boolean exported) {
+ return permissionChecker.check(permission, uid, owningUid, exported);
+ }
+
public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker {
private int mGetStrongAuthForUserReturnValue = 0;
StrongAuthTrackerFake(Context context) {
@@ -165,4 +173,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi
return mGetStrongAuthForUserReturnValue;
}
}
+
+ public interface ComponentPermissionChecker {
+ int check(String permission, int uid, int owningUid, boolean exported);
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index f235d153c658..233a2076a867 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -52,7 +52,8 @@ public class DimmerTests extends WindowTestsBase {
private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
final SurfaceControl mControl = mock(SurfaceControl.class);
- final SurfaceControl.Transaction mTransaction = spy(StubTransaction.class);
+ final SurfaceControl.Transaction mPendingTransaction = spy(StubTransaction.class);
+ final SurfaceControl.Transaction mSyncTransaction = spy(StubTransaction.class);
TestWindowContainer(WindowManagerService wm) {
super(wm);
@@ -65,12 +66,12 @@ public class DimmerTests extends WindowTestsBase {
@Override
public SurfaceControl.Transaction getSyncTransaction() {
- return mTransaction;
+ return mSyncTransaction;
}
@Override
public SurfaceControl.Transaction getPendingTransaction() {
- return mTransaction;
+ return mPendingTransaction;
}
}
@@ -144,7 +145,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
int width = 100;
int height = 300;
@@ -161,13 +162,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(dimLayer, alpha);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
+ verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, 1);
}
@Test
@@ -176,13 +177,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimBelow(mTransaction, child, alpha, 0);
+ mDimmer.dimBelow(child, alpha, 0);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(dimLayer, alpha);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1);
}
@Test
@@ -191,7 +192,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
@@ -208,10 +209,10 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
mDimmer.updateDims(mTransaction);
verify(mTransaction).show(dimLayer);
@@ -224,7 +225,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
final Rect bounds = mDimmer.mDimState.mDimBounds;
SurfaceControl dimLayer = getDimLayer();
@@ -245,7 +246,7 @@ public class DimmerTests extends WindowTestsBase {
TestWindowContainer child = new TestWindowContainer(mWm);
mHost.addChild(child, 0);
- mDimmer.dimAbove(mTransaction, child, 1);
+ mDimmer.dimAbove(child, 1);
SurfaceControl dimLayer = getDimLayer();
mDimmer.updateDims(mTransaction);
verify(mTransaction, times(1)).show(dimLayer);
@@ -266,13 +267,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final int blurRadius = 50;
- mDimmer.dimBelow(mTransaction, child, 0, blurRadius);
+ mDimmer.dimBelow(child, 0, blurRadius);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setBackgroundBlurRadius(dimLayer, blurRadius);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ verify(mHost.getPendingTransaction()).setBackgroundBlurRadius(dimLayer, blurRadius);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1);
}
private SurfaceControl getDimLayer() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index fc5e9cab5447..810cbe8f8080 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -40,9 +40,7 @@ import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.spy;
import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
import android.view.SurfaceControl;
-import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -333,8 +331,7 @@ public class SyncEngineTests extends WindowTestsBase {
w.reparent(botChildWC, POSITION_TOP);
parentWC.prepareSync();
// Assume the window has drawn with the latest configuration.
- w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
- new MergedConfiguration(), true /* useLatestConfig */, true /* relayoutVisible */);
+ makeLastConfigReportedToClient(w, true /* visible */);
assertTrue(w.onSyncFinishedDrawing());
assertEquals(SYNC_STATE_READY, w.mSyncState);
w.reparent(topChildWC, POSITION_TOP);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 9d597b11120d..6a9bb6c85c70 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -653,4 +653,23 @@ public class TaskFragmentTest extends WindowTestsBase {
assertEquals(mDisplayContent.getImeContainer().getParent().getSurfaceControl(),
mDisplayContent.computeImeParent());
}
+
+ @Test
+ public void testIsolatedNavigation() {
+ final Task task = createTask(mDisplayContent);
+ final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .createActivityCount(1)
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(new Binder())
+ .build();
+
+ // Cannot be isolated if not embedded.
+ task.setIsolatedNav(true);
+ assertFalse(task.isIsolatedNav());
+
+ // Ensure the TaskFragment is isolated once set.
+ tf0.setIsolatedNav(true);
+ assertTrue(tf0.isIsolatedNav());
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index e91fdde955ef..ca5d8fe33dba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1240,6 +1240,27 @@ public class TransitionTests extends WindowTestsBase {
}
@Test
+ public void testFinishRotationControllerWithFixedRotation() {
+ final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
+ registerTestTransitionPlayer();
+ mDisplayContent.setLastHasContent();
+ mDisplayContent.requestChangeTransitionIfNeeded(1 /* changes */, null /* displayChange */);
+ assertNotNull(mDisplayContent.getAsyncRotationController());
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(null);
+ assertNull("Clear rotation controller if rotation is not changed",
+ mDisplayContent.getAsyncRotationController());
+
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
+ assertNotNull(mDisplayContent.getAsyncRotationController());
+ mDisplayContent.getDisplayRotation().setRotation(
+ mDisplayContent.getWindowConfiguration().getRotation() + 1);
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(null);
+ assertNotNull("Keep rotation controller if rotation will be changed",
+ mDisplayContent.getAsyncRotationController());
+ }
+
+ @Test
public void testDeferRotationForTransientLaunch() {
final TestTransitionPlayer player = registerTestTransitionPlayer();
assumeFalse(mDisplayContent.mTransitionController.useShellTransitionsRotation());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 4afcd0539b94..e3d1b9c669f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -52,7 +52,6 @@ import android.graphics.Rect;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.DisplayShape;
@@ -63,7 +62,6 @@ import android.view.RoundedCorners;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
-import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -380,8 +378,7 @@ public class WallpaperControllerTests extends WindowTestsBase {
wallpaperWindow.mLayoutSeq = mDisplayContent.mLayoutSeq;
// Assume the token was invisible and the latest config was reported.
wallpaperToken.commitVisibility(false);
- wallpaperWindow.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
- new MergedConfiguration(), true /* useLatestConfig */, false /* relayoutVisible */);
+ makeLastConfigReportedToClient(wallpaperWindow, false /* visible */);
assertTrue(wallpaperWindow.isLastConfigReportedToClient());
final Rect bounds = wallpaperToken.getBounds();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 600681fb332c..7168670f9652 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1247,6 +1247,7 @@ public class WindowOrganizerTests extends WindowTestsBase {
// A drawn window can complete the sync state automatically.
w1.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN;
+ makeLastConfigReportedToClient(w1, true /* visible */);
mWm.mSyncEngine.onSurfacePlacement();
verify(mockCallback).onTransactionReady(anyInt(), any());
assertFalse(w1.useBLASTSync());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index d5547ec69247..873c7f4e0e30 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -85,6 +85,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
+import android.util.MergedConfiguration;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
@@ -102,6 +103,7 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
import android.view.inputmethod.ImeTracker;
+import android.window.ClientWindowFrames;
import android.window.ITransitionPlayer;
import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
@@ -624,6 +626,11 @@ class WindowTestsBase extends SystemServiceTestsBase {
}
}
+ static void makeLastConfigReportedToClient(WindowState w, boolean visible) {
+ w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
+ new MergedConfiguration(), true /* useLatestConfig */, visible);
+ }
+
/**
* Gets the order of the given {@link Task} as its z-order in the hierarchy below this TDA.
* The Task can be a direct child of a child TaskDisplayArea. {@code -1} if not found.
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 248cc26ce656..ccc4ac28876a 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -27,6 +27,7 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH;
+import android.app.AppOpsManager;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
@@ -548,13 +549,15 @@ final class HotwordDetectionConnection {
static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub {
private final HotwordDetectionConnection mHotwordDetectionConnection;
private final IHotwordRecognitionStatusCallback mExternalCallback;
- private final int mVoiceInteractionServiceUid;
+ private final Identity mVoiceInteractorIdentity;
+ private final Context mContext;
- SoundTriggerCallback(IHotwordRecognitionStatusCallback callback,
- HotwordDetectionConnection connection, int uid) {
+ SoundTriggerCallback(Context context, IHotwordRecognitionStatusCallback callback,
+ HotwordDetectionConnection connection, Identity voiceInteractorIdentity) {
+ mContext = context;
mHotwordDetectionConnection = connection;
mExternalCallback = callback;
- mVoiceInteractionServiceUid = uid;
+ mVoiceInteractorIdentity = voiceInteractorIdentity;
}
@Override
@@ -568,15 +571,30 @@ final class HotwordDetectionConnection {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP,
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
- mVoiceInteractionServiceUid);
+ mVoiceInteractorIdentity.uid);
mHotwordDetectionConnection.detectFromDspSource(
recognitionEvent, mExternalCallback);
} else {
- HotwordMetricsLogger.writeKeyphraseTriggerEvent(
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
- mVoiceInteractionServiceUid);
- mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
+ // We have to attribute ops here, since we configure all st clients as trusted to
+ // enable a partial exemption.
+ // TODO (b/292012931) remove once trusted uniformly required.
+ int result = mContext.getSystemService(AppOpsManager.class)
+ .noteOpNoThrow(AppOpsManager.OP_RECORD_AUDIO_HOTWORD,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag,
+ "Non-HDS keyphrase recognition to VoiceInteractionService");
+
+ if (result != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(TAG, "onKeyphraseDetected suppressed, permission check returned: "
+ + result);
+ mExternalCallback.onRecognitionPaused();
+ } else {
+ HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+ mVoiceInteractorIdentity.uid);
+ mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
+ }
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 423a81ac0523..3574ef8e91fb 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -104,6 +104,7 @@ import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.permission.LegacyPermissionManagerInternal;
+import com.android.server.policy.AppOpsPolicy;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -336,6 +337,9 @@ public class VoiceInteractionManagerService extends SystemService {
/** The start value of showSessionId */
private static final int SHOW_SESSION_START_ID = 0;
+ private final boolean IS_HDS_REQUIRED = AppOpsPolicy.isHotwordDetectionServiceRequired(
+ mContext.getPackageManager());
+
@GuardedBy("this")
private int mShowSessionId = SHOW_SESSION_START_ID;
@@ -393,8 +397,14 @@ public class VoiceInteractionManagerService extends SystemService {
}
try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
originatorIdentity)) {
+ if (!IS_HDS_REQUIRED) {
+ // For devices which still have hotword exemption, any client (not just HDS
+ // clients) are trusted.
+ // TODO (b/292012931) remove once trusted uniformly required.
+ forHotwordDetectionService = true;
+ }
return new SoundTriggerSession(mSoundTriggerInternal.attach(client,
- moduleProperties, forHotwordDetectionService));
+ moduleProperties, forHotwordDetectionService), originatorIdentity);
}
}
@@ -1674,10 +1684,13 @@ public class VoiceInteractionManagerService extends SystemService {
final SoundTriggerInternal.Session mSession;
private IHotwordRecognitionStatusCallback mSessionExternalCallback;
private IRecognitionStatusCallback mSessionInternalCallback;
+ private final Identity mVoiceInteractorIdentity;
SoundTriggerSession(
- SoundTriggerInternal.Session session) {
+ SoundTriggerInternal.Session session,
+ Identity voiceInteractorIdentity) {
mSession = session;
+ mVoiceInteractorIdentity = voiceInteractorIdentity;
}
@Override
@@ -1731,7 +1744,8 @@ public class VoiceInteractionManagerService extends SystemService {
if (mSessionExternalCallback == null
|| mSessionInternalCallback == null
|| callback.asBinder() != mSessionExternalCallback.asBinder()) {
- mSessionInternalCallback = createSoundTriggerCallbackLocked(callback);
+ mSessionInternalCallback = createSoundTriggerCallbackLocked(callback,
+ mVoiceInteractorIdentity);
mSessionExternalCallback = callback;
}
}
@@ -1752,7 +1766,8 @@ public class VoiceInteractionManagerService extends SystemService {
if (mSessionExternalCallback == null
|| mSessionInternalCallback == null
|| callback.asBinder() != mSessionExternalCallback.asBinder()) {
- soundTriggerCallback = createSoundTriggerCallbackLocked(callback);
+ soundTriggerCallback = createSoundTriggerCallbackLocked(callback,
+ mVoiceInteractorIdentity);
Slog.w(TAG, "stopRecognition() called with a different callback than"
+ "startRecognition()");
} else {
@@ -2090,6 +2105,7 @@ public class VoiceInteractionManagerService extends SystemService {
pw.println(" mTemporarilyDisabled: " + mTemporarilyDisabled);
pw.println(" mCurUser: " + mCurUser);
pw.println(" mCurUserSupported: " + mCurUserSupported);
+ pw.println(" mIsHdsRequired: " + IS_HDS_REQUIRED);
dumpSupportedUsers(pw, " ");
mDbHelper.dump(pw);
if (mImpl == null) {
@@ -2165,11 +2181,13 @@ public class VoiceInteractionManagerService extends SystemService {
}
private IRecognitionStatusCallback createSoundTriggerCallbackLocked(
- IHotwordRecognitionStatusCallback callback) {
+ IHotwordRecognitionStatusCallback callback,
+ Identity voiceInteractorIdentity) {
if (mImpl == null) {
return null;
}
- return mImpl.createSoundTriggerCallbackLocked(callback);
+ return mImpl.createSoundTriggerCallbackLocked(mContext, callback,
+ voiceInteractorIdentity);
}
class RoleObserver implements OnRoleHoldersChangedListener {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 0ad86c11d29a..5d88a65ce29e 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -877,12 +877,13 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
}
public IRecognitionStatusCallback createSoundTriggerCallbackLocked(
- IHotwordRecognitionStatusCallback callback) {
+ Context context, IHotwordRecognitionStatusCallback callback,
+ Identity voiceInteractorIdentity) {
if (DEBUG) {
Slog.d(TAG, "createSoundTriggerCallbackLocked");
}
- return new HotwordDetectionConnection.SoundTriggerCallback(callback,
- mHotwordDetectionConnection, mInfo.getServiceInfo().applicationInfo.uid);
+ return new HotwordDetectionConnection.SoundTriggerCallback(context, callback,
+ mHotwordDetectionConnection, voiceInteractorIdentity);
}
private static ServiceInfo getServiceInfoLocked(@NonNull ComponentName componentName,
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 64e43568e4d6..8e4ec0914563 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -151,6 +151,15 @@ public class SubscriptionManager {
"restoreSimSpecificSettings";
/**
+ * The key of the boolean flag indicating whether restoring subscriptions actually changes
+ * the subscription database or not.
+ *
+ * @hide
+ */
+ public static final String RESTORE_SIM_SPECIFIC_SETTINGS_DATABASE_UPDATED =
+ "restoreSimSpecificSettingsDatabaseUpdated";
+
+ /**
* Key to the backup & restore data byte array in the Bundle that is returned by {@link
* #getAllSimSpecificSettingsForBackup()} or to be pass in to {@link
* #restoreAllSimSpecificSettingsFromBackup(byte[])}.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
index 45cd65d9776c..45176448a9f4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
@@ -17,9 +17,12 @@
package com.android.server.wm.flicker.activityembedding
import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.BaseTest
import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
import org.junit.Before
+import org.junit.Test
abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(flicker) {
val testApp = ActivityEmbeddingAppHelper(instrumentation)
@@ -29,4 +32,14 @@ abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(
// The test should only be run on devices that support ActivityEmbedding.
ActivityEmbeddingAppHelper.assumeActivityEmbeddingSupportedDevice()
}
+
+ /** Asserts the background animation layer is never visible during bounds change transition. */
+ @Presubmit
+ @Test
+ fun backgroundLayerNeverVisible() {
+ val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
+ flicker.assertLayers {
+ isInvisible(backgroundColorLayer)
+ }
+ }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
new file mode 100644
index 000000000000..badd876ae321
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test changing split ratio at runtime on a horizona split.
+ *
+ * Setup: Launch A|B in horizontal split with B being the secondary activity, by default A and B
+ * windows are equal in size. B is on the top and A is on the bottom.
+ * Transitions:
+ * Change the split ratio to A:B=0.7:0.3, expect bounds change for both A and B.
+ *
+ * To run this test: `atest FlickerTests:HorizontalSplitChangeRatioTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) :
+ ActivityEmbeddingTestBase(flicker) {
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setExpectedRotationCheckEnabled(false)
+ testApp.launchViaIntent(wmHelper)
+ testApp.launchSecondaryActivityHorizontally(wmHelper)
+ startDisplayBounds =
+ wmHelper.currentState.layerState.physicalDisplayBounds
+ ?: error("Display not found")
+ }
+ transitions {
+ testApp.changeSecondaryActivityRatio(wmHelper)
+ }
+ teardown {
+ tapl.goHome()
+ testApp.exit(wmHelper)
+ }
+ }
+
+ /** Assert the Main activity window is always visible. */
+ @Presubmit
+ @Test
+ fun mainActivityWindowIsAlwaysVisible() {
+ flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Main activity window is always visible. */
+ @Presubmit
+ @Test
+ fun mainActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Secondary activity window is always visible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityWindowIsAlwaysVisible() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Secondary activity window is always visible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Main and Secondary activity change height during the transition. */
+ @Presubmit
+ @Test
+ fun secondaryActivityAdjustsHeightRuntime() {
+ flicker.assertLayersStart {
+ val topLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ val bottomLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(topLayerRegion.region.height).isEqual(bottomLayerRegion.region.height)
+ check { "width" }
+ .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
+ topLayerRegion.notOverlaps(bottomLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+
+ flicker.assertLayersEnd {
+ val topLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ val bottomLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(topLayerRegion.region.height).isLower(bottomLayerRegion.region.height)
+ check { "height" }
+ .that(
+ topLayerRegion.region.height / 0.3f -
+ bottomLayerRegion.region.height / 0.7f)
+ .isLower(0.1f)
+ check { "width" }
+ .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
+ topLayerRegion.notOverlaps(bottomLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+ }
+
+ companion object {
+ /** {@inheritDoc} */
+ private var startDisplayBounds = Rect.EMPTY
+ /**
+ * Creates the test configurations.
+ *
+ * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+ }
+} \ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
index 27de12e7dfdb..404f3290f04a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
@@ -142,14 +142,6 @@ class OpenThirdActivityOverSplitTest(flicker: LegacyFlickerTest) :
}
}
- /** Assert the background animation layer is never visible during transition. */
- @Presubmit
- @Test
- fun backgroundLayerNeverVisible() {
- val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
- flicker.assertLayers { isInvisible(backgroundColorLayer) }
- }
-
companion object {
/** {@inheritDoc} */
private var startDisplayBounds = Rect.EMPTY
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
index c05dc324ac9e..d3001d8fdcaf 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.activityembedding
+import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import android.tools.common.datatypes.Rect
import android.tools.common.datatypes.Region
@@ -65,16 +66,6 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding
}
}
- /** Assert the background animation layer is never visible during bounds change transition. */
- @Presubmit
- @Test
- fun backgroundLayerNeverVisible() {
- val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
- flicker.assertLayers {
- isInvisible(backgroundColorLayer)
- }
- }
-
/** Trampoline activity should finish itself before the end of this test. */
@Presubmit
@Test
@@ -178,6 +169,7 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding
}
}
+ @FlakyTest(bugId = 290736037)
/** Main activity should go from fullscreen to being a split with secondary activity. */
@Presubmit
@Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index ade1491fa17b..883c7e6d5785 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -45,18 +45,22 @@ constructor(
* based on the split pair rule.
*/
fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) {
- val launchButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")),
- FIND_TIMEOUT
- )
- require(launchButton != null) { "Can't find launch secondary activity button on screen." }
- launchButton.click()
- wmHelper
- .StateSyncBuilder()
- .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .waitForAndVerify()
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_button")
+ }
+
+ /**
+ * Clicks the button to launch the secondary activity in RTL, which should split with the main
+ * activity based on the split pair rule.
+ */
+ fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_rtl_button")
+ }
+
+ /**
+ * Clicks the button to launch the secondary activity in a horizontal split.
+ */
+ fun launchSecondaryActivityHorizontally(wmHelper: WindowManagerStateHelper) {
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_horizontally_button")
}
/** Clicks the button to launch a third activity over a secondary activity. */
@@ -101,16 +105,38 @@ constructor(
*/
fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) {
val finishButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
- FIND_TIMEOUT
- )
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
+ FIND_TIMEOUT
+ )
require(finishButton != null) { "Can't find finish secondary activity button on screen." }
finishButton.click()
wmHelper
- .StateSyncBuilder()
- .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
- .waitForAndVerify()
+ .StateSyncBuilder()
+ .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
+ .waitForAndVerify()
+ }
+
+ /**
+ * Clicks the button to toggle the split ratio of secondary activity.
+ */
+ fun changeSecondaryActivityRatio(wmHelper: WindowManagerStateHelper) {
+ val launchButton =
+ uiDevice.wait(
+ Until.findObject(
+ By.res(getPackage(),
+ "toggle_split_ratio_button")),
+ FIND_TIMEOUT
+ )
+ require(launchButton != null) {
+ "Can't find toggle ratio for secondary activity button on screen."
+ }
+ launchButton.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withTransitionSnapshotGone()
+ .waitForAndVerify()
}
fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) {
@@ -149,25 +175,19 @@ constructor(
.waitForAndVerify()
}
- /**
- * Clicks the button to launch the secondary activity in RTL, which should split with the main
- * activity based on the split pair rule.
- */
- fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+ private fun launchSecondaryActivityFromButton(
+ wmHelper: WindowManagerStateHelper, buttonName: String) {
val launchButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "launch_secondary_activity_rtl_button")),
- FIND_TIMEOUT
- )
+ uiDevice.wait(Until.findObject(By.res(getPackage(), buttonName)), FIND_TIMEOUT)
require(launchButton != null) {
- "Can't find launch secondary activity rtl button on screen."
+ "Can't find launch secondary activity button : " + buttonName + "on screen."
}
launchButton.click()
wmHelper
- .StateSyncBuilder()
- .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .waitForAndVerify()
+ .StateSyncBuilder()
+ .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+ .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+ .waitForAndVerify()
}
/**
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
index e32a7092bf5d..86c21906163f 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
@@ -38,6 +38,14 @@
android:text="Launch Secondary Activity in RTL" />
<Button
+ android:id="@+id/launch_secondary_activity_horizontally_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:onClick="launchSecondaryActivity"
+ android:tag="BOTTOM_TO_TOP"
+ android:text="Launch Secondary Activity Horizontally" />
+
+ <Button
android:id="@+id/launch_placeholder_split_button"
android:layout_width="wrap_content"
android:layout_height="48dp"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
index 135140aa2377..6d4de995bd73 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
@@ -35,6 +35,14 @@
android:onClick="launchThirdActivity"
android:text="Launch a third activity" />
+ <ToggleButton
+ android:id="@+id/toggle_split_ratio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:textOn="Ratio 0.5"
+ android:textOff="Ratio 0.3"
+ android:checked="false" />
+
<Button
android:id="@+id/secondary_enter_pip_button"
android:layout_width="wrap_content"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
index 3b1a8599f3e1..23fa91c37728 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
@@ -22,6 +22,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
import androidx.window.embedding.EmbeddingAspectRatio;
@@ -152,6 +153,9 @@ public class ActivityEmbeddingMainActivity extends Activity {
if (layoutDirectionStr.equals(LayoutDirection.LEFT_TO_RIGHT.toString())) {
return LayoutDirection.LEFT_TO_RIGHT;
}
+ if (layoutDirectionStr.equals(LayoutDirection.BOTTOM_TO_TOP.toString())) {
+ return LayoutDirection.BOTTOM_TO_TOP;
+ }
if (layoutDirectionStr.equals(LayoutDirection.RIGHT_TO_LEFT.toString())) {
return LayoutDirection.RIGHT_TO_LEFT;
}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index ee087ef9be2c..29cbf01dc6da 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -22,6 +22,11 @@ import android.app.PictureInPictureParams;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
+import android.widget.ToggleButton;
+
+import androidx.window.embedding.SplitAttributes;
+import androidx.window.embedding.SplitAttributesCalculatorParams;
+import androidx.window.embedding.SplitController;
/**
* Activity to be used as the secondary activity to split with
@@ -29,18 +34,41 @@ import android.view.View;
*/
public class ActivityEmbeddingSecondaryActivity extends Activity {
+ private SplitController mSplitController;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_embedding_secondary_activity_layout);
findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW);
findViewById(R.id.finish_secondary_activity_button).setOnClickListener(
- new View.OnClickListener() {
+ new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
- });
+ });
+ mSplitController = SplitController.getInstance(this);
+ final ToggleButton splitRatio = findViewById(R.id.toggle_split_ratio_button);
+ mSplitController.setSplitAttributesCalculator(params -> {
+ return new SplitAttributes.Builder()
+ .setSplitType(
+ SplitAttributes.SplitType.ratio(
+ splitRatio.isChecked() ? 0.7f : 0.5f)
+ )
+ .setLayoutDirection(
+ params.getDefaultSplitAttributes()
+ .getLayoutDirection())
+ .build();
+ });
+ splitRatio.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // This triggers a recalcuation of splitatributes.
+ mSplitController.invalidateTopVisibleSplitAttributes();
+ }
+ });
findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
new View.OnClickListener() {
@Override
diff --git a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
index 24a567130ff0..d3eeac147c2a 100644
--- a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
+++ b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
@@ -129,7 +129,7 @@ class MotionPredictorTest {
// Prediction will happen for t=12 (since it is the next input interval after the requested
// time, 8, plus the model offset, 1).
assertEquals(12, predicted!!.eventTime)
- assertEquals(30f, predicted.x, /*delta=*/5f)
+ assertEquals(30f, predicted.x, /*delta=*/10f)
assertEquals(60f, predicted.y, /*delta=*/15f)
}
}