summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp8
-rw-r--r--Ravenwood.bp24
-rw-r--r--config/Android.bp2
-rw-r--r--core/java/android/app/ActivityManager.java36
-rw-r--r--core/java/android/app/ActivityManagerInternal.java12
-rw-r--r--core/java/android/app/ITaskStackListener.aidl5
-rw-r--r--core/java/android/app/TaskStackListener.java3
-rw-r--r--core/java/android/app/admin/DeviceAdminInfo.java34
-rw-r--r--core/java/android/app/admin/PackageSetPolicyValue.java (renamed from core/java/android/app/admin/StringSetPolicyValue.java)26
-rw-r--r--core/java/android/app/admin/SystemUpdatePolicy.java5
-rw-r--r--core/java/android/app/admin/flags/flags.aconfig21
-rw-r--r--core/java/android/app/notification.aconfig7
-rw-r--r--core/java/android/content/AttributionSource.java3
-rw-r--r--core/java/android/content/PermissionChecker.java3
-rw-r--r--core/java/android/content/pm/flags.aconfig8
-rw-r--r--core/java/android/net/vcn/flags.aconfig10
-rw-r--r--core/java/android/permission/PermissionManager.java2
-rw-r--r--core/java/android/tracing/inputmethod/InputMethodDataSource.java58
-rw-r--r--core/java/android/view/ImeBackAnimationController.java4
-rw-r--r--core/java/android/view/InputWindowHandle.java2
-rw-r--r--core/java/android/view/PointerIcon.java4
-rw-r--r--core/java/android/view/View.java9
-rw-r--r--core/java/android/view/ViewRootImpl.java45
-rw-r--r--core/java/android/view/WindowManager.java7
-rw-r--r--core/java/android/view/autofill/AutofillManager.java13
-rw-r--r--core/java/android/view/autofill/IAutoFillManager.aidl2
-rw-r--r--core/java/android/view/flags/view_flags.aconfig2
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java23
-rw-r--r--core/java/android/window/TaskFragmentOrganizer.java15
-rw-r--r--core/java/android/window/flags/windowing_sdk.aconfig7
-rw-r--r--core/java/com/android/internal/content/PackageMonitor.java4
-rw-r--r--core/java/com/android/internal/inputmethod/ImeTracing.java6
-rw-r--r--core/java/com/android/internal/inputmethod/ImeTracingPerfettoImpl.java178
-rw-r--r--core/java/com/android/internal/jank/Cuj.java14
-rw-r--r--core/java/com/android/internal/util/ProcFileReader.java6
-rw-r--r--core/java/com/android/internal/widget/LockPatternView.java9
-rw-r--r--core/res/AndroidManifest.xml1
-rw-r--r--core/res/res/drawable/ic_thread_network.xml25
-rw-r--r--core/res/res/layout/side_fps_toast.xml18
-rw-r--r--core/res/res/values/attrs_manifest.xml3
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java17
-rw-r--r--core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java40
-rw-r--r--data/etc/platform.xml6
-rw-r--r--data/keyboards/Android.bp6
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java14
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java20
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java2
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java46
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig7
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml20
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java77
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java43
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt96
-rw-r--r--libs/WindowManager/Shell/tests/OWNERS2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java51
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java16
-rw-r--r--libs/input/PointerController.cpp16
-rw-r--r--libs/input/PointerController.h2
-rw-r--r--libs/input/SpriteController.cpp33
-rw-r--r--libs/input/SpriteController.h10
-rw-r--r--libs/input/TouchSpotController.cpp7
-rw-r--r--libs/input/TouchSpotController.h5
-rw-r--r--libs/input/tests/PointerController_test.cpp39
-rw-r--r--libs/input/tests/mocks/MockSprite.h1
-rw-r--r--media/java/android/media/MediaRouter2.java38
-rw-r--r--nfc/java/android/nfc/cardemulation/PollingFrame.java11
-rw-r--r--packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml3
-rw-r--r--packages/CredentialManager/res/values/strings.xml4
-rw-r--r--packages/CredentialManager/res/xml/autofill_service_configuration.xml2
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java59
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java23
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java4
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java1
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java24
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt51
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt30
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt4
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt127
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt18
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt6
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt6
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt8
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt3
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt1
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt67
-rw-r--r--packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt33
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt6
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt8
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt3
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt3
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java3
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java16
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java40
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java75
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt4
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java24
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java86
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtilsTest.java27
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java30
-rw-r--r--packages/SystemUI/OWNERS7
-rw-r--r--packages/SystemUI/TEST_MAPPING5
-rw-r--r--packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java18
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig12
-rw-r--r--packages/SystemUI/checks/Android.bp3
-rw-r--r--packages/SystemUI/checks/src/com/android/internal/systemui/lint/SingletonAndroidComponentDetector.kt160
-rw-r--r--packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt1
-rw-r--r--packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt3
-rw-r--r--packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SingletonAndroidComponentDetectorTest.kt182
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt29
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt29
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt54
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt20
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt24
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt71
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt21
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt51
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt71
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt44
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt270
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt22
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt6
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt19
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt19
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt4
-rw-r--r--packages/SystemUI/compose/scene/OWNERS3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt92
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt19
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt29
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt85
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt11
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt66
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt3
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt69
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt112
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt210
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt5
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt59
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt74
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt254
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt3
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt130
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt138
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt41
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt40
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt42
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt40
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt124
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileDataInteractorTest.kt71
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileUserActionInteractorTest.kt100
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt111
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt45
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt124
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt65
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java12
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt249
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt)160
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt231
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt31
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java14
-rw-r--r--packages/SystemUI/res/drawable/shelf_action_chip_divider.xml2
-rw-r--r--packages/SystemUI/res/layout/clipboard_overlay2.xml191
-rw-r--r--packages/SystemUI/res/values-land/styles.xml6
-rw-r--r--packages/SystemUI/res/values-sw600dp-land/styles.xml6
-rw-r--r--packages/SystemUI/res/values-sw600dp-port/styles.xml2
-rw-r--r--packages/SystemUI/res/values-sw720dp-land/styles.xml6
-rw-r--r--packages/SystemUI/res/values-sw720dp-port/styles.xml2
-rw-r--r--packages/SystemUI/res/values/config.xml6
-rw-r--r--packages/SystemUI/res/values/dimens.xml1
-rw-r--r--packages/SystemUI/res/values/strings.xml4
-rw-r--r--packages/SystemUI/res/values/styles.xml14
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java8
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/ExpandHelper.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepository.kt87
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java20
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java116
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt97
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt68
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt81
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java38
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileDataInteractor.kt45
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/model/OneHandedModeTileModel.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt133
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java28
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt134
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt102
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt72
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt118
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt84
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt69
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt111
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt42
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java23
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java32
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt64
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java115
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt44
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java50
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java218
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt119
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt118
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeOneHandedModeRepository.kt39
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt56
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractorKosmos.kt34
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/onehanded/OneHandedModeTileKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt60
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt52
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt14
-rw-r--r--ravenwood/Android.bp32
-rwxr-xr-xravenwood/scripts/ravenwood-stats-collector.sh10
-rw-r--r--ravenwood/texts/ravenwood-framework-policies.txt (renamed from ravenwood/texts/framework-minus-apex-ravenwood-policies.txt)0
-rw-r--r--ravenwood/texts/ravenwood-services-policies.txt (renamed from ravenwood/texts/services.core-ravenwood-policies.txt)0
-rw-r--r--services/accessibility/accessibility.aconfig10
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java86
-rw-r--r--services/autofill/java/com/android/server/autofill/AutofillManagerService.java6
-rw-r--r--services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java52
-rw-r--r--services/autofill/java/com/android/server/autofill/SaveEventLogger.java45
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java15
-rw-r--r--services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java2
-rw-r--r--services/core/java/com/android/server/SensitiveContentProtectionManagerService.java11
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java10
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java23
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java7
-rw-r--r--services/core/java/com/android/server/am/AppRestrictionController.java2
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java1
-rw-r--r--services/core/java/com/android/server/apphibernation/AppHibernationService.java6
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceBroker.java11
-rw-r--r--services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java3
-rw-r--r--services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java15
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java11
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodBindingController.java24
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java172
-rw-r--r--services/core/java/com/android/server/inputmethod/UserDataRepository.java19
-rw-r--r--services/core/java/com/android/server/notification/GroupHelper.java39
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java115
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig7
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerService.java39
-rw-r--r--services/core/java/com/android/server/os/BugreportManagerServiceImpl.java50
-rw-r--r--services/core/java/com/android/server/os/core_os_flags.aconfig10
-rw-r--r--services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java27
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerServiceUtils.java18
-rw-r--r--services/core/java/com/android/server/pm/ShortcutService.java16
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java1
-rw-r--r--services/core/java/com/android/server/power/batterysaver/flags.aconfig2
-rw-r--r--services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java23
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperCropper.java8
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java3
-rw-r--r--services/core/java/com/android/server/wm/ActivityClientController.java21
-rw-r--r--services/core/java/com/android/server/wm/ActivityMetricsLogger.java11
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java12
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java4
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java4
-rw-r--r--services/core/java/com/android/server/wm/InputConfigAdapter.java4
-rw-r--r--services/core/java/com/android/server/wm/RecentTasks.java26
-rw-r--r--services/core/java/com/android/server/wm/Task.java7
-rw-r--r--services/core/java/com/android/server/wm/TaskChangeNotificationController.java17
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java26
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java82
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java7
-rw-r--r--services/credentials/java/com/android/server/credentials/CredentialManagerService.java28
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java4
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java10
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java4
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java38
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java4
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java5
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java4
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/PackageSetPolicySerializer.java (renamed from services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java)12
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/PackageSetUnion.java (renamed from services/devicepolicy/java/com/android/server/devicepolicy/StringSetUnion.java)13
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java5
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java8
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java1
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java4
-rw-r--r--services/java/com/android/server/SystemServer.java3
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java10
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java2
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java35
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java10
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java60
-rw-r--r--services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java85
-rw-r--r--services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java6
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java253
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java63
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java8
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java20
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java41
-rw-r--r--telephony/java/android/telephony/CarrierConfigManager.java4
-rw-r--r--telephony/java/android/telephony/PhoneNumberUtils.java13
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java4
-rw-r--r--tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java53
-rw-r--r--tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java1
-rw-r--r--tools/aapt2/link/ManifestFixer.cpp10
-rw-r--r--tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt26
530 files changed, 11070 insertions, 3030 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index ab5d503eac62..0ccdf37f0c2c 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1043,12 +1043,20 @@ aconfig_declarations {
name: "device_policy_aconfig_flags",
package: "android.app.admin.flags",
container: "system",
+ exportable: true,
srcs: [
"core/java/android/app/admin/flags/flags.aconfig",
],
}
java_aconfig_library {
+ name: "device_policy_exported_aconfig_flags_lib",
+ aconfig_declarations: "device_policy_aconfig_flags",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ mode: "exported",
+}
+
+java_aconfig_library {
name: "device_policy_aconfig_flags_lib",
aconfig_declarations: "device_policy_aconfig_flags",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 3ab0934e9acc..255ec924abee 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -30,7 +30,7 @@ java_genrule {
name: "framework-minus-apex.ravenwood-base",
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
- "@$(location ravenwood/texts/ravenwood-standard-options.txt) " +
+ "@$(location :ravenwood-standard-options) " +
"--debug-log $(location hoststubgen_framework-minus-apex.log) " +
"--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
@@ -42,13 +42,13 @@ java_genrule {
"--gen-input-dump-file $(location hoststubgen_dump.txt) " +
"--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location ravenwood/texts/framework-minus-apex-ravenwood-policies.txt) " +
- "--annotation-allowed-classes-file $(location ravenwood/texts/ravenwood-annotation-allowed-classes.txt) ",
+ "--policy-override-file $(location :ravenwood-framework-policies) " +
+ "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
srcs: [
":framework-minus-apex-for-hoststubgen",
- "ravenwood/texts/framework-minus-apex-ravenwood-policies.txt",
- "ravenwood/texts/ravenwood-standard-options.txt",
- "ravenwood/texts/ravenwood-annotation-allowed-classes.txt",
+ ":ravenwood-framework-policies",
+ ":ravenwood-standard-options",
+ ":ravenwood-annotation-allowed-classes",
],
out: [
"ravenwood.jar",
@@ -118,7 +118,7 @@ java_genrule {
name: "services.core.ravenwood-base",
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
- "@$(location ravenwood/texts/ravenwood-standard-options.txt) " +
+ "@$(location :ravenwood-standard-options) " +
"--debug-log $(location hoststubgen_services.core.log) " +
"--stats-file $(location hoststubgen_services.core_stats.csv) " +
@@ -130,13 +130,13 @@ java_genrule {
"--gen-input-dump-file $(location hoststubgen_dump.txt) " +
"--in-jar $(location :services.core-for-hoststubgen) " +
- "--policy-override-file $(location ravenwood/texts/services.core-ravenwood-policies.txt) " +
- "--annotation-allowed-classes-file $(location ravenwood/texts/ravenwood-annotation-allowed-classes.txt) ",
+ "--policy-override-file $(location :ravenwood-services-policies) " +
+ "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
srcs: [
":services.core-for-hoststubgen",
- "ravenwood/texts/services.core-ravenwood-policies.txt",
- "ravenwood/texts/ravenwood-standard-options.txt",
- "ravenwood/texts/ravenwood-annotation-allowed-classes.txt",
+ ":ravenwood-services-policies",
+ ":ravenwood-standard-options",
+ ":ravenwood-annotation-allowed-classes",
],
out: [
"ravenwood.jar",
diff --git a/config/Android.bp b/config/Android.bp
index adce203e1140..c9948c31f1c3 100644
--- a/config/Android.bp
+++ b/config/Android.bp
@@ -33,7 +33,7 @@ prebuilt_etc {
name: "preloaded-classes",
src: "preloaded-classes",
filename: "preloaded-classes",
- installable: false,
+ no_full_install: true,
}
filegroup {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 5e9fdfbb6f6f..1e824a19193c 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -6257,6 +6257,29 @@ public class ActivityManager {
* {@link #RESTRICTION_LEVEL_ADAPTIVE} is a normal state, where there is default lifecycle
* management applied to the app. Also, {@link #RESTRICTION_LEVEL_EXEMPTED} is used when the
* app is being put in a power-save allowlist.
+ * <p>
+ * Example arguments when user force-stops an app from Settings:
+ * <pre>
+ * noteAppRestrictionEnabled(
+ * "com.example.app",
+ * appUid,
+ * RESTRICTION_LEVEL_FORCE_STOPPED,
+ * true,
+ * RESTRICTION_REASON_USER,
+ * "settings",
+ * 0);
+ * </pre>
+ * Example arguments when app is put in restricted standby bucket for exceeding X hours of jobs:
+ * <pre>
+ * noteAppRestrictionEnabled(
+ * "com.example.app",
+ * appUid,
+ * RESTRICTION_LEVEL_RESTRICTED_BUCKET,
+ * true,
+ * RESTRICTION_REASON_SYSTEM_HEALTH,
+ * "job_duration",
+ * X * 3600 * 1000L);
+ * </pre>
*
* @param packageName the package name of the app
* @param uid the uid of the app
@@ -6264,11 +6287,20 @@ public class ActivityManager {
* @param enabled whether the state is being applied or removed
* @param reason the reason for the restriction state change, from {@code RestrictionReason}
* @param subReason a string sub reason limited to 16 characters that specifies additional
- * information about the reason for restriction.
+ * information about the reason for restriction. This string must only contain
+ * reasons related to excessive system resource usage or in some cases,
+ * source of the restriction. This string must not contain any details that
+ * identify user behavior beyond their actions to restrict/unrestrict/launch
+ * apps in some way.
+ * Examples of system resource usage: wakelock, wakeups, mobile_data,
+ * binder_calls, memory, excessive_threads, excessive_cpu, gps_scans, etc.
+ * Examples of user actions: settings, notification, command_line, launch, etc.
+ *
* @param threshold for reasons that are due to exceeding some threshold, the threshold value
* must be specified. The unit of the threshold depends on the reason and/or
* subReason. For time, use milliseconds. For memory, use KB. For count, use
- * the actual count or normalized as per-hour. For power, use milliwatts. Etc.
+ * the actual count or if rate limited, normalized per-hour. For power,
+ * use milliwatts. For CPU, use mcycles.
*
* @hide
*/
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index e66f7fe195e6..d8df447982a0 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -1270,4 +1270,16 @@ public abstract class ActivityManagerInternal {
* @hide
*/
public abstract boolean shouldDelayHomeLaunch(int userId);
+
+ /**
+ * Add a startup timestamp to the most recent start of the specified process.
+ *
+ * @param key The {@link ApplicationStartInfo} start timestamp key of the timestamp to add.
+ * @param timestampNs The clock monotonic timestamp to add in nanoseconds.
+ * @param uid The UID of the process to add this timestamp to.
+ * @param pid The process id of the process to add this timestamp to.
+ * @param userId The userId in the multi-user environment.
+ */
+ public abstract void addStartInfoTimestamp(int key, long timestampNs, int uid, int pid,
+ int userId);
}
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl
index 3c6ff2865d04..f2228f94ff01 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -145,6 +145,11 @@ oneway interface ITaskStackListener {
void onTaskSnapshotChanged(int taskId, in TaskSnapshot snapshot);
/**
+ * Called when a task snapshot become invalidated.
+ */
+ void onTaskSnapshotInvalidated(int taskId);
+
+ /**
* Reports that an Activity received a back key press when there were no additional activities
* on the back stack.
*
diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java
index 0290cee94dc3..36f61fd3ef59 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -178,6 +178,9 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub {
}
@Override
+ public void onTaskSnapshotInvalidated(int taskId) { }
+
+ @Override
public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo)
throws RemoteException {
}
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 9ef8b38666c6..46c9e781bed1 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -21,6 +21,7 @@ import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_US
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.app.admin.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -176,6 +177,10 @@ public final class DeviceAdminInfo implements Parcelable {
* provisioned into "affiliated" mode when on a Headless System User Mode device.
*
* <p>This mode adds a Profile Owner to all users other than the user the Device Owner is on.
+ *
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+ * DPCs should set the value of attribute "headless-device-owner-mode" inside the
+ * "headless-system-user" tag as "affiliated".
*/
public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;
@@ -185,6 +190,10 @@ public final class DeviceAdminInfo implements Parcelable {
*
* <p>This mode only allows a single secondary user on the device blocking the creation of
* additional secondary users.
+ *
+ * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
+ * DPCs should set the value of attribute "headless-device-owner-mode" inside the
+ * "headless-system-user" tag as "single_user".
*/
@FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
@@ -383,17 +392,30 @@ public final class DeviceAdminInfo implements Parcelable {
}
mSupportsTransferOwnership = true;
} else if (tagName.equals("headless-system-user")) {
- String deviceOwnerModeStringValue =
- parser.getAttributeValue(null, "device-owner-mode");
+ String deviceOwnerModeStringValue = null;
+ if (Flags.headlessSingleUserCompatibilityFix()) {
+ deviceOwnerModeStringValue = parser.getAttributeValue(
+ null, "headless-device-owner-mode");
+ }
+ if (deviceOwnerModeStringValue == null) {
+ deviceOwnerModeStringValue =
+ parser.getAttributeValue(null, "device-owner-mode");
+ }
- if (deviceOwnerModeStringValue.equalsIgnoreCase("unsupported")) {
+ if ("unsupported".equalsIgnoreCase(deviceOwnerModeStringValue)) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
- } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) {
+ } else if ("affiliated".equalsIgnoreCase(deviceOwnerModeStringValue)) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
- } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) {
+ } else if ("single_user".equalsIgnoreCase(deviceOwnerModeStringValue)) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
} else {
- throw new XmlPullParserException("headless-system-user mode must be valid");
+ if (Flags.headlessSingleUserCompatibilityFix()) {
+ Log.e(TAG, "Unknown headless-system-user mode: "
+ + deviceOwnerModeStringValue);
+ } else {
+ throw new XmlPullParserException(
+ "headless-system-user mode must be valid");
+ }
}
}
}
diff --git a/core/java/android/app/admin/StringSetPolicyValue.java b/core/java/android/app/admin/PackageSetPolicyValue.java
index 12b11f4ba687..8b253a23a299 100644
--- a/core/java/android/app/admin/StringSetPolicyValue.java
+++ b/core/java/android/app/admin/PackageSetPolicyValue.java
@@ -28,18 +28,18 @@ import java.util.Set;
/**
* @hide
*/
-public final class StringSetPolicyValue extends PolicyValue<Set<String>> {
+public final class PackageSetPolicyValue extends PolicyValue<Set<String>> {
- public StringSetPolicyValue(@NonNull Set<String> value) {
+ public PackageSetPolicyValue(@NonNull Set<String> value) {
super(value);
if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) {
- for (String str : value) {
- PolicySizeVerifier.enforceMaxStringLength(str, "policyValue");
+ for (String packageName : value) {
+ PolicySizeVerifier.enforceMaxPackageNameLength(packageName);
}
}
}
- public StringSetPolicyValue(Parcel source) {
+ public PackageSetPolicyValue(Parcel source) {
this(readValues(source));
}
@@ -56,7 +56,7 @@ public final class StringSetPolicyValue extends PolicyValue<Set<String>> {
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- StringSetPolicyValue other = (StringSetPolicyValue) o;
+ PackageSetPolicyValue other = (PackageSetPolicyValue) o;
return Objects.equals(getValue(), other.getValue());
}
@@ -67,7 +67,7 @@ public final class StringSetPolicyValue extends PolicyValue<Set<String>> {
@Override
public String toString() {
- return "StringSetPolicyValue { " + getValue() + " }";
+ return "PackageNameSetPolicyValue { " + getValue() + " }";
}
@Override
@@ -84,16 +84,16 @@ public final class StringSetPolicyValue extends PolicyValue<Set<String>> {
}
@NonNull
- public static final Creator<StringSetPolicyValue> CREATOR =
- new Creator<StringSetPolicyValue>() {
+ public static final Creator<PackageSetPolicyValue> CREATOR =
+ new Creator<PackageSetPolicyValue>() {
@Override
- public StringSetPolicyValue createFromParcel(Parcel source) {
- return new StringSetPolicyValue(source);
+ public PackageSetPolicyValue createFromParcel(Parcel source) {
+ return new PackageSetPolicyValue(source);
}
@Override
- public StringSetPolicyValue[] newArray(int size) {
- return new StringSetPolicyValue[size];
+ public PackageSetPolicyValue[] newArray(int size) {
+ return new PackageSetPolicyValue[size];
}
};
}
diff --git a/core/java/android/app/admin/SystemUpdatePolicy.java b/core/java/android/app/admin/SystemUpdatePolicy.java
index 7320cea16451..dede5b5f48f3 100644
--- a/core/java/android/app/admin/SystemUpdatePolicy.java
+++ b/core/java/android/app/admin/SystemUpdatePolicy.java
@@ -78,6 +78,11 @@ import java.util.stream.Collectors;
*
* <h3>Developer guide</h3>
* To learn more, read <a href="{@docRoot}work/dpc/system-updates">Manage system updates</a>.
+ * <p><strong>Note:</strong> <a href="https://source.android.com/docs/core/ota/modular-system">
+ * Google Play system updates</a> (also called Mainline updates) are automatically downloaded
+ * but require a device reboot to be installed. Refer to the mainline section in
+ * <a href="{@docRoot}work/dpc/system-updates#mainline">Manage system
+ * updates</a> for further details.</p>
*
* @see DevicePolicyManager#setSystemUpdatePolicy
* @see DevicePolicyManager#getSystemUpdatePolicy
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 18914e120d52..83daa4524696 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -303,3 +303,24 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "headless_single_user_compatibility_fix"
+ namespace: "enterprise"
+ description: "Fix for compatibility issue introduced from using single_user mode on pre-Android V builds"
+ bug: "338050276"
+ is_exported: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "headless_single_min_target_sdk"
+ namespace: "enterprise"
+ description: "Only allow DPCs targeting Android V to provision into single user mode"
+ bug: "338588825"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index e3c367f8adce..63ffaa0f0630 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -39,6 +39,13 @@ flag {
}
flag {
+ name: "check_autogroup_before_post"
+ namespace: "systemui"
+ description: "Does a check to see if notification should be autogrouped before posting, and if so groups before post."
+ bug: "330214226"
+}
+
+flag {
name: "visit_risky_uris"
namespace: "systemui"
description: "Guards the security fix that ensures all URIs in intents and Person.java are valid"
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index af1301140358..b070742178e6 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -753,6 +753,9 @@ public final class AttributionSource implements Parcelable {
@FlaggedApi(Flags.FLAG_SET_NEXT_ATTRIBUTION_SOURCE)
public @NonNull Builder setNextAttributionSource(@NonNull AttributionSource value) {
checkNotUsed();
+ if (value == null) {
+ throw new IllegalArgumentException("Null AttributionSource not permitted.");
+ }
mBuilderFieldsSet |= 0x20;
mAttributionSourceState.next =
new AttributionSourceState[]{value.mAttributionSourceState};
diff --git a/core/java/android/content/PermissionChecker.java b/core/java/android/content/PermissionChecker.java
index 0e3217d7071d..cb8eb835154b 100644
--- a/core/java/android/content/PermissionChecker.java
+++ b/core/java/android/content/PermissionChecker.java
@@ -73,13 +73,12 @@ public final class PermissionChecker {
public static final int PERMISSION_GRANTED = PermissionCheckerManager.PERMISSION_GRANTED;
/**
- * The permission is denied. Applicable only to runtime and app op permissions.
+ * The permission is denied. Applicable only to runtime permissions.
*
* <p>Returned when:
* <ul>
* <li>the runtime permission is granted, but the corresponding app op is denied
* for runtime permissions.</li>
- * <li>the app ops is ignored for app op permissions.</li>
* </ul>
*
* @hide
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 205f1e9c1f5c..45591d79ee00 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -248,3 +248,11 @@ flag {
bug: "316916801"
is_fixed_read_only: true
}
+
+flag {
+ name: "package_restart_query_disabled_by_default"
+ namespace: "package_manager_service"
+ description: "Feature flag to register broadcast receiver only support package restart query."
+ bug: "300309050"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index fea2c253e743..9fe0befb4f97 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -45,4 +45,14 @@ flag{
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag{
+ name: "allow_disable_ipsec_loss_detector"
+ namespace: "vcn"
+ description: "Allow disabling IPsec packet loss detector"
+ bug: "336638836"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
} \ No newline at end of file
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index 55bb430d1523..7e51cb020196 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -112,7 +112,7 @@ public final class PermissionManager {
public static final int PERMISSION_GRANTED = 0;
/**
- * The permission is denied. Applicable only to runtime and app op permissions.
+ * The permission is denied. Applicable only to runtime permissions.
* <p>
* The app isn't expecting the permission to be denied so that a "no-op" action should be taken,
* such as returning an empty result.
diff --git a/core/java/android/tracing/inputmethod/InputMethodDataSource.java b/core/java/android/tracing/inputmethod/InputMethodDataSource.java
new file mode 100644
index 000000000000..5c5ad6947e4d
--- /dev/null
+++ b/core/java/android/tracing/inputmethod/InputMethodDataSource.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.inputmethod;
+
+import android.annotation.NonNull;
+import android.tracing.perfetto.DataSource;
+import android.tracing.perfetto.DataSourceInstance;
+import android.tracing.perfetto.StartCallbackArguments;
+import android.tracing.perfetto.StopCallbackArguments;
+import android.util.proto.ProtoInputStream;
+
+/**
+ * @hide
+ */
+public final class InputMethodDataSource
+ extends DataSource<DataSourceInstance, Void, Void> {
+ public static final String DATA_SOURCE_NAME = "android.inputmethod";
+
+ @NonNull
+ private final Runnable mOnStartCallback;
+ @NonNull
+ private final Runnable mOnStopCallback;
+
+ public InputMethodDataSource(@NonNull Runnable onStart, @NonNull Runnable onStop) {
+ super(DATA_SOURCE_NAME);
+ mOnStartCallback = onStart;
+ mOnStopCallback = onStop;
+ }
+
+ @Override
+ public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+ return new DataSourceInstance(this, instanceIndex) {
+ @Override
+ protected void onStart(StartCallbackArguments args) {
+ mOnStartCallback.run();
+ }
+
+ @Override
+ protected void onStop(StopCallbackArguments args) {
+ mOnStopCallback.run();
+ }
+ };
+ }
+}
diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java
index 1afedc185c85..4530157d2fe1 100644
--- a/core/java/android/view/ImeBackAnimationController.java
+++ b/core/java/android/view/ImeBackAnimationController.java
@@ -134,7 +134,9 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {
@Override
public void onBackInvoked() {
- if (!isBackAnimationAllowed()) {
+ if (!isBackAnimationAllowed() || !mIsPreCommitAnimationInProgress) {
+ // play regular hide animation if back-animation is not allowed or if insets control has
+ // been cancelled by the system (this can happen in split screen for example)
mInsetsController.hide(ime());
return;
}
diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java
index de5fc7f3e358..58ef5efe846f 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -67,7 +67,7 @@ public final class InputWindowHandle {
InputConfig.SPY,
InputConfig.INTERCEPTS_STYLUS,
InputConfig.CLONE,
- InputConfig.SENSITIVE_FOR_TRACING,
+ InputConfig.SENSITIVE_FOR_PRIVACY,
})
public @interface InputConfigFlags {}
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index 7dc151d8f9ee..71199e9c3619 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -234,7 +234,7 @@ public final class PointerIcon implements Parcelable {
}
int typeIndex = getSystemIconTypeIndex(type);
- if (typeIndex == 0) {
+ if (typeIndex < 0) {
typeIndex = getSystemIconTypeIndex(TYPE_DEFAULT);
}
@@ -606,7 +606,7 @@ public final class PointerIcon implements Parcelable {
case TYPE_HANDWRITING:
return com.android.internal.R.styleable.Pointer_pointerIconHandwriting;
default:
- return 0;
+ return -1;
}
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 9579614ac379..60ad926f2be1 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -40,7 +40,6 @@ import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
import static android.view.flags.Flags.sensitiveContentAppProtection;
-import static android.view.flags.Flags.sensitiveContentPrematureProtectionRemovedFix;
import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly;
import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly;
import static android.view.flags.Flags.toolkitFrameRateSmallUsesPercentReadOnly;
@@ -32230,7 +32229,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
void increaseSensitiveViewsCount() {
if (mSensitiveViewsCount == 0) {
- mViewRootImpl.notifySensitiveContentAppProtection(true);
+ mViewRootImpl.addSensitiveContentAppProtection();
}
mSensitiveViewsCount++;
}
@@ -32238,11 +32237,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
void decreaseSensitiveViewsCount() {
mSensitiveViewsCount--;
if (mSensitiveViewsCount == 0) {
- if (sensitiveContentPrematureProtectionRemovedFix()) {
- mViewRootImpl.removeSensitiveContentProtectionOnTransactionCommit();
- } else {
- mViewRootImpl.notifySensitiveContentAppProtection(false);
- }
+ mViewRootImpl.removeSensitiveContentAppProtection();
}
if (mSensitiveViewsCount < 0) {
Log.wtf(VIEW_LOG_TAG, "mSensitiveViewsCount is negative" + mSensitiveViewsCount);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 1d843757489e..fa579611a3a1 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -25,6 +25,7 @@ import static android.os.Trace.TRACE_TAG_VIEW;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.DragEvent.ACTION_DRAG_LOCATION;
+import static android.view.flags.Flags.sensitiveContentPrematureProtectionRemovedFix;
import static android.view.InputDevice.SOURCE_CLASS_NONE;
import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.FRAME_RATE_CATEGORY_DEFAULT;
@@ -4244,7 +4245,14 @@ public final class ViewRootImpl implements ViewParent,
mReportNextDraw = false;
mLastReportNextDrawReason = null;
mActiveSurfaceSyncGroup = null;
- mHasPendingTransactions = false;
+ if (mHasPendingTransactions) {
+ // TODO: We shouldn't ever actually hit this, it means mPendingTransaction wasn't
+ // merged with a sync group or BLASTBufferQueue before making it to this point
+ // But better a one or two frame flicker than steady-state broken from dropping
+ // whatever is in this transaction
+ mPendingTransaction.apply();
+ mHasPendingTransactions = false;
+ }
mSyncBuffer = false;
if (isInWMSRequestedSync()) {
mWmsRequestSyncGroup.markSyncReady();
@@ -4331,29 +4339,42 @@ public final class ViewRootImpl implements ViewParent,
* <li>It should only notify service to unblock projection when all sensitive view are
* removed from the window.
* </ol>
+ *
+ * @param enableProtection if true, the protection is enabled for this window.
+ * if false, the protection is removed for this window.
*/
- void notifySensitiveContentAppProtection(boolean showSensitiveContent) {
+ private void applySensitiveContentAppProtection(boolean enableProtection) {
try {
if (mSensitiveContentProtectionService == null) {
return;
}
if (DEBUG_SENSITIVE_CONTENT) {
Log.d(TAG, "Notify sensitive content, package=" + mContext.getPackageName()
- + ", token=" + getWindowToken() + ", flag=" + showSensitiveContent);
+ + ", token=" + getWindowToken() + ", flag=" + enableProtection);
}
// The window would be blocked during screen share if it shows sensitive content.
mSensitiveContentProtectionService.setSensitiveContentProtection(
- getWindowToken(), mContext.getPackageName(), showSensitiveContent);
+ getWindowToken(), mContext.getPackageName(), enableProtection);
} catch (RemoteException ex) {
Log.e(TAG, "Unable to protect sensitive content during screen share", ex);
}
}
/**
- * Sensitive protection is removed on transaction commit to avoid prematurely removing
- * the protection.
+ * Add sensitive content protection, when there are one or more visible sensitive views.
+ */
+ void addSensitiveContentAppProtection() {
+ applySensitiveContentAppProtection(true);
+ }
+
+ /**
+ * Remove sensitive content protection, when there is no visible sensitive view.
*/
- void removeSensitiveContentProtectionOnTransactionCommit() {
+ void removeSensitiveContentAppProtection() {
+ if (!sensitiveContentPrematureProtectionRemovedFix()) {
+ applySensitiveContentAppProtection(false);
+ return;
+ }
if (DEBUG_SENSITIVE_CONTENT) {
Log.d(TAG, "Add transaction to remove sensitive content protection, package="
+ mContext.getPackageName() + ", token=" + getWindowToken());
@@ -4361,7 +4382,7 @@ public final class ViewRootImpl implements ViewParent,
Transaction t = new Transaction();
t.addTransactionCommittedListener(mExecutor, () -> {
if (mAttachInfo.mSensitiveViewsCount == 0) {
- notifySensitiveContentAppProtection(false);
+ applySensitiveContentAppProtection(false);
}
});
applyTransactionOnDraw(t);
@@ -12696,9 +12717,11 @@ public final class ViewRootImpl implements ViewParent,
return;
}
+ boolean traceFrameRate = false;
try {
if (mLastPreferredFrameRate != preferredFrameRate) {
- if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ traceFrameRate = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+ if (traceFrameRate) {
Trace.traceBegin(
Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate "
+ preferredFrameRate + " compatibility "
@@ -12713,7 +12736,9 @@ public final class ViewRootImpl implements ViewParent,
} catch (Exception e) {
Log.e(mTag, "Unable to set frame rate", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ if (traceFrameRate) {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
}
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 0bc2430f8805..f22e8f583e1a 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -4365,7 +4365,8 @@ public interface WindowManager extends ViewManager {
public static final int INPUT_FEATURE_SPY = 1 << 2;
/**
- * Input feature used to indicate that this window is sensitive for tracing.
+ * Input feature used to indicate that this window is privacy sensitive. This may be used
+ * to redact input interactions from tracing or screen mirroring.
* <p>
* A window that uses {@link LayoutParams#FLAG_SECURE} will automatically be treated as
* a sensitive for input tracing, but this input feature can be set on windows that don't
@@ -4378,7 +4379,7 @@ public interface WindowManager extends ViewManager {
*
* @hide
*/
- public static final int INPUT_FEATURE_SENSITIVE_FOR_TRACING = 1 << 3;
+ public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3;
/**
* An internal annotation for flags that can be specified to {@link #inputFeatures}.
@@ -4392,7 +4393,7 @@ public interface WindowManager extends ViewManager {
INPUT_FEATURE_NO_INPUT_CHANNEL,
INPUT_FEATURE_DISABLE_USER_ACTIVITY,
INPUT_FEATURE_SPY,
- INPUT_FEATURE_SENSITIVE_FOR_TRACING,
+ INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
})
public @interface InputFeatureFlags {
}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index c7df15c7d5fa..bfe4e6f3cc9b 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -1592,7 +1592,8 @@ public final class AutofillManager {
// request comes in but PCC Detection hasn't been triggered. There is no benefit to
// trigger PCC Detection separately in those cases.
if (!isActiveLocked()) {
- final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+ final boolean clientAdded =
+ tryAddServiceClientIfNeededLocked(isCredmanRequested);
if (clientAdded) {
startSessionLocked(/* id= */ AutofillId.NO_AUTOFILL_ID, /* bounds= */ null,
/* value= */ null, /* flags= */ FLAG_PCC_DETECTION);
@@ -1850,7 +1851,8 @@ public final class AutofillManager {
Rect bounds, AutofillValue value, int flags) {
if (shouldIgnoreViewEnteredLocked(id, flags)) return null;
- final boolean clientAdded = tryAddServiceClientIfNeededLocked();
+ boolean credmanRequested = isCredmanRequested(view);
+ final boolean clientAdded = tryAddServiceClientIfNeededLocked(credmanRequested);
if (!clientAdded) {
if (sVerbose) Log.v(TAG, "ignoring notifyViewEntered(" + id + "): no service client");
return null;
@@ -2645,6 +2647,11 @@ public final class AutofillManager {
*/
@GuardedBy("mLock")
private boolean tryAddServiceClientIfNeededLocked() {
+ return tryAddServiceClientIfNeededLocked(/*credmanRequested=*/ false);
+ }
+
+ @GuardedBy("mLock")
+ private boolean tryAddServiceClientIfNeededLocked(boolean credmanRequested) {
final AutofillClient client = getClient();
if (client == null) {
return false;
@@ -2659,7 +2666,7 @@ public final class AutofillManager {
final int userId = mContext.getUserId();
final SyncResultReceiver receiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS);
mService.addClient(mServiceClient, client.autofillClientGetComponentName(),
- userId, receiver);
+ userId, receiver, credmanRequested);
int flags = 0;
try {
flags = receiver.getIntResult();
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index cefd6dc13005..1a9322e6f2b8 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -38,7 +38,7 @@ import com.android.internal.os.IResultReceiver;
oneway interface IAutoFillManager {
// Returns flags: FLAG_ADD_CLIENT_ENABLED | FLAG_ADD_CLIENT_DEBUG | FLAG_ADD_CLIENT_VERBOSE
void addClient(in IAutoFillManagerClient client, in ComponentName componentName, int userId,
- in IResultReceiver result);
+ in IResultReceiver result, boolean credmanRequested);
void removeClient(in IAutoFillManagerClient client, int userId);
void startSession(IBinder activityToken, in IBinder appCallback, in AutofillId autoFillId,
in Rect bounds, in AutofillValue value, int userId, boolean hasCallback, int flags,
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 12bd45af7ffb..c0d31fae4b8f 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -54,7 +54,7 @@ flag {
is_fixed_read_only: true
metadata {
purpose: PURPOSE_BUGFIX
- }
+ }
}
flag {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 1cdcd2015f03..a073873cb2a0 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2461,24 +2461,25 @@ public final class InputMethodManager {
* @hide
*/
public boolean hideSoftInputFromView(@NonNull View view, @HideFlags int flags) {
+ checkFocus();
final boolean isFocusedAndWindowFocused = view.hasWindowFocus() && view.isFocused();
synchronized (mH) {
- if (!isFocusedAndWindowFocused && !hasServedByInputMethodLocked(view)) {
+ final boolean hasServedByInputMethod = hasServedByInputMethodLocked(view);
+ if (!isFocusedAndWindowFocused && !hasServedByInputMethod) {
// Fail early if the view is not focused and not served
// to avoid logging many erroneous calls.
return false;
}
- }
- final int reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW;
- final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE,
- ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(view));
- ImeTracker.forLatency().onRequestHide(statsToken,
- ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication);
- ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromView",
- this, null /* icProto */);
- synchronized (mH) {
- if (!hasServedByInputMethodLocked(view)) {
+ final int reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW;
+ final var statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_HIDE,
+ ImeTracker.ORIGIN_CLIENT, reason, ImeTracker.isFromUser(view));
+ ImeTracker.forLatency().onRequestHide(statsToken,
+ ImeTracker.ORIGIN_CLIENT, reason, ActivityThread::currentApplication);
+ ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromView",
+ this, null /* icProto */);
+
+ if (!hasServedByInputMethod) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
ImeTracker.forLatency().onShowFailed(statsToken,
ImeTracker.PHASE_CLIENT_VIEW_SERVED, ActivityThread::currentApplication);
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 5c113f865d45..461eab61e393 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -18,6 +18,7 @@ package android.window;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -93,6 +94,19 @@ public class TaskFragmentOrganizer extends WindowOrganizer {
@TaskFragmentTransitionType
public static final int TASK_FRAGMENT_TRANSIT_CHANGE = TRANSIT_CHANGE;
+
+ /**
+ * The task fragment drag resize transition used by activity embedding.
+ *
+ * This value is also used in Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE and must not
+ * conflict with other predefined transition types.
+ *
+ * @hide
+ */
+ @WindowManager.TransitionType
+ @TaskFragmentTransitionType
+ public static final int TASK_FRAGMENT_TRANSIT_DRAG_RESIZE = TRANSIT_FIRST_CUSTOM + 17;
+
/**
* Introduced a sub set of {@link WindowManager.TransitionType} for the types that are used for
* TaskFragment transition.
@@ -106,6 +120,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer {
TASK_FRAGMENT_TRANSIT_OPEN,
TASK_FRAGMENT_TRANSIT_CLOSE,
TASK_FRAGMENT_TRANSIT_CHANGE,
+ TASK_FRAGMENT_TRANSIT_DRAG_RESIZE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface TaskFragmentTransitionType {}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 945164a7f25f..4d1b87a3d97a 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -120,4 +120,11 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "pip_restore_to_overlay"
+ description: "Restore exit-pip activity back to ActivityEmbedding overlay"
+ bug: "297887697"
} \ No newline at end of file
diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java
index 7ac553c56bf7..3af1dd7a28e4 100644
--- a/core/java/com/android/internal/content/PackageMonitor.java
+++ b/core/java/com/android/internal/content/PackageMonitor.java
@@ -22,6 +22,7 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.Flags;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
@@ -68,7 +69,8 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver {
@UnsupportedAppUsage
public PackageMonitor() {
- this(true);
+ // If the feature flag is enabled, set mSupportsPackageRestartQuery to false by default
+ this(!Flags.packageRestartQueryDisabledByDefault());
}
/**
diff --git a/core/java/com/android/internal/inputmethod/ImeTracing.java b/core/java/com/android/internal/inputmethod/ImeTracing.java
index ee9c3aa57d72..cd4ccdad6f58 100644
--- a/core/java/com/android/internal/inputmethod/ImeTracing.java
+++ b/core/java/com/android/internal/inputmethod/ImeTracing.java
@@ -60,7 +60,9 @@ public abstract class ImeTracing {
*/
public static ImeTracing getInstance() {
if (sInstance == null) {
- if (isSystemProcess()) {
+ if (android.tracing.Flags.perfettoIme()) {
+ sInstance = new ImeTracingPerfettoImpl();
+ } else if (isSystemProcess()) {
sInstance = new ImeTracingServerImpl();
} else {
sInstance = new ImeTracingClientImpl();
@@ -78,7 +80,7 @@ public abstract class ImeTracing {
* and {@see #IME_TRACING_FROM_IMS}
* @param where
*/
- public void sendToService(byte[] protoDump, int source, String where) {
+ protected void sendToService(byte[] protoDump, int source, String where) {
InputMethodManagerGlobal.startProtoDump(protoDump, source, where,
e -> Log.e(TAG, "Exception while sending ime-related dump to server", e));
}
diff --git a/core/java/com/android/internal/inputmethod/ImeTracingPerfettoImpl.java b/core/java/com/android/internal/inputmethod/ImeTracingPerfettoImpl.java
new file mode 100644
index 000000000000..91b80ddaa836
--- /dev/null
+++ b/core/java/com/android/internal/inputmethod/ImeTracingPerfettoImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.inputmethod;
+
+import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.internal.perfetto.protos.Inputmethodeditor.InputMethodClientsTraceProto;
+import android.internal.perfetto.protos.Inputmethodeditor.InputMethodManagerServiceTraceProto;
+import android.internal.perfetto.protos.Inputmethodeditor.InputMethodServiceTraceProto;
+import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket;
+import android.internal.perfetto.protos.WinscopeExtensionsImplOuterClass.WinscopeExtensionsImpl;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.tracing.inputmethod.InputMethodDataSource;
+import android.tracing.perfetto.DataSourceParams;
+import android.tracing.perfetto.InitArguments;
+import android.tracing.perfetto.Producer;
+import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.InputMethodManager;
+
+import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An implementation of {@link ImeTracing} for perfetto tracing.
+ */
+final class ImeTracingPerfettoImpl extends ImeTracing {
+ private final AtomicInteger mTracingSessionsCount = new AtomicInteger(0);
+ private final AtomicBoolean mIsClientDumpInProgress = new AtomicBoolean(false);
+ private final AtomicBoolean mIsServiceDumpInProgress = new AtomicBoolean(false);
+ private final AtomicBoolean mIsManagerServiceDumpInProgress = new AtomicBoolean(false);
+ private final InputMethodDataSource mDataSource = new InputMethodDataSource(
+ mTracingSessionsCount::incrementAndGet,
+ mTracingSessionsCount::decrementAndGet);
+
+ ImeTracingPerfettoImpl() {
+ Producer.init(InitArguments.DEFAULTS);
+ mDataSource.register(
+ new DataSourceParams(PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT));
+ }
+
+
+ @Override
+ public void triggerClientDump(String where, InputMethodManager immInstance,
+ @Nullable byte[] icProto) {
+ if (!isEnabled() || !isAvailable()) {
+ return;
+ }
+
+ if (!mIsClientDumpInProgress.compareAndSet(false, true)) {
+ return;
+ }
+
+ if (immInstance == null) {
+ return;
+ }
+
+ try {
+ Trace.beginSection("inputmethod_client_dump");
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+ os.write(TracePacket.TIMESTAMP, SystemClock.elapsedRealtimeNanos());
+ final long tokenWinscopeExtensions =
+ os.start(TracePacket.WINSCOPE_EXTENSIONS);
+ final long tokenExtensionsField =
+ os.start(WinscopeExtensionsImpl.INPUTMETHOD_CLIENTS);
+ os.write(InputMethodClientsTraceProto.WHERE, where);
+ final long tokenClient =
+ os.start(InputMethodClientsTraceProto.CLIENT);
+ immInstance.dumpDebug(os, icProto);
+ os.end(tokenClient);
+ os.end(tokenExtensionsField);
+ os.end(tokenWinscopeExtensions);
+ });
+ } finally {
+ mIsClientDumpInProgress.set(false);
+ Trace.endSection();
+ }
+ }
+
+ @Override
+ public void triggerServiceDump(String where,
+ @NonNull ServiceDumper dumper, @Nullable byte[] icProto) {
+ if (!isEnabled() || !isAvailable()) {
+ return;
+ }
+
+ if (!mIsServiceDumpInProgress.compareAndSet(false, true)) {
+ return;
+ }
+
+ try {
+ Trace.beginSection("inputmethod_service_dump");
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+ os.write(TracePacket.TIMESTAMP, SystemClock.elapsedRealtimeNanos());
+ final long tokenWinscopeExtensions =
+ os.start(TracePacket.WINSCOPE_EXTENSIONS);
+ final long tokenExtensionsField =
+ os.start(WinscopeExtensionsImpl.INPUTMETHOD_SERVICE);
+ os.write(InputMethodServiceTraceProto.WHERE, where);
+ dumper.dumpToProto(os, icProto);
+ os.end(tokenExtensionsField);
+ os.end(tokenWinscopeExtensions);
+ });
+ } finally {
+ mIsServiceDumpInProgress.set(false);
+ Trace.endSection();
+ }
+ }
+
+ @Override
+ public void triggerManagerServiceDump(@NonNull String where, @NonNull ServiceDumper dumper) {
+ if (!isEnabled() || !isAvailable()) {
+ return;
+ }
+
+ if (!mIsManagerServiceDumpInProgress.compareAndSet(false, true)) {
+ return;
+ }
+
+ try {
+ Trace.beginSection("inputmethod_manager_service_dump");
+ mDataSource.trace((ctx) -> {
+ final ProtoOutputStream os = ctx.newTracePacket();
+ os.write(TracePacket.TIMESTAMP, SystemClock.elapsedRealtimeNanos());
+ final long tokenWinscopeExtensions =
+ os.start(TracePacket.WINSCOPE_EXTENSIONS);
+ final long tokenExtensionsField =
+ os.start(WinscopeExtensionsImpl.INPUTMETHOD_MANAGER_SERVICE);
+ os.write(InputMethodManagerServiceTraceProto.WHERE, where);
+ dumper.dumpToProto(os, null);
+ os.end(tokenExtensionsField);
+ os.end(tokenWinscopeExtensions);
+ });
+ } finally {
+ mIsManagerServiceDumpInProgress.set(false);
+ Trace.endSection();
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mTracingSessionsCount.get() > 0;
+ }
+
+ @Override
+ public void startTrace(@Nullable PrintWriter pw) {
+ // Intentionally left empty. Tracing start/stop is managed through Perfetto.
+ }
+
+ @Override
+ public void stopTrace(@Nullable PrintWriter pw) {
+ // Intentionally left empty. Tracing start/stop is managed through Perfetto.
+ }
+
+ @Override
+ public void addToBuffer(ProtoOutputStream proto, int source) {
+ // Intentionally left empty. Only used for legacy tracing.
+ }
+}
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index f2d2c1b637c6..6ffa8261c8fd 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -134,10 +134,12 @@ public class Cuj {
public static final int CUJ_LAUNCHER_WIDGET_PICKER_SEARCH_BACK = 99;
public static final int CUJ_LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK = 100;
public static final int CUJ_LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK = 101;
+ public static final int CUJ_LAUNCHER_PRIVATE_SPACE_LOCK = 102;
+ public static final int CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK = 103;
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
@VisibleForTesting
- static final int LAST_CUJ = CUJ_LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK;
+ static final int LAST_CUJ = CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK;
/** @hide */
@IntDef({
@@ -230,7 +232,9 @@ public class Cuj {
CUJ_LAUNCHER_TASKBAR_ALL_APPS_SEARCH_BACK,
CUJ_LAUNCHER_WIDGET_PICKER_CLOSE_BACK,
CUJ_LAUNCHER_WIDGET_PICKER_SEARCH_BACK,
- CUJ_LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK
+ CUJ_LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK,
+ CUJ_LAUNCHER_PRIVATE_SPACE_LOCK,
+ CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
@@ -335,6 +339,8 @@ public class Cuj {
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WIDGET_PICKER_SEARCH_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WIDGET_PICKER_SEARCH_BACK;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_PRIVATE_SPACE_LOCK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_PRIVATE_SPACE_LOCK;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_PRIVATE_SPACE_UNLOCK;
}
private Cuj() {
@@ -533,6 +539,10 @@ public class Cuj {
return "LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK";
case CUJ_LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK:
return "LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK";
+ case CUJ_LAUNCHER_PRIVATE_SPACE_LOCK:
+ return "LAUNCHER_PRIVATE_SPACE_LOCK";
+ case CUJ_LAUNCHER_PRIVATE_SPACE_UNLOCK:
+ return "LAUNCHER_PRIVATE_SPACE_UNLOCK";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/util/ProcFileReader.java b/core/java/com/android/internal/util/ProcFileReader.java
index 6cf241e65d00..ddbb586f150e 100644
--- a/core/java/com/android/internal/util/ProcFileReader.java
+++ b/core/java/com/android/internal/util/ProcFileReader.java
@@ -89,6 +89,12 @@ public class ProcFileReader implements Closeable {
mTail -= count;
if (mTail == 0) {
fillBuf();
+
+ if (mTail > 0 && mBuffer[0] == ' ') {
+ // After filling the buffer, it contains more consecutive
+ // delimiters that need to be skipped.
+ consumeBuf(0);
+ }
}
}
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 66b0158fbd67..0734e6827d4d 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -886,9 +886,16 @@ public class LockPatternView extends View {
cellState.activationAnimator.cancel();
}
AnimatorSet animatorSet = new AnimatorSet();
+
+ // When running the line end animation (see doc for createLineEndAnimation), if cell is in:
+ // - activate state - use finger position at the time of hit detection
+ // - deactivate state - use current position where the end was last during initial animation
+ // Note that deactivate state will only come if mKeepDotActivated is themed true.
+ final float startX = activate == CELL_ACTIVATE ? mInProgressX : cellState.lineEndX;
+ final float startY = activate == CELL_ACTIVATE ? mInProgressY : cellState.lineEndY;
AnimatorSet.Builder animatorSetBuilder = animatorSet
.play(createLineDisappearingAnimation())
- .with(createLineEndAnimation(cellState, mInProgressX, mInProgressY,
+ .with(createLineEndAnimation(cellState, startX, startY,
getCenterXForColumn(cell.column), getCenterYForRow(cell.row)));
if (mDotSize != mDotSizeActivated) {
animatorSetBuilder.with(createDotRadiusAnimation(cellState));
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 77a99120072e..bfbfb3aa3d56 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -178,6 +178,7 @@
<protected-broadcast android:name="android.bluetooth.device.action.CONNECTION_ACCESS_REPLY" />
<protected-broadcast android:name="android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL" />
<protected-broadcast android:name="android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST" />
+ <protected-broadcast android:name="android.bluetooth.device.action.KEY_MISSING" />
<protected-broadcast android:name="android.bluetooth.device.action.SDP_RECORD" />
<protected-broadcast android:name="android.bluetooth.device.action.BATTERY_LEVEL_CHANGED" />
<protected-broadcast android:name="android.bluetooth.device.action.REMOTE_ISSUE_OCCURRED" />
diff --git a/core/res/res/drawable/ic_thread_network.xml b/core/res/res/drawable/ic_thread_network.xml
new file mode 100644
index 000000000000..1d7608f3daa2
--- /dev/null
+++ b/core/res/res/drawable/ic_thread_network.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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/white"
+ android:pathData="M476,880Q394,879 322,847.5Q250,816 196,761.5Q142,707 111,634.5Q80,562 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,623 791.5,732.5Q703,842 563,871L563,480L607,480Q661,480 699.5,441.5Q738,403 738,349Q738,295 699.5,256.5Q661,218 607,218Q553,218 514.5,256.5Q476,295 476,349L476,393L345,393Q279,393 233,439Q187,485 187,551Q187,617 233,662.5Q279,708 345,708L345,621Q316,621 295,600.5Q274,580 274,551Q274,522 295,501Q316,480 345,480L476,480L476,880ZM563,393L563,349Q563,331 576,318Q589,305 607,305Q625,305 638,318Q651,331 651,349Q651,367 638,380Q625,393 607,393L563,393Z"/>
+</vector>
diff --git a/core/res/res/layout/side_fps_toast.xml b/core/res/res/layout/side_fps_toast.xml
index 96860b050393..2c35c9b888cf 100644
--- a/core/res/res/layout/side_fps_toast.xml
+++ b/core/res/res/layout/side_fps_toast.xml
@@ -18,28 +18,26 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:minWidth="350dp"
android:layout_gravity="center"
+ android:minWidth="350dp"
android:background="@color/side_fps_toast_background">
<TextView
- android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="6"
android:text="@string/fp_power_button_enrollment_title"
- android:singleLine="true"
- android:ellipsize="end"
android:textColor="@color/side_fps_text_color"
android:paddingLeft="20dp"/>
<Space
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_weight="1"/>
+ android:layout_width="5dp"
+ android:layout_height="match_parent" />
<Button
android:id="@+id/turn_off_screen"
- android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="3"
android:text="@string/fp_power_button_enrollment_button_text"
- android:paddingRight="20dp"
style="?android:attr/buttonBarNegativeButtonStyle"
android:textColor="@color/side_fps_button_color"
- android:maxLines="1"/>
+ />
</LinearLayout> \ No newline at end of file
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 5e900f773a65..27b756d46b12 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -842,7 +842,8 @@
that created the task, and therefore there will only be one instance of this activity
in a task. In contrast to the {@code singleTask} launch mode, this activity can be
started in multiple instances in different tasks if the
- {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set.-->
+ {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set.
+ This enum value is introduced in API level 31. -->
<enum name="singleInstancePerTask" value="4" />
</attr>
<!-- Specify the orientation an activity should be run in. If not
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b3d8f392700d..54dbc48c99fb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1387,6 +1387,7 @@
<java-symbol type="drawable" name="platlogo" />
<java-symbol type="drawable" name="stat_notify_sync_error" />
<java-symbol type="drawable" name="stat_notify_wifi_in_range" />
+ <java-symbol type="drawable" name="ic_thread_network" />
<java-symbol type="drawable" name="ic_wifi_signal_0" />
<java-symbol type="drawable" name="ic_wifi_signal_1" />
<java-symbol type="drawable" name="ic_wifi_signal_2" />
diff --git a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
index c00ebe487620..57bbb1cc9b57 100644
--- a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
+++ b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
@@ -241,6 +241,23 @@ public class ImeBackAnimationControllerTest {
});
}
+ @Test
+ public void testOnBackInvokedHidesImeEvenIfInsetsControlCancelled() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // start back gesture
+ WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
+
+ // simulate ImeBackAnimationController not receiving control (e.g. due to split screen)
+ animationControlListener.onCancelled(mWindowInsetsAnimationController);
+
+ // commit back gesture
+ mBackAnimationController.onBackInvoked();
+
+ // verify that InsetsController#hide is called
+ verify(mInsetsController, times(1)).hide(ime());
+ });
+ }
+
private WindowInsetsAnimationControlListener startBackGesture() {
// start back gesture
mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
diff --git a/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java b/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java
index 4c00c1667e3c..9785ca7face5 100644
--- a/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/ProcFileReaderTest.java
@@ -216,6 +216,46 @@ public class ProcFileReaderTest {
}
@Test
+ public void testBufferSizeWithConsecutiveDelimiters() throws Exception {
+ // Read numbers using very small buffer size, exercising fillBuf()
+ // Include more consecutive delimiters than the buffer size.
+ final ProcFileReader reader =
+ buildReader("1 21 3 41 5 61 7 81 9 10\n", 3);
+
+ assertEquals(1, reader.nextInt());
+ assertEquals(21, reader.nextInt());
+ assertEquals(3, reader.nextInt());
+ assertEquals(41, reader.nextInt());
+ assertEquals(5, reader.nextInt());
+ assertEquals(61, reader.nextInt());
+ assertEquals(7, reader.nextInt());
+ assertEquals(81, reader.nextInt());
+ assertEquals(9, reader.nextInt());
+ assertEquals(10, reader.nextInt());
+ reader.finishLine();
+ assertFalse(reader.hasMoreData());
+ }
+
+ @Test
+ public void testBufferSizeWithConsecutiveDelimitersAndMultipleLines() throws Exception {
+ final ProcFileReader reader =
+ buildReader("1 21 41 \n 5 7 81 \n 9 10 \n", 3);
+
+ assertEquals(1, reader.nextInt());
+ assertEquals(21, reader.nextInt());
+ assertEquals(41, reader.nextInt());
+ reader.finishLine();
+ assertEquals(5, reader.nextInt());
+ assertEquals(7, reader.nextInt());
+ assertEquals(81, reader.nextInt());
+ reader.finishLine();
+ assertEquals(9, reader.nextInt());
+ assertEquals(10, reader.nextInt());
+ reader.finishLine();
+ assertFalse(reader.hasMoreData());
+ }
+
+ @Test
public void testIgnore() throws Exception {
final ProcFileReader reader = buildReader("a b c\n");
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 9d1e5074dd3e..65615e62c3df 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -322,11 +322,11 @@
<library name="android.hidl.manager-V1.0-java"
file="/system/framework/android.hidl.manager-V1.0-java.jar" />
- <!-- These are the standard packages that are white-listed to always have internet
+ <!-- These are the standard packages that are allowed to always have internet
access while in power save mode, even if they aren't in the foreground. -->
<allow-in-power-save package="com.android.providers.downloads" />
- <!-- These are the standard packages that are white-listed to always have internet
+ <!-- These are the standard packages that are allowed to always have internet
access while in data mode, even if they aren't in the foreground. -->
<allow-in-data-usage-save package="com.android.providers.downloads" />
@@ -338,7 +338,7 @@
<!-- Emergency app needs to run in the background to reliably provide safety features -->
<allow-in-power-save package="com.android.emergency" />
- <!-- Whitelist system providers -->
+ <!-- Allow system providers -->
<!-- Calendar provider needs alarms while in idle -->
<allow-in-power-save package="com.android.providers.calendar" />
<allow-in-power-save-except-idle package="com.android.providers.contacts" />
diff --git a/data/keyboards/Android.bp b/data/keyboards/Android.bp
index e62678f92d8b..423b55bd85db 100644
--- a/data/keyboards/Android.bp
+++ b/data/keyboards/Android.bp
@@ -33,7 +33,7 @@ prebuilt_usr_keylayout {
srcs: [
"*.kl",
],
- installable: false,
+ no_full_install: true,
}
prebuilt_usr_keychars {
@@ -41,7 +41,7 @@ prebuilt_usr_keychars {
srcs: [
"*.kcm",
],
- installable: false,
+ no_full_install: true,
}
prebuilt_usr_idc {
@@ -49,5 +49,5 @@ prebuilt_usr_idc {
srcs: [
"*.idc",
],
- installable: false,
+ no_full_install: true,
}
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 e38038e38c25..14388a6d42ff 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -28,6 +28,7 @@ import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
@@ -850,6 +851,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId);
return;
}
+
+ if (!parentInfo.isVisible()) {
+ // Only making the TaskContainer invisible and drops the other info, and perform the
+ // update when the next time the Task becomes visible.
+ taskContainer.setIsVisible(false);
+ return;
+ }
+
// Checks if container should be updated before apply new parentInfo.
final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
taskContainer.updateTaskFragmentParentInfo(parentInfo);
@@ -3137,11 +3146,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
private static EmbeddedActivityWindowInfo translateActivityWindowInfo(
@NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) {
final boolean isEmbedded = activityWindowInfo.isEmbedded();
- final Rect activityBounds = new Rect(activity.getResources().getConfiguration()
- .windowConfiguration.getBounds());
final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds());
final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds());
- return new EmbeddedActivityWindowInfo(activity, isEmbedded, activityBounds, taskBounds,
+ return new EmbeddedActivityWindowInfo(activity, isEmbedded, taskBounds,
activityStackBounds);
}
@@ -3245,6 +3252,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
synchronized (mLock) {
final TransactionRecord transactionRecord =
mTransactionManager.startNewTransaction();
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_DRAG_RESIZE);
final WindowContainerTransaction wct = transactionRecord.getTransaction();
final TaskContainer taskContainer = mTaskContainers.get(taskId);
if (taskContainer != null) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 67d34c71b4d6..a68373832a14 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -151,6 +151,10 @@ class TaskContainer {
return mIsVisible;
}
+ void setIsVisible(boolean visible) {
+ mIsVisible = visible;
+ }
+
boolean hasDirectActivity() {
return mHasDirectActivity;
}
@@ -185,13 +189,15 @@ class TaskContainer {
boolean shouldUpdateContainer(@NonNull TaskFragmentParentInfo info) {
final Configuration configuration = info.getConfiguration();
- return info.isVisible()
- // No need to update presentation in PIP until the Task exit PIP.
- && !isInPictureInPicture(configuration)
- // If the task properties equals regardless of starting position, don't need to
- // update the container.
- && (mConfiguration.diffPublicOnly(configuration) != 0
- || mDisplayId != info.getDisplayId());
+ if (isInPictureInPicture(configuration)) {
+ // No need to update presentation in PIP until the Task exit PIP.
+ return false;
+ }
+
+ // If the task properties equals regardless of starting position, don't
+ // need to update the container.
+ return mConfiguration.diffPublicOnly(configuration) != 0
+ || mDisplayId != info.getDisplayId();
}
/**
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index fab298d6d58b..049a9e2c2aca 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -580,7 +580,7 @@ public class OverlayPresentationTest {
final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(),
- false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
+ true /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 8bc3a300136a..7d86ec2272af 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -1570,8 +1570,6 @@ public class SplitControllerTest {
mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
final boolean isEmbedded = true;
- final Rect activityBounds = mActivity.getResources().getConfiguration().windowConfiguration
- .getBounds();
final Rect taskBounds = new Rect(0, 0, 1000, 2000);
final Rect activityStackBounds = new Rect(0, 0, 500, 2000);
doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded();
@@ -1579,7 +1577,7 @@ public class SplitControllerTest {
doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds();
final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity,
- isEmbedded, activityBounds, taskBounds, activityStackBounds);
+ isEmbedded, taskBounds, activityStackBounds);
assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity));
}
@@ -1621,6 +1619,48 @@ public class SplitControllerTest {
verify(mEmbeddedActivityWindowInfoCallback, never()).accept(any());
}
+ @Test
+ public void testTaskFragmentParentInfoChanged() {
+ // Making a split
+ final Activity secondaryActivity = createMockActivity();
+ addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */);
+
+ // Updates the parent info.
+ final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+ final Configuration configuration = new Configuration();
+ final TaskFragmentParentInfo originalInfo = new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */,
+ null /* decorSurface */);
+ mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class),
+ TASK_ID, originalInfo);
+ assertTrue(taskContainer.isVisible());
+
+ // Making a public configuration change while the Task is invisible.
+ configuration.densityDpi += 100;
+ final TaskFragmentParentInfo invisibleInfo = new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, false /* visible */, false /* hasDirectActivity */,
+ null /* decorSurface */);
+ mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class),
+ TASK_ID, invisibleInfo);
+
+ // Ensure the TaskContainer is inivisible, but the configuration is not updated.
+ assertFalse(taskContainer.isVisible());
+ assertTrue(taskContainer.getTaskFragmentParentInfo().getConfiguration().diffPublicOnly(
+ configuration) > 0);
+
+ // Updates when Task to become visible
+ final TaskFragmentParentInfo visibleInfo = new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */,
+ null /* decorSurface */);
+ mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class),
+ TASK_ID, visibleInfo);
+
+ // Ensure the Task is visible and configuration is updated.
+ assertTrue(taskContainer.isVisible());
+ assertFalse(taskContainer.getTaskFragmentParentInfo().getConfiguration().diffPublicOnly(
+ configuration) > 0);
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
return createMockActivity(TASK_ID);
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 7ff204c695f8..fe68123513ca 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -64,3 +64,10 @@ flag {
description: "Enables long-press action for nav handle when a bubble is expanded"
bug: "324910035"
}
+
+flag {
+ name: "enable_optional_bubble_overflow"
+ namespace: "multitasking"
+ description: "Hides the bubble overflow if there aren't any overflowed bubbles"
+ bug: "334175587"
+}
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 9f0a425a82f8..9599658384f0 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -23,7 +23,8 @@
android:orientation="horizontal"
android:gravity="center"
android:padding="16dp"
- android:background="@drawable/desktop_mode_maximize_menu_background">
+ android:background="@drawable/desktop_mode_maximize_menu_background"
+ android:elevation="1dp">
<LinearLayout
android:layout_width="wrap_content"
@@ -37,7 +38,8 @@
android:background="@drawable/desktop_mode_maximize_menu_layout_background"
android:padding="4dp"
android:layout_marginRight="8dp"
- android:layout_marginBottom="4dp">
+ android:layout_marginBottom="4dp"
+ android:alpha="0">
<Button
android:id="@+id/maximize_menu_maximize_button"
style="?android:attr/buttonBarButtonStyle"
@@ -48,6 +50,7 @@
</FrameLayout>
<TextView
+ android:id="@+id/maximize_menu_maximize_window_text"
android:layout_width="94dp"
android:layout_height="18dp"
android:textSize="11sp"
@@ -55,7 +58,8 @@
android:gravity="center"
android:fontFamily="google-sans-text"
android:text="@string/desktop_mode_maximize_menu_maximize_text"
- android:textColor="?androidprv:attr/materialColorOnSurface"/>
+ android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:alpha="0"/>
</LinearLayout>
<LinearLayout
@@ -69,7 +73,8 @@
android:orientation="horizontal"
android:padding="4dp"
android:background="@drawable/desktop_mode_maximize_menu_layout_background"
- android:layout_marginBottom="4dp">
+ android:layout_marginBottom="4dp"
+ android:alpha="0">
<Button
android:id="@+id/maximize_menu_snap_left_button"
style="?android:attr/buttonBarButtonStyle"
@@ -88,6 +93,7 @@
android:stateListAnimator="@null"/>
</LinearLayout>
<TextView
+ android:id="@+id/maximize_menu_snap_window_text"
android:layout_width="94dp"
android:layout_height="18dp"
android:textSize="11sp"
@@ -96,6 +102,8 @@
android:gravity="center"
android:fontFamily="google-sans-text"
android:text="@string/desktop_mode_maximize_menu_snap_text"
- android:textColor="?androidprv:attr/materialColorOnSurface"/>
+ android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:alpha="0"/>
</LinearLayout>
-</LinearLayout> \ No newline at end of file
+</LinearLayout>
+
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index c8bfe7a4ef80..f532f96ccfc9 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -464,6 +464,9 @@
<!-- The height of the maximize menu in desktop mode. -->
<dimen name="desktop_mode_maximize_menu_height">114dp</dimen>
+ <!-- The padding of the maximize menu in desktop mode. -->
+ <dimen name="desktop_mode_menu_padding">16dp</dimen>
+
<!-- The height of the buttons in the maximize menu. -->
<dimen name="desktop_mode_maximize_menu_button_height">52dp</dimen>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index d44033c72302..a426b206b0cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -26,6 +26,7 @@ import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationS
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
import android.animation.Animator;
import android.animation.ValueAnimator;
@@ -190,6 +191,10 @@ class ActivityEmbeddingAnimationRunner {
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) {
+ // Jump cut for AE drag resizing because the content is veiled.
+ return new ArrayList<>();
+ }
boolean isChangeTransition = false;
for (TransitionInfo.Change change : info.getChanges()) {
if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index 1f9358e2aa91..d6b9d34c5ab3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -22,6 +22,7 @@ import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static com.android.wm.shell.transition.DefaultTransitionHandler.isSupportedOverrideAnimation;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
import static java.util.Objects.requireNonNull;
@@ -90,6 +91,12 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
/** Whether ActivityEmbeddingController should animate this transition. */
public boolean shouldAnimate(@NonNull TransitionInfo info) {
+ if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) {
+ // The TRANSIT_TASK_FRAGMENT_DRAG_RESIZE type happens when the user drags the
+ // interactive divider to resize the split containers. The content is veiled, so we will
+ // handle the transition with a jump cut.
+ return true;
+ }
boolean containsEmbeddingChange = false;
for (TransitionInfo.Change change : info.getChanges()) {
if (!change.hasFlags(FLAG_FILLS_TASK) && change.hasFlags(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 037397c8e40b..87aac0b55e35 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1368,28 +1368,32 @@ public class BubbleController implements ConfigurationChangeListener,
}
String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
- Log.i(TAG, "showOrHideAppBubble, key= " + appBubbleKey + " stackVisibility= "
- + (mStackView != null ? mStackView.getVisibility() : " null ")
- + " statusBarShade=" + mIsStatusBarShade);
PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
- if (!isResizableActivity(intent, packageManager, appBubbleKey)) return;
+ if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors
Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey);
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s "
+ + "statusBarShade=%s",
+ appBubbleKey, existingAppBubble,
+ (mStackView != null ? mStackView.getVisibility() : "null"),
+ mIsStatusBarShade);
+
if (existingAppBubble != null) {
BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
if (isStackExpanded()) {
if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey);
// App bubble is expanded, lets collapse
- Log.i(TAG, " showOrHideAppBubble, selected bubble is app bubble, collapsing");
collapseStack();
} else {
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey);
// App bubble is not selected, select it
- Log.i(TAG, " showOrHideAppBubble, expanded, selecting existing app bubble");
mBubbleData.setSelectedBubble(existingAppBubble);
}
} else {
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey);
// App bubble is not selected, select it & expand
- Log.i(TAG, " showOrHideAppBubble, expand and select existing app bubble");
mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble);
}
} else {
@@ -1397,13 +1401,12 @@ public class BubbleController implements ConfigurationChangeListener,
Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
if (b != null) {
// It's in the overflow, so remove it & reinflate
- Log.i(TAG, " showOrHideAppBubble, expanding app bubble from overflow");
mBubbleData.removeOverflowBubble(b);
} else {
// App bubble does not exist, lets add and expand it
- Log.i(TAG, " showOrHideAppBubble, creating and expanding app bubble");
b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
}
+ ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey);
b.setShouldAutoExpand(true);
inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index a87116ea4670..607a3b5423d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -185,7 +185,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
nextTarget = snapAlgorithm.getDismissStartTarget();
}
if (nextTarget != null) {
- mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), nextTarget);
+ mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget);
return true;
}
return super.performAccessibilityAction(host, action, args);
@@ -345,9 +345,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mMoving = true;
}
if (mMoving) {
- final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+ final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos;
mLastDraggingPosition = position;
- mSplitLayout.updateDivideBounds(position);
+ mSplitLayout.updateDividerBounds(position);
}
break;
case MotionEvent.ACTION_UP:
@@ -363,7 +363,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
final float velocity = isLeftRightSplit
? mVelocityTracker.getXVelocity()
: mVelocityTracker.getYVelocity();
- final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+ final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos;
final DividerSnapAlgorithm.SnapTarget snapTarget =
mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */);
mSplitLayout.snapToTarget(position, snapTarget);
@@ -472,12 +472,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mInteractive = interactive;
mHideHandle = hideHandle;
if (!mInteractive && mHideHandle && mMoving) {
- final int position = mSplitLayout.getDividePosition();
- mSplitLayout.flingDividePosition(
+ final int position = mSplitLayout.getDividerPosition();
+ mSplitLayout.flingDividerPosition(
mLastDraggingPosition,
position,
mSplitLayout.FLING_RESIZE_DURATION,
- () -> mSplitLayout.setDividePosition(position, true /* applyLayoutChange */));
+ () -> mSplitLayout.setDividerPosition(position, true /* applyLayoutChange */));
mMoving = false;
}
releaseTouching();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 6b2d544c192a..2ea32f44a78b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -78,7 +78,7 @@ import java.util.function.Consumer;
/**
* Records and handles layout of splits. Helps to calculate proper bounds when configuration or
- * divide position changes.
+ * divider position changes.
*/
public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
private static final String TAG = "SplitLayout";
@@ -278,7 +278,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
}
- int getDividePosition() {
+ int getDividerPosition() {
return mDividerPosition;
}
@@ -489,20 +489,20 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void setDividerAtBorder(boolean start) {
final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
: mDividerSnapAlgorithm.getDismissEndTarget().position;
- setDividePosition(pos, false /* applyLayoutChange */);
+ setDividerPosition(pos, false /* applyLayoutChange */);
}
/**
* Updates bounds with the passing position. Usually used to update recording bounds while
* performing animation or dragging divider bar to resize the splits.
*/
- void updateDivideBounds(int position) {
+ void updateDividerBounds(int position) {
updateBounds(position);
mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x,
mSurfaceEffectPolicy.mParallaxOffset.y);
}
- void setDividePosition(int position, boolean applyLayoutChange) {
+ void setDividerPosition(int position, boolean applyLayoutChange) {
mDividerPosition = position;
updateBounds(mDividerPosition);
if (applyLayoutChange) {
@@ -511,14 +511,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
/**
- * Updates divide position and split bounds base on the ratio within root bounds. Falls back
+ * Updates divider position and split bounds base on the ratio within root bounds. Falls back
* to middle position if the provided SnapTarget is not supported.
*/
public void setDivideRatio(@PersistentSnapPosition int snapPosition) {
final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget(
snapPosition);
- setDividePosition(snapTarget != null
+ setDividerPosition(snapTarget != null
? snapTarget.position
: mDividerSnapAlgorithm.getMiddleTarget().position,
false /* applyLayoutChange */);
@@ -546,24 +546,24 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
/**
- * Sets new divide position and updates bounds correspondingly. Notifies listener if the new
+ * Sets new divider position and updates bounds correspondingly. Notifies listener if the new
* target indicates dismissing split.
*/
public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
switch (snapTarget.snapPosition) {
case SNAP_TO_START_AND_DISMISS:
- flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
case SNAP_TO_END_AND_DISMISS:
- flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
default:
- flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
- () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
+ flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ () -> setDividerPosition(snapTarget.position, true /* applyLayoutChange */));
break;
}
}
@@ -615,19 +615,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void flingDividerToDismiss(boolean toEnd, int reason) {
final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
: mDividerSnapAlgorithm.getDismissStartTarget().position;
- flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION,
+ flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
}
/** Fling divider from current position to center position. */
public void flingDividerToCenter() {
final int pos = mDividerSnapAlgorithm.getMiddleTarget().position;
- flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION,
- () -> setDividePosition(pos, true /* applyLayoutChange */));
+ flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION,
+ () -> setDividerPosition(pos, true /* applyLayoutChange */));
}
@VisibleForTesting
- void flingDividePosition(int from, int to, int duration,
+ void flingDividerPosition(int from, int to, int duration,
@Nullable Runnable flingFinishedCallback) {
if (from == to) {
if (flingFinishedCallback != null) {
@@ -647,7 +647,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
.setDuration(duration);
mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mDividerFlingAnimator.addUpdateListener(
- animation -> updateDivideBounds((int) animation.getAnimatedValue()));
+ animation -> updateDividerBounds((int) animation.getAnimatedValue()));
mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
index cf3ad4299cea..713d04bce4e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -194,6 +194,10 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi
return mHideSizeCompatRestartButtonTolerance;
}
+ int getDefaultHideRestartButtonTolerance() {
+ return MAX_PERCENTAGE_VAL;
+ }
+
boolean getHasSeenLetterboxEducation(int userId) {
return mLetterboxEduSharedPreferences
.getBoolean(dontShowLetterboxEduKey(userId), /* default= */ false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 4e5c2fa24f25..f195f955692e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -20,11 +20,11 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.AppCompatTaskInfo;
import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.content.Context;
@@ -219,14 +219,30 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
@VisibleForTesting
boolean shouldShowSizeCompatRestartButton(@NonNull TaskInfo taskInfo) {
- if (!Flags.allowHideScmButton()) {
+ // Always show button if display is phone sized.
+ if (!Flags.allowHideScmButton() || taskInfo.configuration.smallestScreenWidthDp
+ < LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP) {
return true;
}
- final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo;
- final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
- final int letterboxArea = computeArea(appCompatTaskInfo.topActivityLetterboxWidth,
- appCompatTaskInfo.topActivityLetterboxHeight);
- final int taskArea = computeArea(taskBounds.width(), taskBounds.height());
+
+ final int letterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth;
+ final int letterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight;
+ final Rect stableBounds = getTaskStableBounds();
+ final int appWidth = stableBounds.width();
+ final int appHeight = stableBounds.height();
+ // App is floating, should always show restart button.
+ if (appWidth > letterboxWidth && appHeight > letterboxHeight) {
+ return true;
+ }
+ // If app fills the width of the display, don't show restart button (for landscape apps)
+ // if device has a custom tolerance value.
+ if (mHideScmTolerance != mCompatUIConfiguration.getDefaultHideRestartButtonTolerance()
+ && appWidth == letterboxWidth) {
+ return false;
+ }
+
+ final int letterboxArea = letterboxWidth * letterboxHeight;
+ final int taskArea = appWidth * appHeight;
if (letterboxArea == 0 || taskArea == 0) {
return false;
}
@@ -234,13 +250,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
return percentageAreaOfLetterboxInTask < mHideScmTolerance;
}
- private int computeArea(int width, int height) {
- if (width == 0 || height == 0) {
- return 0;
- }
- return width * height;
- }
-
private void updateVisibilityOfViews() {
if (mLayout == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 12dce5bf70c0..8b2d0ddf2510 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -45,7 +45,6 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.util.Preconditions;
-import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
@@ -64,6 +63,9 @@ public class PipTransition extends PipTransitionController implements
private static final String TAG = PipTransition.class.getSimpleName();
private static final String PIP_TASK_TOKEN = "pip_task_token";
private static final String PIP_TASK_LEASH = "pip_task_leash";
+ private static final String PIP_START_TX = "pip_start_tx";
+ private static final String PIP_FINISH_TX = "pip_finish_tx";
+ private static final String PIP_DESTINATION_BOUNDS = "pip_dest_bounds";
/**
* The fixed start delay in ms when fading out the content overlay from bounds animation.
@@ -98,6 +100,8 @@ public class PipTransition extends PipTransitionController implements
private WindowContainerToken mPipTaskToken;
@Nullable
private SurfaceControl mPipLeash;
+ @Nullable
+ private Transitions.TransitionFinishCallback mFinishCallback;
public PipTransition(
Context context,
@@ -223,7 +227,6 @@ public class PipTransition extends PipTransitionController implements
return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
} else if (transition == mResizeTransition) {
mResizeTransition = null;
- mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS);
return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback);
}
@@ -246,31 +249,27 @@ public class PipTransition extends PipTransitionController implements
return false;
}
SurfaceControl pipLeash = pipChange.getLeash();
- Rect destinationBounds = pipChange.getEndAbsBounds();
// Even though the final bounds and crop are applied with finishTransaction since
// this is a visible change, we still need to handle the app draw coming in. Snapshot
// covering app draw during collection will be removed by startTransaction. So we make
- // the crop equal to the final bounds and then scale the leash back to starting bounds.
+ // the crop equal to the final bounds and then let the current
+ // animator scale the leash back to starting bounds.
+ // Note: animator is responsible for applying the startTx but NOT finishTx.
startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(),
pipChange.getEndAbsBounds().height());
- startTransaction.setScale(pipLeash,
- (float) mPipBoundsState.getBounds().width() / destinationBounds.width(),
- (float) mPipBoundsState.getBounds().height() / destinationBounds.height());
- startTransaction.apply();
- finishTransaction.setScale(pipLeash,
- (float) mPipBoundsState.getBounds().width() / destinationBounds.width(),
- (float) mPipBoundsState.getBounds().height() / destinationBounds.height());
-
- // We are done with the transition, but will continue animating leash to final bounds.
- finishCallback.onTransitionFinished(null);
-
- // Animate the pip leash with the new buffer
- final int duration = mContext.getResources().getInteger(
- R.integer.config_pipResizeAnimationDuration);
// TODO: b/275910498 Couple this routine with a new implementation of the PiP animator.
- startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration);
+ // Classes interested in continuing the animation would subscribe to this state update
+ // getting info such as endBounds, startTx, and finishTx as an extra Bundle once
+ // animators are in place. Once done state needs to be updated to CHANGED_PIP_BOUNDS.
+ Bundle extra = new Bundle();
+ extra.putParcelable(PIP_START_TX, startTransaction);
+ extra.putParcelable(PIP_FINISH_TX, finishTransaction);
+ extra.putParcelable(PIP_DESTINATION_BOUNDS, pipChange.getEndAbsBounds());
+
+ mFinishCallback = finishCallback;
+ mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra);
return true;
}
@@ -285,12 +284,17 @@ public class PipTransition extends PipTransitionController implements
WindowContainerToken pipTaskToken = pipChange.getContainer();
SurfaceControl pipLeash = pipChange.getLeash();
+ if (pipTaskToken == null || pipLeash == null) {
+ return false;
+ }
+
PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
Rect srcRectHint = params.getSourceRectHint();
Rect startBounds = pipChange.getStartAbsBounds();
Rect destinationBounds = pipChange.getEndAbsBounds();
WindowContainerTransaction finishWct = new WindowContainerTransaction();
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) {
final float scale = (float) destinationBounds.width() / srcRectHint.width();
@@ -316,19 +320,17 @@ public class PipTransition extends PipTransitionController implements
.reparent(overlayLeash, pipLeash)
.setLayer(overlayLeash, Integer.MAX_VALUE);
- if (pipTaskToken != null) {
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
- this::onClientDrawAtTransitionEnd)
- .setScale(overlayLeash, 1f, 1f)
- .setPosition(overlayLeash,
- (destinationBounds.width() - overlaySize) / 2f,
- (destinationBounds.height() - overlaySize) / 2f);
- finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
- }
+ // Overlay needs to be adjusted once a new draw comes in resetting surface transform.
+ tx.setScale(overlayLeash, 1f, 1f);
+ tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f,
+ (destinationBounds.height() - overlaySize) / 2f);
}
startTransaction.apply();
+ tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
+ this::onClientDrawAtTransitionEnd);
+ finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
+
// Note that finishWct should be free of any actual WM state changes; we are using
// it for syncing with the client draw after delayed configuration changes are dispatched.
finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
@@ -412,14 +414,6 @@ public class PipTransition extends PipTransitionController implements
return true;
}
- /**
- * TODO: b/275910498 Use a new implementation of the PiP animator here.
- */
- private void startResizeAnimation(SurfaceControl leash, Rect startBounds,
- Rect endBounds, int duration) {
- mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
- }
-
//
// Various helpers to resolve transition requests and infos
//
@@ -537,6 +531,15 @@ public class PipTransition extends PipTransitionController implements
mPipTransitionState.mPipTaskToken = null;
mPipTransitionState.mPinnedTaskLeash = null;
break;
+ case PipTransitionState.CHANGED_PIP_BOUNDS:
+ // Note: this might not be the end of the animation, rather animator just finished
+ // adjusting startTx and finishTx and is ready to finishTransition(). The animator
+ // can still continue playing the leash into the destination bounds after.
+ if (mFinishCallback != null) {
+ mFinishCallback.onTransitionFinished(null);
+ mFinishCallback = null;
+ }
+ break;
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
index f7bc622b6195..9a9c59e2fa8e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -257,6 +257,7 @@ public class PipTransitionState {
private String stateToString() {
switch (mState) {
case UNDEFINED: return "undefined";
+ case SWIPING_TO_PIP: return "swiping_to_pip";
case ENTERING_PIP: return "entering-pip";
case ENTERED_PIP: return "entered-pip";
case CHANGING_PIP_BOUNDS: return "changing-bounds";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 6aad4e2c9da4..8df287d12cbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -69,7 +69,9 @@ public interface SplitScreen {
default void onSplitVisibilityChanged(boolean visible) {}
}
- /** Callback interface for listening to requests to enter split select */
+ /**
+ * Callback interface for listening to requests to enter split select. Used for desktop -> split
+ */
interface SplitSelectListener {
default boolean onRequestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
int splitPosition, Rect taskBounds) {
@@ -90,6 +92,24 @@ public interface SplitScreen {
/** Unregisters listener that gets split screen callback. */
void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
+ interface SplitInvocationListener {
+ /**
+ * Called whenever shell starts or stops the split screen animation
+ * @param animationRunning if {@code true} the animation has begun, if {@code false} the
+ * animation has finished
+ */
+ default void onSplitAnimationInvoked(boolean animationRunning) { }
+ }
+
+ /**
+ * Registers a {@link SplitInvocationListener} to notify when the animation to enter split
+ * screen has started and stopped
+ *
+ * @param executor callbacks to the listener will be executed on this executor
+ */
+ void registerSplitAnimationListener(@NonNull SplitInvocationListener listener,
+ @NonNull Executor executor);
+
/** Called when device waking up finished. */
void onFinishedWakingUp();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 547457b018a1..b9d70e1a599d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -1166,6 +1166,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
+ public void registerSplitAnimationListener(@NonNull SplitInvocationListener listener,
+ @NonNull Executor executor) {
+ mStageCoordinator.registerSplitAnimationListener(listener, executor);
+ }
+
+ @Override
public void onFinishedWakingUp() {
mMainExecutor.execute(SplitScreenController.this::onFinishedWakingUp);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 1a53a1d10dd2..6e5b7673e206 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -55,6 +55,7 @@ import com.android.wm.shell.transition.OneShotRemoteHandler;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
+import java.util.concurrent.Executor;
/** Manages transition animations for split-screen. */
class SplitScreenTransitions {
@@ -79,6 +80,8 @@ class SplitScreenTransitions {
private Transitions.TransitionFinishCallback mFinishCallback = null;
private SurfaceControl.Transaction mFinishTransaction;
+ private SplitScreen.SplitInvocationListener mSplitInvocationListener;
+ private Executor mSplitInvocationListenerExecutor;
SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
@NonNull Runnable onFinishCallback, StageCoordinator stageCoordinator) {
@@ -353,6 +356,10 @@ class SplitScreenTransitions {
+ " skip to start enter split transition since it already exist. ");
return null;
}
+ if (mSplitInvocationListenerExecutor != null && mSplitInvocationListener != null) {
+ mSplitInvocationListenerExecutor.execute(() -> mSplitInvocationListener
+ .onSplitAnimationInvoked(true /*animationRunning*/));
+ }
final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
setEnterTransition(transition, remoteTransition, extraTransitType, resizeAnim);
return transition;
@@ -457,6 +464,7 @@ class SplitScreenTransitions {
mPendingEnter.onConsumed(aborted);
mPendingEnter = null;
+ mStageCoordinator.notifySplitAnimationFinished();
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for enter transition");
} else if (isPendingDismiss(transition)) {
mPendingDismiss.onConsumed(aborted);
@@ -529,6 +537,12 @@ class SplitScreenTransitions {
mTransitions.getAnimExecutor().execute(va::start);
}
+ public void registerSplitAnimListener(@NonNull SplitScreen.SplitInvocationListener listener,
+ @NonNull Executor executor) {
+ mSplitInvocationListener = listener;
+ mSplitInvocationListenerExecutor = executor;
+ }
+
/** Calls when the transition got consumed. */
interface TransitionConsumedCallback {
void onConsumed(boolean aborted);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 5e9451a09d41..b10176df5826 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -157,6 +157,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.Executor;
/**
* Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
@@ -237,6 +238,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private DefaultMixedHandler mMixedHandler;
private final Toast mSplitUnsupportedToast;
private SplitRequest mSplitRequest;
+ /** Used to notify others of when shell is animating into split screen */
+ private SplitScreen.SplitInvocationListener mSplitInvocationListener;
+ private Executor mSplitInvocationListenerExecutor;
/**
* Since StageCoordinator only coordinates MainStage and SideStage, it shouldn't support
@@ -247,6 +251,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return false;
}
+ /** NOTE: Will overwrite any previously set {@link #mSplitInvocationListener} */
+ public void registerSplitAnimationListener(
+ @NonNull SplitScreen.SplitInvocationListener listener, @NonNull Executor executor) {
+ mSplitInvocationListener = listener;
+ mSplitInvocationListenerExecutor = executor;
+ mSplitTransitions.registerSplitAnimListener(listener, executor);
+ }
+
class SplitRequest {
@SplitPosition
int mActivatePosition;
@@ -535,7 +547,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
null /* childrenToTop */, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("startShortcut",
"side stage was not populated"));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
}
if (finishedCallback != null) {
@@ -666,7 +678,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
null /* childrenToTop */, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("startIntentLegacy",
"side stage was not populated"));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
}
if (apps != null) {
@@ -1287,7 +1299,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("onRemoteAnimationFinishedOrCancelled",
"main or side stage was not populated."));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
} else {
mSyncQueue.queue(evictWct);
mSyncQueue.runInSync(t -> {
@@ -1308,7 +1320,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("onRemoteAnimationFinished",
"main or side stage was not populated"));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return;
}
@@ -2890,6 +2902,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (hasEnteringPip) {
mMixedHandler.animatePendingEnterPipFromSplit(transition, info,
startTransaction, finishTransaction, finishCallback);
+ notifySplitAnimationFinished();
return true;
}
@@ -2924,6 +2937,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// the transition, or synchronize task-org callbacks.
}
// Use normal animations.
+ notifySplitAnimationFinished();
return false;
} else if (mMixedHandler != null && TransitionUtil.hasDisplayChange(info)) {
// A display-change has been un-expectedly inserted into the transition. Redirect
@@ -2937,6 +2951,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.update(startTransaction, true /* resetImePosition */);
startTransaction.apply();
}
+ notifySplitAnimationFinished();
return true;
}
}
@@ -3110,7 +3125,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
pendingEnter.mRemoteHandler.onTransitionConsumed(transition,
false /*aborted*/, finishT);
}
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return true;
}
}
@@ -3139,6 +3154,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final TransitionInfo.Change finalMainChild = mainChild;
final TransitionInfo.Change finalSideChild = sideChild;
enterTransition.setFinishedCallback((callbackWct, callbackT) -> {
+ notifySplitAnimationFinished();
if (finalMainChild != null) {
if (!mainNotContainOpenTask) {
mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId);
@@ -3560,6 +3576,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.isLeftRightSplit());
}
+ private void handleUnsupportedSplitStart() {
+ mSplitUnsupportedToast.show();
+ notifySplitAnimationFinished();
+ }
+
+ void notifySplitAnimationFinished() {
+ if (mSplitInvocationListener == null || mSplitInvocationListenerExecutor == null) {
+ return;
+ }
+ mSplitInvocationListenerExecutor.execute(() ->
+ mSplitInvocationListener.onSplitAnimationInvoked(false /*animationRunning*/));
+ }
+
/**
* Logs the exit of splitscreen to a specific stage. This must be called before the exit is
* executed.
@@ -3622,7 +3651,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (!ENABLE_SHELL_TRANSITIONS) {
StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return;
}
@@ -3642,7 +3671,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
"app package " + taskInfo.baseActivity.getPackageName()
+ " does not support splitscreen, or is a controlled activity type"));
if (splitScreenVisible) {
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 9b2922dff2f5..4d3c76322fa8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -61,6 +61,7 @@ import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.ITransitionPlayer;
import android.window.RemoteTransition;
+import android.window.TaskFragmentOrganizer;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionMetrics;
@@ -183,6 +184,13 @@ public class Transitions implements RemoteCallable<Transitions>,
/** Transition to resize PiP task. */
public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16;
+ /**
+ * The task fragment drag resize transition used by activity embedding.
+ */
+ public static final int TRANSIT_TASK_FRAGMENT_DRAG_RESIZE =
+ // TRANSIT_FIRST_CUSTOM + 17
+ TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE;
+
private final ShellTaskOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 899b7cc0ea0d..22f0adc42f5d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -16,6 +16,9 @@
package com.android.wm.shell.windowdecor
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
import android.annotation.IdRes
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
@@ -30,16 +33,21 @@ import android.view.SurfaceControlViewHost
import android.view.View.OnClickListener
import android.view.View.OnGenericMotionListener
import android.view.View.OnTouchListener
+import android.view.View.SCALE_Y
+import android.view.View.TRANSLATION_Y
+import android.view.View.TRANSLATION_Z
import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
+import android.widget.TextView
import android.window.TaskConstants
import androidx.core.content.withStyledAttributes
import com.android.internal.R.attr.colorAccentPrimary
import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.windowdecor.WindowDecoration.AdditionalWindow
@@ -65,14 +73,13 @@ class MaximizeMenu(
private var maximizeMenu: AdditionalWindow? = null
private lateinit var viewHost: SurfaceControlViewHost
private lateinit var leash: SurfaceControl
- private val shadowRadius = loadDimensionPixelSize(
- R.dimen.desktop_mode_maximize_menu_shadow_radius
- ).toFloat()
+ private val openMenuAnimatorSet = AnimatorSet()
private val cornerRadius = loadDimensionPixelSize(
R.dimen.desktop_mode_maximize_menu_corner_radius
).toFloat()
private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
+ private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
private lateinit var snapRightButton: Button
private lateinit var snapLeftButton: Button
@@ -91,10 +98,12 @@ class MaximizeMenu(
if (maximizeMenu != null) return
createMaximizeMenu()
setupMaximizeMenu()
+ animateOpenMenu()
}
/** Closes the maximize window and releases its view. */
fun close() {
+ openMenuAnimatorSet.cancel()
maximizeMenu?.releaseView()
maximizeMenu = null
}
@@ -134,8 +143,6 @@ class MaximizeMenu(
// Bring menu to front when open
t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
.setPosition(leash, menuPosition.x, menuPosition.y)
- .setWindowCrop(leash, menuWidth, menuHeight)
- .setShadowRadius(leash, shadowRadius)
.setCornerRadius(leash, cornerRadius)
.show(leash)
maximizeMenu = AdditionalWindow(leash, viewHost, transactionSupplier)
@@ -146,6 +153,77 @@ class MaximizeMenu(
}
}
+ private fun animateOpenMenu() {
+ val viewHost = maximizeMenu?.mWindowViewHost
+ val maximizeMenuView = viewHost?.view ?: return
+ val maximizeWindowText = maximizeMenuView.requireViewById<TextView>(
+ R.id.maximize_menu_maximize_window_text)
+ val snapWindowText = maximizeMenuView.requireViewById<TextView>(
+ R.id.maximize_menu_snap_window_text)
+
+ openMenuAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
+ .apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ },
+ ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
+ .apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ addUpdateListener {
+ // Animate padding so that controls stay pinned to the bottom of
+ // the menu.
+ val value = animatedValue as Float
+ val topPadding = menuPadding -
+ ((1 - value) * menuHeight).toInt()
+ maximizeMenuView.setPadding(menuPadding, topPadding,
+ menuPadding, menuPadding)
+ }
+ },
+ ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ addUpdateListener {
+ // Scale up the children of the maximize menu so that the menu
+ // scale is cancelled out and only the background is scaled.
+ val value = animatedValue as Float
+ maximizeButtonLayout.scaleY = value
+ snapButtonsLayout.scaleY = value
+ maximizeWindowText.scaleY = value
+ snapWindowText.scaleY = value
+ }
+ },
+ ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y,
+ (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ },
+ ObjectAnimator.ofInt(maximizeMenuView.background, "alpha",
+ MAX_DRAWABLE_ALPHA_VALUE).apply {
+ duration = ALPHA_ANIMATION_DURATION_MS
+ },
+ ValueAnimator.ofFloat(0f, 1f)
+ .apply {
+ duration = ALPHA_ANIMATION_DURATION_MS
+ startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+ addUpdateListener {
+ val value = animatedValue as Float
+ maximizeButtonLayout.alpha = value
+ snapButtonsLayout.alpha = value
+ maximizeWindowText.alpha = value
+ snapWindowText.alpha = value
+ }
+ },
+ ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION)
+ .apply {
+ duration = ELEVATION_ANIMATION_DURATION_MS
+ startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+ }
+ )
+ openMenuAnimatorSet.start()
+ }
+
private fun loadDimensionPixelSize(resourceId: Int): Int {
return if (resourceId == Resources.ID_NULL) {
0
@@ -263,6 +341,14 @@ class MaximizeMenu(
}
companion object {
+ // Open menu animation constants
+ private const val ALPHA_ANIMATION_DURATION_MS = 50L
+ private const val MAX_DRAWABLE_ALPHA_VALUE = 255
+ private const val STARTING_MENU_HEIGHT_SCALE = 0.8f
+ private const val MENU_HEIGHT_ANIMATION_DURATION_MS = 300L
+ private const val ELEVATION_ANIMATION_DURATION_MS = 50L
+ private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L
+ private const val MENU_Z_TRANSLATION = 1f
fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
return viewId == R.id.maximize_menu ||
viewId == R.id.maximize_menu_maximize_button ||
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index 0f24bb549158..b8a19ad35307 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -13,3 +13,5 @@ nmusgrave@google.com
pbdr@google.com
tkachenkoi@google.com
mpodolian@google.com
+jeremysim@google.com
+peanutbutter@google.com
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index 2ac72affbb0c..ea522cdf2509 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -20,6 +20,8 @@ import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -100,6 +102,20 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
}
@Test
+ public void testTransitionTypeDragResize() {
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TASK_FRAGMENT_DRAG_RESIZE, 0)
+ .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
+ .build();
+ final Animator animator = mAnimRunner.createAnimator(
+ info, mStartTransaction, mFinishTransaction,
+ () -> mFinishCallback.onTransitionFinished(null /* wct */),
+ new ArrayList());
+
+ // The animation should be empty when it is a jump cut for drag resize.
+ assertEquals(0, animator.getDuration());
+ }
+
+ @Test
public void testInvalidCustomAnimation() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
.addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 56d0f8e13f08..8de60b7acc91 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -115,27 +115,27 @@ public class SplitLayoutTests extends ShellTestCase {
@Test
public void testUpdateDivideBounds() {
- mSplitLayout.updateDivideBounds(anyInt());
+ mSplitLayout.updateDividerBounds(anyInt());
verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(),
anyInt());
}
@Test
public void testSetDividePosition() {
- mSplitLayout.setDividePosition(100, false /* applyLayoutChange */);
- assertThat(mSplitLayout.getDividePosition()).isEqualTo(100);
+ mSplitLayout.setDividerPosition(100, false /* applyLayoutChange */);
+ assertThat(mSplitLayout.getDividerPosition()).isEqualTo(100);
verify(mSplitLayoutHandler, never()).onLayoutSizeChanged(any(SplitLayout.class));
- mSplitLayout.setDividePosition(200, true /* applyLayoutChange */);
- assertThat(mSplitLayout.getDividePosition()).isEqualTo(200);
+ mSplitLayout.setDividerPosition(200, true /* applyLayoutChange */);
+ assertThat(mSplitLayout.getDividerPosition()).isEqualTo(200);
verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class));
}
@Test
public void testSetDivideRatio() {
- mSplitLayout.setDividePosition(200, false /* applyLayoutChange */);
+ mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */);
mSplitLayout.setDivideRatio(SNAP_TO_50_50);
- assertThat(mSplitLayout.getDividePosition()).isEqualTo(
+ assertThat(mSplitLayout.getDividerPosition()).isEqualTo(
mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position);
}
@@ -152,7 +152,7 @@ public class SplitLayoutTests extends ShellTestCase {
DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
SNAP_TO_START_AND_DISMISS);
- mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget);
+ mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget);
waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt());
}
@@ -164,7 +164,7 @@ public class SplitLayoutTests extends ShellTestCase {
DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
SNAP_TO_END_AND_DISMISS);
- mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget);
+ mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget);
waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt());
}
@@ -188,7 +188,7 @@ public class SplitLayoutTests extends ShellTestCase {
}
private void waitDividerFlingFinished() {
- verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(),
+ verify(mSplitLayout).flingDividerPosition(anyInt(), anyInt(), anyInt(),
mRunnableCaptor.capture());
mRunnableCaptor.getValue().run();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 5209d0e4bdd0..41a81c1a9921 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -22,6 +22,7 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_A
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -98,14 +99,28 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
private CompatUIWindowManager mWindowManager;
private TaskInfo mTaskInfo;
+ private DisplayLayout mDisplayLayout;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.logicalWidth = TASK_WIDTH;
+ displayInfo.logicalHeight = TASK_HEIGHT;
+ mDisplayLayout = new DisplayLayout(displayInfo,
+ mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false);
+ final InsetsState insetsState = new InsetsState();
+ insetsState.setDisplayFrame(new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT));
+ final InsetsSource insetsSource = new InsetsSource(
+ InsetsSource.createId(null, 0, navigationBars()), navigationBars());
+ insetsSource.setFrame(0, TASK_HEIGHT - 200, TASK_WIDTH, TASK_HEIGHT);
+ insetsState.addSource(insetsSource);
+ mDisplayLayout.setInsets(mContext.getResources(), insetsState);
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
- mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+ mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(),
mCompatUIConfiguration, mOnRestartButtonClicked);
spyOn(mWindowManager);
@@ -363,9 +378,9 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
// Update if the insets change on the existing display layout
clearInvocations(mWindowManager);
- InsetsState insetsState = new InsetsState();
+ final InsetsState insetsState = new InsetsState();
insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000));
- InsetsSource insetsSource = new InsetsSource(
+ final InsetsSource insetsSource = new InsetsSource(
InsetsSource.createId(null, 0, navigationBars()), navigationBars());
insetsSource.setFrame(0, 1800, 1000, 2000);
insetsState.addSource(insetsSource);
@@ -493,16 +508,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
@Test
public void testShouldShowSizeCompatRestartButton() {
mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON);
-
- doReturn(86).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
+ doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
- mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+ mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(),
mCompatUIConfiguration, mOnRestartButtonClicked);
// Simulate rotation of activity in square display
TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN);
- taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
- taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850;
assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
@@ -512,11 +525,21 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
// Simulate folding
- taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000));
- assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
+ final InsetsState insetsState = new InsetsState();
+ insetsState.setDisplayFrame(new Rect(0, 0, 1000, TASK_HEIGHT));
+ final InsetsSource insetsSource = new InsetsSource(
+ InsetsSource.createId(null, 0, navigationBars()), navigationBars());
+ insetsSource.setFrame(0, TASK_HEIGHT - 200, 1000, TASK_HEIGHT);
+ insetsState.addSource(insetsSource);
+ mDisplayLayout.setInsets(mContext.getResources(), insetsState);
+ mWindowManager.updateDisplayLayout(mDisplayLayout);
+ taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP - 100;
+ assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
- taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
- taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 500;
+ // Simulate floating app with 90& area, more than tolerance
+ taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 950;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1900;
assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
}
@@ -529,10 +552,10 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
cameraCompatControlState;
taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
// Letterboxed activity that takes half the screen should show size compat restart button
- taskInfo.configuration.windowConfiguration.setBounds(
- new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT));
taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
+ // Screen width dp larger than a normal phone.
+ taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index befc702b01aa..34b2eebb15a1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -39,10 +39,13 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -63,6 +66,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -105,6 +109,8 @@ public class SplitTransitionTests extends ShellTestCase {
@Mock private ShellExecutor mMainExecutor;
@Mock private LaunchAdjacentController mLaunchAdjacentController;
@Mock private DefaultMixedHandler mMixedHandler;
+ @Mock private SplitScreen.SplitInvocationListener mInvocationListener;
+ private final TestShellExecutor mTestShellExecutor = new TestShellExecutor();
private SplitLayout mSplitLayout;
private MainStage mMainStage;
private SideStage mSideStage;
@@ -147,6 +153,7 @@ public class SplitTransitionTests extends ShellTestCase {
.setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
+ mStageCoordinator.registerSplitAnimationListener(mInvocationListener, mTestShellExecutor);
}
@Test
@@ -452,6 +459,15 @@ public class SplitTransitionTests extends ShellTestCase {
mMainStage.activate(new WindowContainerTransaction(), true /* includingTopTask */);
}
+ @Test
+ @UiThreadTest
+ public void testSplitInvocationCallback() {
+ enterSplit();
+ mTestShellExecutor.flushAll();
+ verify(mInvocationListener, times(1))
+ .onSplitAnimationInvoked(eq(true));
+ }
+
private boolean containsSplitEnter(@NonNull WindowContainerTransaction wct) {
for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i);
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index f9dc5fac7e21..933a33eb8b0e 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -272,7 +272,10 @@ void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t
if (it == mLocked.spotControllers.end()) {
mLocked.spotControllers.try_emplace(displayId, displayId, mContext);
}
- mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits);
+ bool skipScreenshot = mLocked.displaysToSkipScreenshot.find(displayId) !=
+ mLocked.displaysToSkipScreenshot.end();
+ mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits,
+ skipScreenshot);
}
void PointerController::clearSpots() {
@@ -352,6 +355,17 @@ void PointerController::setCustomPointerIcon(const SpriteIcon& icon) {
mCursorController.setCustomPointerIcon(icon);
}
+void PointerController::setSkipScreenshot(int32_t displayId, bool skip) {
+ if (!mEnabled) return;
+
+ std::scoped_lock lock(getLock());
+ if (skip) {
+ mLocked.displaysToSkipScreenshot.insert(displayId);
+ } else {
+ mLocked.displaysToSkipScreenshot.erase(displayId);
+ }
+}
+
void PointerController::doInactivityTimeout() {
fade(Transition::GRADUAL);
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 6ee5707622ca..d76ca5d15a31 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -67,6 +67,7 @@ public:
void clearSpots() override;
void updatePointerIcon(PointerIconStyle iconId) override;
void setCustomPointerIcon(const SpriteIcon& icon) override;
+ void setSkipScreenshot(int32_t displayId, bool skip) override;
virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void doInactivityTimeout();
@@ -115,6 +116,7 @@ private:
std::vector<gui::DisplayInfo> mDisplayInfos;
std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers;
+ std::unordered_set<int32_t /* displayId */> displaysToSkipScreenshot;
} mLocked GUARDED_BY(getLock());
class DisplayInfoListener : public gui::WindowInfosListener {
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index a63453d655e2..0baa9291f54d 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -129,7 +129,7 @@ void SpriteController::doUpdateSprites() {
update.state.surfaceVisible = false;
update.state.surfaceControl =
obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight,
- update.state.displayId);
+ update.state.displayId, update.state.skipScreenshot);
if (update.state.surfaceControl != NULL) {
update.surfaceChanged = surfaceChanged = true;
}
@@ -209,7 +209,7 @@ void SpriteController::doUpdateSprites() {
(update.state.dirty &
(DIRTY_ALPHA | DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER |
DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID | DIRTY_ICON_STYLE |
- DIRTY_DRAW_DROP_SHADOW))))) {
+ DIRTY_DRAW_DROP_SHADOW | DIRTY_SKIP_SCREENSHOT))))) {
needApplyTransaction = true;
if (wantSurfaceVisibleAndDrawn
@@ -260,6 +260,14 @@ void SpriteController::doUpdateSprites() {
t.setLayer(update.state.surfaceControl, surfaceLayer);
}
+ if (wantSurfaceVisibleAndDrawn &&
+ (becomingVisible || (update.state.dirty & DIRTY_SKIP_SCREENSHOT))) {
+ int32_t flags =
+ update.state.skipScreenshot ? ISurfaceComposerClient::eSkipScreenshot : 0;
+ t.setFlags(update.state.surfaceControl, flags,
+ ISurfaceComposerClient::eSkipScreenshot);
+ }
+
if (becomingVisible) {
t.show(update.state.surfaceControl);
@@ -332,8 +340,8 @@ void SpriteController::ensureSurfaceComposerClient() {
}
}
-sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height,
- int32_t displayId) {
+sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height, int32_t displayId,
+ bool hideOnMirrored) {
ensureSurfaceComposerClient();
const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId);
@@ -341,11 +349,13 @@ sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height
ALOGE("Failed to get the parent surface for pointers on display %d", displayId);
}
+ int32_t createFlags = ISurfaceComposerClient::eHidden | ISurfaceComposerClient::eCursorWindow;
+ if (hideOnMirrored) {
+ createFlags |= ISurfaceComposerClient::eSkipScreenshot;
+ }
const sp<SurfaceControl> surfaceControl =
mSurfaceComposerClient->createSurface(String8("Sprite"), width, height,
- PIXEL_FORMAT_RGBA_8888,
- ISurfaceComposerClient::eHidden |
- ISurfaceComposerClient::eCursorWindow,
+ PIXEL_FORMAT_RGBA_8888, createFlags,
parent ? parent->getHandle() : nullptr);
if (surfaceControl == nullptr || !surfaceControl->isValid()) {
ALOGE("Error creating sprite surface.");
@@ -474,6 +484,15 @@ void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
}
}
+void SpriteController::SpriteImpl::setSkipScreenshot(bool skip) {
+ AutoMutex _l(mController.mLock);
+
+ if (mLocked.state.skipScreenshot != skip) {
+ mLocked.state.skipScreenshot = skip;
+ invalidateLocked(DIRTY_SKIP_SCREENSHOT);
+ }
+}
+
void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
bool wasDirty = mLocked.state.dirty;
mLocked.state.dirty |= dirty;
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 35776e9961b3..4e4ba6551aec 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -96,6 +96,10 @@ public:
/* Sets the id of the display where the sprite should be shown. */
virtual void setDisplayId(int32_t displayId) = 0;
+
+ /* Sets the flag to hide sprite on mirrored displays.
+ * This will add ISurfaceComposerClient::eSkipScreenshot flag to the sprite. */
+ virtual void setSkipScreenshot(bool skip) = 0;
};
/*
@@ -152,6 +156,7 @@ private:
DIRTY_DISPLAY_ID = 1 << 7,
DIRTY_ICON_STYLE = 1 << 8,
DIRTY_DRAW_DROP_SHADOW = 1 << 9,
+ DIRTY_SKIP_SCREENSHOT = 1 << 10,
};
/* Describes the state of a sprite.
@@ -182,6 +187,7 @@ private:
int32_t surfaceHeight;
bool surfaceDrawn;
bool surfaceVisible;
+ bool skipScreenshot;
inline bool wantSurfaceVisible() const {
return visible && alpha > 0.0f && icon.isValid();
@@ -209,6 +215,7 @@ private:
virtual void setAlpha(float alpha);
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
virtual void setDisplayId(int32_t displayId);
+ virtual void setSkipScreenshot(bool skip);
inline const SpriteState& getStateLocked() const {
return mLocked.state;
@@ -272,7 +279,8 @@ private:
void doDisposeSurfaces();
void ensureSurfaceComposerClient();
- sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId);
+ sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId,
+ bool hideOnMirrored);
};
} // namespace android
diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp
index 99952aa14904..530d54129791 100644
--- a/libs/input/TouchSpotController.cpp
+++ b/libs/input/TouchSpotController.cpp
@@ -40,12 +40,13 @@ namespace android {
// --- Spot ---
void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float newX, float newY,
- int32_t displayId) {
+ int32_t displayId, bool skipScreenshot) {
sprite->setLayer(Sprite::BASE_LAYER_SPOT + id);
sprite->setAlpha(alpha);
sprite->setTransformationMatrix(SpriteTransformationMatrix(scale, 0.0f, 0.0f, scale));
sprite->setPosition(newX, newY);
sprite->setDisplayId(displayId);
+ sprite->setSkipScreenshot(skipScreenshot);
x = newX;
y = newY;
@@ -84,7 +85,7 @@ TouchSpotController::~TouchSpotController() {
}
void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits) {
+ BitSet32 spotIdBits, bool skipScreenshot) {
#if DEBUG_SPOT_UPDATES
ALOGD("setSpots: idBits=%08x", spotIdBits.value);
for (BitSet32 idBits(spotIdBits); !idBits.isEmpty();) {
@@ -116,7 +117,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32
spot = createAndAddSpotLocked(id, mLocked.displaySpots);
}
- spot->updateSprite(&icon, x, y, mDisplayId);
+ spot->updateSprite(&icon, x, y, mDisplayId, skipScreenshot);
}
for (Spot* spot : mLocked.displaySpots) {
diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h
index 5bbc75d9570b..608653c6a2e7 100644
--- a/libs/input/TouchSpotController.h
+++ b/libs/input/TouchSpotController.h
@@ -32,7 +32,7 @@ public:
TouchSpotController(int32_t displayId, PointerControllerContext& context);
~TouchSpotController();
void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits);
+ BitSet32 spotIdBits, bool skipScreenshot);
void clearSpots();
void reloadSpotResources();
@@ -59,7 +59,8 @@ private:
y(0.0f),
mLastIcon(nullptr) {}
- void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId);
+ void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId,
+ bool skipScreenshot);
void dump(std::string& out, const char* prefix = "") const;
private:
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index a1bb5b3f1cc4..fcf226c9b949 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -372,6 +372,45 @@ TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) {
<< "The pointer display changes to invalid when PointerController is destroyed.";
}
+TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) {
+ ensureDisplayViewportIsSet();
+
+ PointerCoords testSpotCoords;
+ testSpotCoords.clear();
+ testSpotCoords.setAxisValue(AMOTION_EVENT_AXIS_X, 1);
+ testSpotCoords.setAxisValue(AMOTION_EVENT_AXIS_Y, 1);
+ BitSet32 testIdBits;
+ testIdBits.markBit(0);
+ std::array<uint32_t, MAX_POINTER_ID + 1> testIdToIndex;
+
+ sp<MockSprite> testSpotSprite(new NiceMock<MockSprite>);
+
+ // By default sprite is not marked secure
+ EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testSpotSprite));
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
+
+ // Update spots to sync state with sprite
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ADISPLAY_ID_DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
+
+ // Marking the display to skip screenshot should update sprite as well
+ mPointerController->setSkipScreenshot(ADISPLAY_ID_DEFAULT, true);
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(true));
+
+ // Update spots to sync state with sprite
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ADISPLAY_ID_DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
+
+ // Reset flag and verify again
+ mPointerController->setSkipScreenshot(ADISPLAY_ID_DEFAULT, false);
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ADISPLAY_ID_DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
+}
+
class PointerControllerWindowInfoListenerTest : public Test {};
TEST_F(PointerControllerWindowInfoListenerTest,
diff --git a/libs/input/tests/mocks/MockSprite.h b/libs/input/tests/mocks/MockSprite.h
index 013b79c3a3bf..0867221d9eed 100644
--- a/libs/input/tests/mocks/MockSprite.h
+++ b/libs/input/tests/mocks/MockSprite.h
@@ -34,6 +34,7 @@ public:
MOCK_METHOD(void, setAlpha, (float), (override));
MOCK_METHOD(void, setTransformationMatrix, (const SpriteTransformationMatrix&), (override));
MOCK_METHOD(void, setDisplayId, (int32_t), (override));
+ MOCK_METHOD(void, setSkipScreenshot, (bool), (override));
};
} // namespace android
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 554fe5efac3d..b2838c809854 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -2064,24 +2064,31 @@ public final class MediaRouter2 {
}
/**
- * Transfers to a given route for the remote session. The given route must be included in
- * {@link RoutingSessionInfo#getTransferableRoutes()}.
+ * Attempts a transfer to a {@link RoutingSessionInfo#getTransferableRoutes() transferable
+ * route}.
+ *
+ * <p>Transferring to a transferable route does not require the app to transfer the playback
+ * state from one route to the other. The route provider completely manages the transfer. An
+ * example of provider-managed transfers are the switches between the system's routes, like
+ * the built-in speakers and a BT headset.
*
+ * @return True if the transfer is handled by this controller, or false if a new controller
+ * should be created instead.
* @see RoutingSessionInfo#getSelectedRoutes()
* @see RoutingSessionInfo#getTransferableRoutes()
* @see ControllerCallback#onControllerUpdated
*/
- void transferToRoute(@NonNull MediaRoute2Info route) {
+ boolean tryTransferWithinProvider(@NonNull MediaRoute2Info route) {
Objects.requireNonNull(route, "route must not be null");
synchronized (mControllerLock) {
if (isReleased()) {
Log.w(TAG, "transferToRoute: Called on released controller. Ignoring.");
- return;
+ return true;
}
if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) {
Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route);
- return;
+ return false;
}
}
@@ -2096,6 +2103,7 @@ public final class MediaRouter2 {
Log.e(TAG, "Unable to transfer to route for session.", ex);
}
}
+ return true;
}
/**
@@ -3587,20 +3595,14 @@ public final class MediaRouter2 {
}
RoutingController controller = getCurrentController();
- if (controller
- .getRoutingSessionInfo()
- .getTransferableRoutes()
- .contains(route.getId())) {
- controller.transferToRoute(route);
- return;
+ if (!controller.tryTransferWithinProvider(route)) {
+ requestCreateController(
+ controller,
+ route,
+ MANAGER_REQUEST_ID_NONE,
+ Process.myUserHandle(),
+ mContext.getPackageName());
}
-
- requestCreateController(
- controller,
- route,
- MANAGER_REQUEST_ID_NONE,
- Process.myUserHandle(),
- mContext.getPackageName());
}
@Override
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index b52faba79ed7..4c76fb02f7d8 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -44,8 +44,15 @@ public final class PollingFrame implements Parcelable{
/**
* @hide
*/
- @IntDef(prefix = { "POLLING_LOOP_TYPE_"}, value = { POLLING_LOOP_TYPE_A, POLLING_LOOP_TYPE_B,
- POLLING_LOOP_TYPE_F, POLLING_LOOP_TYPE_OFF, POLLING_LOOP_TYPE_ON })
+ @IntDef(prefix = { "POLLING_LOOP_TYPE_"},
+ value = {
+ POLLING_LOOP_TYPE_A,
+ POLLING_LOOP_TYPE_B,
+ POLLING_LOOP_TYPE_F,
+ POLLING_LOOP_TYPE_OFF,
+ POLLING_LOOP_TYPE_ON,
+ POLLING_LOOP_TYPE_UNKNOWN
+ })
@Retention(RetentionPolicy.SOURCE)
@FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
public @interface PollingFrameType {}
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 2aff2c30939f..3fc61545a713 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -46,6 +46,7 @@
android:paddingEnd="@dimen/autofill_view_right_padding"
android:paddingTop="@dimen/autofill_view_top_padding"
android:paddingBottom="@dimen/autofill_view_bottom_padding"
+ android:textDirection="locale"
android:orientation="vertical">
<TextView
@@ -53,6 +54,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:textDirection="locale"
style="@style/autofill.TextTitle"/>
<TextView
@@ -60,6 +62,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ android:textDirection="locale"
style="@style/autofill.TextSubtitle"/>
</LinearLayout>
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 46a51389214f..0bae63a1c525 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -24,7 +24,7 @@
<string name="string_cancel">Cancel</string>
<!-- This is a label for a button that takes user to the next screen. [CHAR LIMIT=20] -->
<string name="string_continue">Continue</string>
- <!-- This is a label for a button that leads to a holistic view of all different options where the user can save their new app credential. [CHAR LIMIT=20] -->
+ <!-- This is a label for a button that leads to a holistic view of all different options where the user can save their new app credential. [CHAR LIMIT=30] -->
<string name="string_more_options">Save another way</string>
<!-- This is a label for a button that links to additional information about passkeys. [CHAR LIMIT=20] -->
<string name="string_learn_more">Learn more</string>
@@ -174,4 +174,4 @@
<!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] -->
<string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string>
<string name="more_options_content_description">More</string>
-</resources> \ No newline at end of file
+</resources>
diff --git a/packages/CredentialManager/res/xml/autofill_service_configuration.xml b/packages/CredentialManager/res/xml/autofill_service_configuration.xml
index 25cc094fa44e..0151add1c8e8 100644
--- a/packages/CredentialManager/res/xml/autofill_service_configuration.xml
+++ b/packages/CredentialManager/res/xml/autofill_service_configuration.xml
@@ -5,6 +5,6 @@
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
-<autofill-service-configuration
+<autofill-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:supportsInlineSuggestions="true"/> \ No newline at end of file
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
index eef21991b845..c96644ca8920 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java
@@ -23,23 +23,23 @@ import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
-import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
-
import androidx.annotation.Nullable;
/**
* Installation failed: Return status code to the caller or display failure UI to user
*/
public class InstallFailed extends Activity {
+
private static final String LOG_TAG = InstallFailed.class.getSimpleName();
- /** Label of the app that failed to install */
+ /**
+ * Label of the app that failed to install
+ */
private CharSequence mLabel;
private AlertDialog mDialog;
@@ -80,29 +80,29 @@ public class InstallFailed extends Activity {
setFinishOnTouchOutside(true);
- int statusCode = getIntent().getIntExtra(PackageInstaller.EXTRA_STATUS,
- PackageInstaller.STATUS_FAILURE);
+ Intent intent = getIntent();
+ int statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ PackageInstaller.STATUS_FAILURE);
+ boolean returnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
- if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
- int legacyStatus = getIntent().getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS,
- PackageManager.INSTALL_FAILED_INTERNAL_ERROR);
+ if (returnResult) {
+ int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS,
+ PackageManager.INSTALL_FAILED_INTERNAL_ERROR);
// Return result if requested
Intent result = new Intent();
result.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus);
setResult(Activity.RESULT_FIRST_USER, result);
finish();
- } else {
- Intent intent = getIntent();
- ApplicationInfo appInfo = intent
- .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);
- Uri packageURI = intent.getData();
+ } else if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) {
+ // statusCode will be STATUS_FAILURE_ABORTED if the update-owner confirmation dialog was
+ // dismissed by the user. We don't want to show a InstallFailed dialog in this case.
+ // If the user denies install permission for normal installs, this dialog will never be
+ // triggered as the status code is returned from PackageInstallerActivity.java
// Set header icon and title
- PackageUtil.AppSnippet as;
- PackageManager pm = getPackageManager();
- as = intent.getParcelableExtra(PackageInstallerActivity.EXTRA_APP_SNIPPET,
- PackageUtil.AppSnippet.class);
+ PackageUtil.AppSnippet as = intent.getParcelableExtra(
+ PackageInstallerActivity.EXTRA_APP_SNIPPET, PackageUtil.AppSnippet.class);
// Store label for dialog
mLabel = as.label;
@@ -127,6 +127,8 @@ public class InstallFailed extends Activity {
// Get status messages
setExplanationFromErrorCode(statusCode);
+ } else {
+ finish();
}
}
@@ -135,6 +137,7 @@ public class InstallFailed extends Activity {
* "manage applications" settings page.
*/
public static class OutOfSpaceDialog extends DialogFragment {
+
private InstallFailed mActivity;
@Override
@@ -147,16 +150,16 @@ public class InstallFailed extends Activity {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(mActivity)
- .setTitle(R.string.out_of_space_dlg_title)
- .setMessage(getString(R.string.out_of_space_dlg_text, mActivity.mLabel))
- .setPositiveButton(R.string.manage_applications, (dialog, which) -> {
- // launch manage applications
- Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE");
- startActivity(intent);
- mActivity.finish();
- })
- .setNegativeButton(R.string.cancel, (dialog, which) -> mActivity.finish())
- .create();
+ .setTitle(R.string.out_of_space_dlg_title)
+ .setMessage(getString(R.string.out_of_space_dlg_text, mActivity.mLabel))
+ .setPositiveButton(R.string.manage_applications, (dialog, which) -> {
+ // launch manage applications
+ Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE");
+ startActivity(intent);
+ mActivity.finish();
+ })
+ .setNegativeButton(R.string.cancel, (dialog, which) -> mActivity.finish())
+ .create();
}
@Override
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
index 1a6c2bb2ec18..59a511db5b3a 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
@@ -30,6 +30,8 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
@@ -91,8 +93,11 @@ public class InstallInstalling extends Activity {
// ContentResolver.SCHEME_FILE
// STAGED_SESSION_ID extra contains an ID of a previously staged install session.
final File sourceFile = new File(mPackageURI.getPath());
- PackageUtil.AppSnippet as = getIntent()
- .getParcelableExtra(EXTRA_APP_SNIPPET, PackageUtil.AppSnippet.class);
+
+ // Dialogs displayed while changing update-owner have a blank icon. To fix this,
+ // fetch the appSnippet from the source file again
+ PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
+ getIntent().putExtra(EXTRA_APP_SNIPPET, as);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
@@ -244,6 +249,14 @@ public class InstallInstalling extends Activity {
super.onDestroy();
}
+ @Override
+ public void finish() {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ super.finish();
+ }
+
/**
* Launch the appropriate finish activity (success or failed) for the installation result.
*
@@ -299,7 +312,11 @@ public class InstallInstalling extends Activity {
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
try {
- session.commit(pendingIntent.getIntentSender());
+ // Delay committing the session by 100ms to fix a UI glitch while displaying the
+ // Update-Owner change dialog on top of the Installing dialog
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ session.commit(pendingIntent.getIntentSender());
+ }, 100);
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot install package: ", e);
launchFailure(PackageInstaller.STATUS_FAILURE,
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
index cf2f85ed5356..13251d8da109 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
@@ -165,7 +165,9 @@ public class InstallStaging extends Activity {
if (mStagingTask != null) {
mStagingTask.cancel(true);
}
-
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
super.onDestroy();
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
index a4c6ac7d95c7..3fea5996e3ef 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@@ -193,6 +193,7 @@ public class InstallStart extends Activity {
if (isSessionInstall) {
nextActivity.setClass(this, PackageInstallerActivity.class);
+ nextActivity.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
} else {
Uri packageUri = intent.getData();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
index 8bed945af32c..e0398aa49dc9 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@@ -176,11 +176,14 @@ public class PackageInstallerActivity extends Activity {
}
private CharSequence getExistingUpdateOwnerLabel() {
+ return getApplicationLabel(getExistingUpdateOwner());
+ }
+
+ private String getExistingUpdateOwner() {
try {
final String packageName = mPkgInfo.packageName;
final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName);
- final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName();
- return getApplicationLabel(existingUpdateOwner);
+ return sourceInfo.getUpdateOwnerPackageName();
} catch (NameNotFoundException e) {
return null;
}
@@ -299,6 +302,18 @@ public class PackageInstallerActivity extends Activity {
}
private void initiateInstall() {
+ final String existingUpdateOwner = getExistingUpdateOwner();
+ if (mSessionId == SessionInfo.INVALID_ID &&
+ !TextUtils.isEmpty(existingUpdateOwner) &&
+ !TextUtils.equals(existingUpdateOwner, mOriginatingPackage)) {
+ // Since update ownership is being changed, the system will request another
+ // user confirmation shortly. Thus, we don't need to ask the user to confirm
+ // installation here.
+ startInstall();
+ return;
+ }
+
+ // Proceed with user confirmation as we are not changing the update-owner in this install.
String pkgName = mPkgInfo.packageName;
// Check if there is already a package on the device with this name
// but it has been renamed to something else.
@@ -465,10 +480,13 @@ public class PackageInstallerActivity extends Activity {
@Override
protected void onDestroy() {
- super.onDestroy();
while (!mActiveUnknownSourcesListeners.isEmpty()) {
unregister(mActiveUnknownSourcesListeners.get(0));
}
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ super.onDestroy();
}
private void bindUi() {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index f7752ffad899..d969d1cf0a80 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -420,25 +420,48 @@ class InstallRepository(private val context: Context) {
* * If AppOP is granted and user action is required to proceed with install
* * If AppOp grant is to be requested from the user
*/
- fun requestUserConfirmation(): InstallStage {
+ fun requestUserConfirmation(): InstallStage? {
return if (isTrustedSource) {
if (localLogv) {
Log.i(LOG_TAG, "Install allowed")
}
- // Returns InstallUserActionRequired stage if install details could be successfully
- // computed, else it returns InstallAborted.
- generateConfirmationSnippet()
+ maybeDeferUserConfirmation()
} else {
val unknownSourceStage = handleUnknownSources(appOpRequestInfo)
if (unknownSourceStage.stageCode == InstallStage.STAGE_READY) {
// Source app already has appOp granted.
- generateConfirmationSnippet()
+ maybeDeferUserConfirmation()
} else {
unknownSourceStage
}
}
}
+ /**
+ * If the update-owner for the incoming app is being changed, defer confirming with the
+ * user and directly proceed with the install. The system will request another
+ * user confirmation shortly.
+ */
+ private fun maybeDeferUserConfirmation(): InstallStage? {
+ // Returns InstallUserActionRequired stage if install details could be successfully
+ // computed, else it returns InstallAborted.
+ val confirmationSnippet: InstallStage = generateConfirmationSnippet()
+
+ val existingUpdateOwner: CharSequence? = getExistingUpdateOwner(newPackageInfo!!)
+ return if (sessionId == SessionInfo.INVALID_ID &&
+ !TextUtils.isEmpty(existingUpdateOwner) &&
+ !TextUtils.equals(existingUpdateOwner, callingPackage)
+ ) {
+ // Since update ownership is being changed, the system will request another
+ // user confirmation shortly. Thus, we don't need to ask the user to confirm
+ // installation here.
+ initiateInstall()
+ null
+ } else {
+ confirmationSnippet
+ }
+ }
+
private fun generateConfirmationSnippet(): InstallStage {
val packageSource: Any?
val pendingUserActionReason: Int
@@ -639,11 +662,14 @@ class InstallRepository(private val context: Context) {
}
private fun getExistingUpdateOwnerLabel(pkgInfo: PackageInfo): CharSequence? {
+ return getApplicationLabel(getExistingUpdateOwner(pkgInfo))
+ }
+
+ private fun getExistingUpdateOwner(pkgInfo: PackageInfo): String? {
return try {
val packageName = pkgInfo.packageName
val sourceInfo = packageManager.getInstallSourceInfo(packageName)
- val existingUpdateOwner = sourceInfo.updateOwnerPackageName
- getApplicationLabel(existingUpdateOwner)
+ sourceInfo.updateOwnerPackageName
} catch (e: PackageManager.NameNotFoundException) {
null
}
@@ -861,7 +887,12 @@ class InstallRepository(private val context: Context) {
}
_installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent))
} else {
- _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message))
+ if (statusCode != PackageInstaller.STATUS_FAILURE_ABORTED) {
+ _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message))
+ } else {
+ _installResult.setValue(InstallAborted(ABORT_REASON_INTERNAL_ERROR))
+ }
+
}
}
@@ -889,8 +920,8 @@ class InstallRepository(private val context: Context) {
* When the identity of the install source could not be determined, user can skip checking the
* source and directly proceed with the install.
*/
- fun forcedSkipSourceCheck(): InstallStage {
- return generateConfirmationSnippet()
+ fun forcedSkipSourceCheck(): InstallStage? {
+ return maybeDeferUserConfirmation()
}
val stagingProgress: LiveData<Int>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
index 072fb2d34928..388e03f023a1 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.distinctUntilChanged
import com.android.packageinstaller.v2.model.InstallRepository
import com.android.packageinstaller.v2.model.InstallStage
import com.android.packageinstaller.v2.model.InstallStaging
@@ -37,6 +38,19 @@ class InstallViewModel(application: Application, val repository: InstallReposito
val currentInstallStage: MutableLiveData<InstallStage>
get() = _currentInstallStage
+ init {
+ // Since installing is an async operation, we may get the install result later in time.
+ // Result of the installation will be set in InstallRepository#installResult.
+ // As such, currentInstallStage will need to add another MutableLiveData as a data source
+ _currentInstallStage.addSource(
+ repository.installResult.distinctUntilChanged()
+ ) { installStage: InstallStage? ->
+ if (installStage != null) {
+ _currentInstallStage.value = installStage
+ }
+ }
+ }
+
fun preprocessIntent(intent: Intent, callerInfo: InstallRepository.CallerInfo) {
val stage = repository.performPreInstallChecks(intent, callerInfo)
if (stage.stageCode == InstallStage.STAGE_ABORTED) {
@@ -62,12 +76,16 @@ class InstallViewModel(application: Application, val repository: InstallReposito
private fun checkIfAllowedAndInitiateInstall() {
val stage = repository.requestUserConfirmation()
- _currentInstallStage.value = stage
+ if (stage != null) {
+ _currentInstallStage.value = stage
+ }
}
fun forcedSkipSourceCheck() {
val stage = repository.forcedSkipSourceCheck()
- _currentInstallStage.value = stage
+ if (stage != null) {
+ _currentInstallStage.value = stage
+ }
}
fun cleanupInstall() {
@@ -80,15 +98,7 @@ class InstallViewModel(application: Application, val repository: InstallReposito
}
fun initiateInstall() {
- // Since installing is an async operation, we will get the install result later in time.
- // Result of the installation will be set in InstallRepository#mInstallResult.
- // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source
repository.initiateInstall()
- _currentInstallStage.addSource(repository.installResult) { installStage: InstallStage? ->
- if (installStage != null) {
- _currentInstallStage.value = installStage
- }
- }
}
val stagedSessionId: Int
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
index 52c489324699..d6704a534c3a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
@@ -47,3 +47,7 @@ val ColorScheme.divider: Color
val ColorScheme.surfaceTone: Color
get() = primary.copy(SettingsOpacity.SurfaceTone)
+
+/** The overall background color in Settings. */
+val ColorScheme.settingsBackground: Color
+ get() = surfaceContainer
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
deleted file mode 100644
index 69aba71413f9..000000000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt
+++ /dev/null
@@ -1,127 +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.settingslib.spa.framework.theme
-
-import android.content.Context
-import android.os.Build
-import androidx.annotation.VisibleForTesting
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-
-data class SettingsColorScheme(
- val background: Color = Color.Unspecified,
- val categoryTitle: Color = Color.Unspecified,
- val surface: Color = Color.Unspecified,
- val surfaceHeader: Color = Color.Unspecified,
- val secondaryText: Color = Color.Unspecified,
- val primaryContainer: Color = Color.Unspecified,
- val onPrimaryContainer: Color = Color.Unspecified,
-)
-
-internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() }
-
-@Composable
-internal fun settingsColorScheme(isDarkTheme: Boolean): SettingsColorScheme {
- val context = LocalContext.current
- return remember(isDarkTheme) {
- when {
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- if (isDarkTheme) dynamicDarkColorScheme(context)
- else dynamicLightColorScheme(context)
- }
- isDarkTheme -> darkColorScheme()
- else -> lightColorScheme()
- }
- }
-}
-
-/**
- * Creates a light dynamic color scheme.
- *
- * Use this function to create a color scheme based off the system wallpaper. If the developer
- * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a
- * light theme variant.
- *
- * @param context The context required to get system resource data.
- */
-@VisibleForTesting
-internal fun dynamicLightColorScheme(context: Context): SettingsColorScheme {
- val tonalPalette = dynamicTonalPalette(context)
- return SettingsColorScheme(
- background = tonalPalette.neutral95,
- categoryTitle = tonalPalette.primary40,
- surface = tonalPalette.neutral99,
- surfaceHeader = tonalPalette.neutral90,
- secondaryText = tonalPalette.neutralVariant30,
- primaryContainer = tonalPalette.primary90,
- onPrimaryContainer = tonalPalette.neutral10,
- )
-}
-
-/**
- * Creates a dark dynamic color scheme.
- *
- * Use this function to create a color scheme based off the system wallpaper. If the developer
- * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a dark
- * theme variant.
- *
- * @param context The context required to get system resource data.
- */
-@VisibleForTesting
-internal fun dynamicDarkColorScheme(context: Context): SettingsColorScheme {
- val tonalPalette = dynamicTonalPalette(context)
- return SettingsColorScheme(
- background = tonalPalette.neutral10,
- categoryTitle = tonalPalette.primary90,
- surface = tonalPalette.neutral20,
- surfaceHeader = tonalPalette.neutral30,
- secondaryText = tonalPalette.neutralVariant80,
- primaryContainer = tonalPalette.secondary90,
- onPrimaryContainer = tonalPalette.neutral10,
- )
-}
-
-@VisibleForTesting
-internal fun darkColorScheme(): SettingsColorScheme {
- val tonalPalette = tonalPalette()
- return SettingsColorScheme(
- background = tonalPalette.neutral10,
- categoryTitle = tonalPalette.primary90,
- surface = tonalPalette.neutral20,
- surfaceHeader = tonalPalette.neutral30,
- secondaryText = tonalPalette.neutralVariant80,
- primaryContainer = tonalPalette.secondary90,
- onPrimaryContainer = tonalPalette.neutral10,
- )
-}
-
-@VisibleForTesting
-internal fun lightColorScheme(): SettingsColorScheme {
- val tonalPalette = tonalPalette()
- return SettingsColorScheme(
- background = tonalPalette.neutral95,
- categoryTitle = tonalPalette.primary40,
- surface = tonalPalette.neutral99,
- surfaceHeader = tonalPalette.neutral90,
- secondaryText = tonalPalette.neutralVariant30,
- primaryContainer = tonalPalette.primary90,
- onPrimaryContainer = tonalPalette.neutral10,
- )
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
index c395558b769c..d9f82e8c6986 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt
@@ -21,7 +21,6 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.ReadOnlyComposable
/**
* The Material 3 Theme for Settings.
@@ -29,24 +28,15 @@ import androidx.compose.runtime.ReadOnlyComposable
@Composable
fun SettingsTheme(content: @Composable () -> Unit) {
val isDarkTheme = isSystemInDarkTheme()
- val settingsColorScheme = settingsColorScheme(isDarkTheme)
- val colorScheme = materialColorScheme(isDarkTheme).copy(
- background = settingsColorScheme.background,
- )
- MaterialTheme(colorScheme = colorScheme, typography = rememberSettingsTypography()) {
+ MaterialTheme(
+ colorScheme = materialColorScheme(isDarkTheme),
+ typography = rememberSettingsTypography(),
+ ) {
CompositionLocalProvider(
- LocalColorScheme provides settingsColorScheme(isDarkTheme),
LocalContentColor provides MaterialTheme.colorScheme.onSurface,
) {
content()
}
}
}
-
-object SettingsTheme {
- val colorScheme: SettingsColorScheme
- @Composable
- @ReadOnlyComposable
- get() = LocalColorScheme.current
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
index 979cf3bddae6..70d353da496c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
@@ -88,9 +88,9 @@ private fun RowScope.ActionButton(actionButton: ActionButton) {
interactionSource = remember(actionButton) { MutableInteractionSource() },
shape = RectangleShape,
colors = ButtonDefaults.filledTonalButtonColors(
- containerColor = SettingsTheme.colorScheme.surface,
- contentColor = SettingsTheme.colorScheme.categoryTitle,
- disabledContainerColor = SettingsTheme.colorScheme.surface,
+ containerColor = MaterialTheme.colorScheme.surface,
+ contentColor = MaterialTheme.colorScheme.primary,
+ disabledContainerColor = MaterialTheme.colorScheme.surface,
),
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 20.dp),
) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
index d08d97eb89db..0546719eb8cd 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -83,7 +83,7 @@ fun SettingsCardContent(
Card(
shape = CornerExtraSmall,
colors = CardDefaults.cardColors(
- containerColor = containerColor.takeOrElse { SettingsTheme.colorScheme.surface },
+ containerColor = containerColor.takeOrElse { MaterialTheme.colorScheme.surface },
),
modifier = Modifier
.fillMaxWidth()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 56534f41c3df..36cd136602f3 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -74,7 +74,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.settingsBackground
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
@@ -140,8 +140,8 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
@Composable
private fun topAppBarColors() = TopAppBarColors(
- containerColor = MaterialTheme.colorScheme.background,
- scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
+ containerColor = MaterialTheme.colorScheme.settingsBackground,
+ scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
index 711c8a753532..feb9737bbf39 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/HomeScaffold.kt
@@ -28,13 +28,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.settingsBackground
@Composable
fun HomeScaffold(title: String, content: @Composable () -> Unit) {
Column(
Modifier
.fillMaxSize()
- .background(color = MaterialTheme.colorScheme.background)
+ .background(color = MaterialTheme.colorScheme.settingsBackground)
.systemBarsPadding()
.verticalScroll(rememberScrollState()),
) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
index c87178db8ffa..a49b358ca782 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -59,6 +59,7 @@ import com.android.settingslib.spa.framework.compose.hideKeyboardAction
import com.android.settingslib.spa.framework.compose.horizontalValues
import com.android.settingslib.spa.framework.theme.SettingsOpacity
import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.settingsBackground
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -90,6 +91,7 @@ fun SearchScaffold(
onSearchQueryChange = { viewModel.searchQuery = it },
)
},
+ containerColor = MaterialTheme.colorScheme.settingsBackground,
) { paddingValues ->
Box(
Modifier
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
index 89194021ecd5..af7a14647570 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -36,6 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.android.settingslib.spa.framework.compose.horizontalValues
import com.android.settingslib.spa.framework.compose.verticalValues
import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.framework.theme.settingsBackground
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
@@ -54,6 +56,7 @@ fun SettingsScaffold(
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { SettingsTopAppBar(title, scrollBehavior, actions) },
+ containerColor = MaterialTheme.colorScheme.settingsBackground,
) { paddingValues ->
Box(Modifier.padding(paddingValues.horizontalValues())) {
content(paddingValues.verticalValues())
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
index 6f2c38caa3bc..60814bf8cc25 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
@@ -51,8 +51,8 @@ internal fun SettingsTab(
.clip(SettingsShape.CornerMedium)
.background(
color = lerp(
- start = SettingsTheme.colorScheme.primaryContainer,
- stop = SettingsTheme.colorScheme.surface,
+ start = MaterialTheme.colorScheme.primaryContainer,
+ stop = MaterialTheme.colorScheme.surface,
fraction = colorFraction,
),
),
@@ -61,8 +61,8 @@ internal fun SettingsTab(
text = title,
style = MaterialTheme.typography.labelLarge,
color = lerp(
- start = SettingsTheme.colorScheme.onPrimaryContainer,
- stop = SettingsTheme.colorScheme.secondaryText,
+ start = MaterialTheme.colorScheme.onPrimaryContainer,
+ stop = MaterialTheme.colorScheme.onSurface,
fraction = colorFraction,
),
)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
index e39b17597406..fc409302a2eb 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt
@@ -37,6 +37,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.settingsBackground
import com.android.settingslib.spa.framework.theme.toMediumWeight
data class BottomAppBarButton(
@@ -54,7 +55,7 @@ fun SuwScaffold(
content: @Composable () -> Unit,
) {
ActivityTitle(title)
- Scaffold { innerPadding ->
+ Scaffold(containerColor = MaterialTheme.colorScheme.settingsBackground) { innerPadding ->
BoxWithConstraints(
Modifier
.padding(innerPadding)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
index 6aac5bf3839a..48cd145da124 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt
@@ -46,7 +46,7 @@ fun CategoryTitle(title: String) {
end = SettingsDimension.itemPaddingEnd,
bottom = 8.dp,
),
- color = SettingsTheme.colorScheme.categoryTitle,
+ color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelMedium,
)
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
index 930d0a1872ab..99b2524f0f9e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
@@ -37,7 +37,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.DpOffset
import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
@Composable
fun CopyableBody(body: String) {
@@ -78,7 +77,7 @@ private fun DropdownMenuTitle(text: String) {
top = SettingsDimension.itemPaddingAround,
bottom = SettingsDimension.buttonPaddingVertical,
),
- color = SettingsTheme.colorScheme.categoryTitle,
+ color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelMedium,
)
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
index d423d9fe5897..6e5f32ebe545 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
@@ -47,7 +47,6 @@ fun SettingsTitle(
modifier = Modifier
.padding(vertical = SettingsDimension.paddingTiny)
.contentDescription(contentDescription),
- color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium.withWeight(useMediumWeight),
)
}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
deleted file mode 100644
index 625663dc9f9a..000000000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt
+++ /dev/null
@@ -1,67 +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.settingslib.spa.framework.theme
-
-import android.content.Context
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import androidx.compose.ui.graphics.Color
-
-@RunWith(AndroidJUnit4::class)
-class SettingsColorsTest {
- private val context: Context = ApplicationProvider.getApplicationContext()
-
- @Test
- fun testDynamicTheme() {
- // The dynamic color could be different in different device, just check basic restrictions:
- // 1. text color is different with background color
- // 2. primary / spinner color is different with its on-item color
- val ls = dynamicLightColorScheme(context)
- assertThat(ls.categoryTitle).isNotEqualTo(ls.background)
- assertThat(ls.secondaryText).isNotEqualTo(ls.background)
- assertThat(ls.primaryContainer).isNotEqualTo(ls.onPrimaryContainer)
-
- val ds = dynamicDarkColorScheme(context)
- assertThat(ds.categoryTitle).isNotEqualTo(ds.background)
- assertThat(ds.secondaryText).isNotEqualTo(ds.background)
- assertThat(ds.primaryContainer).isNotEqualTo(ds.onPrimaryContainer)
- }
-
- @Test
- fun testStaticTheme() {
- val ls = lightColorScheme()
- assertThat(ls.background).isEqualTo(Color(red = 244, green = 239, blue = 244))
- assertThat(ls.categoryTitle).isEqualTo(Color(red = 103, green = 80, blue = 164))
- assertThat(ls.surface).isEqualTo(Color(red = 255, green = 251, blue = 254))
- assertThat(ls.surfaceHeader).isEqualTo(Color(red = 230, green = 225, blue = 229))
- assertThat(ls.secondaryText).isEqualTo(Color(red = 73, green = 69, blue = 79))
- assertThat(ls.primaryContainer).isEqualTo(Color(red = 234, green = 221, blue = 255))
- assertThat(ls.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31))
-
- val ds = darkColorScheme()
- assertThat(ds.background).isEqualTo(Color(red = 28, green = 27, blue = 31))
- assertThat(ds.categoryTitle).isEqualTo(Color(red = 234, green = 221, blue = 255))
- assertThat(ds.surface).isEqualTo(Color(red = 49, green = 48, blue = 51))
- assertThat(ds.surfaceHeader).isEqualTo(Color(red = 72, green = 70, blue = 73))
- assertThat(ds.secondaryText).isEqualTo(Color(red = 202, green = 196, blue = 208))
- assertThat(ds.primaryContainer).isEqualTo(Color(red = 232, green = 222, blue = 248))
- assertThat(ds.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31))
- }
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
index bd8a54bfa4a3..ed7735e38d8a 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsThemeTest.kt
@@ -26,42 +26,35 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.text.font.FontFamily
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
import org.mockito.kotlin.any
-import org.mockito.kotlin.whenever
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class)
class SettingsThemeTest {
- @get:Rule
- val mockito: MockitoRule = MockitoJUnit.rule()
@get:Rule
val composeTestRule = createComposeRule()
- @Mock
- private lateinit var context: Context
+ private val resources = mock<Resources> {
+ on { getString(any()) } doReturn ""
+ }
- @Mock
- private lateinit var resources: Resources
+ private val context = mock<Context> {
+ on { resources } doReturn resources
+ }
private var nextMockResId = 1
- @Before
- fun setUp() {
- whenever(context.resources).thenReturn(resources)
- whenever(resources.getString(any())).thenReturn("")
- }
-
private fun mockAndroidConfig(configName: String, configValue: String) {
- whenever(resources.getIdentifier(configName, "string", "android"))
- .thenReturn(nextMockResId)
- whenever(resources.getString(nextMockResId)).thenReturn(configValue)
+ resources.stub {
+ on { getIdentifier(configName, "string", "android") } doReturn nextMockResId
+ on { getString(nextMockResId) } doReturn configValue
+ }
nextMockResId++
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index 68da1431c594..bededf03a0f4 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -31,6 +31,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.settingslib.spa.framework.compose.LifecycleEffect
import com.android.settingslib.spa.framework.compose.LogCompositions
@@ -49,7 +50,6 @@ import com.android.settingslib.spaprivileged.model.app.AppListViewModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.IAppListViewModel
import com.android.settingslib.spaprivileged.model.app.userId
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
private const val TAG = "AppList"
@@ -95,9 +95,9 @@ internal fun <T : AppRecord> AppListInput<T>.AppListImpl(
LogCompositions(TAG, config.userIds.toString())
val viewModel = viewModelSupplier()
Column(Modifier.fillMaxSize()) {
- val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO)
+ val optionsState = viewModel.spinnerOptionsFlow.collectAsStateWithLifecycle(null)
SpinnerOptions(optionsState, viewModel.optionFlow)
- val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
+ val appListData = viewModel.appListDataFlow.collectAsStateWithLifecycle(null)
listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
index 5a6c0a1bf275..dd7c0368bf4b 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt
@@ -27,8 +27,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spa.testutils.delay
-import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -71,9 +69,8 @@ class DisposableBroadcastReceiverAsUserTest {
DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {}
}
}
- composeTestRule.delay()
- assertThat(registeredBroadcastReceiver).isNotNull()
+ composeTestRule.waitUntil { registeredBroadcastReceiver != null }
}
@Test
@@ -91,9 +88,8 @@ class DisposableBroadcastReceiverAsUserTest {
}
registeredBroadcastReceiver!!.onReceive(context, Intent())
- composeTestRule.delay()
- assertThat(onReceiveIsCalled).isTrue()
+ composeTestRule.waitUntil { onReceiveIsCalled }
}
private companion object {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt
index 70b38feae9d5..cd747cc142c1 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt
@@ -102,7 +102,8 @@ class SettingsGlobalBooleanTest {
delay(100)
value = true
- assertThat(listDeferred.await()).containsExactly(false, true).inOrder()
+ assertThat(listDeferred.await())
+ .containsAtLeast(false, true).inOrder()
}
private companion object {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt
index 29a89be87acd..ecc92f8f8d5c 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt
@@ -102,7 +102,8 @@ class SettingsSecureBooleanTest {
delay(100)
value = true
- assertThat(listDeferred.await()).containsExactly(false, true).inOrder()
+ assertThat(listDeferred.await())
+ .containsAtLeast(false, true).inOrder()
}
private companion object {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index a7b7da598d12..30bec7724dd5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -649,6 +649,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
for (CachedBluetoothDevice cbd : mMemberDevices) {
cbd.setName(name);
}
+ if (mSubDevice != null) {
+ mSubDevice.setName(name);
+ }
}
/**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 4e52c77f27b4..cb6a93002ea7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -176,6 +176,22 @@ public class CachedBluetoothDeviceManager {
}
/**
+ * Sync device status of the pair of the hearing aid if needed.
+ *
+ * @param device the remote device
+ */
+ public synchronized void syncDeviceWithinHearingAidSetIfNeeded(CachedBluetoothDevice device,
+ int state, int profileId) {
+ if (profileId == BluetoothProfile.HAP_CLIENT
+ || profileId == BluetoothProfile.HEARING_AID
+ || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
+ if (state == BluetoothProfile.STATE_CONNECTED) {
+ mHearingAidDeviceManager.syncDeviceIfNeeded(device);
+ }
+ }
+ }
+
+ /**
* Search for existing sub device {@link CachedBluetoothDevice}.
*
* @param device the address of the Bluetooth device
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index 1069b715d946..ed964a9d0f40 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -15,6 +15,8 @@
*/
package com.android.settingslib.bluetooth;
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
@@ -98,6 +100,7 @@ public class HearingAidDeviceManager {
// device.
if (hearingAidDevice != null) {
hearingAidDevice.setSubDevice(newDevice);
+ newDevice.setName(hearingAidDevice.getName());
return true;
}
}
@@ -108,6 +111,10 @@ public class HearingAidDeviceManager {
return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
}
+ private boolean isValidGroupId(int groupId) {
+ return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+
private CachedBluetoothDevice getCachedDevice(long hiSyncId) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
@@ -258,6 +265,27 @@ public class HearingAidDeviceManager {
}
}
+ void syncDeviceIfNeeded(CachedBluetoothDevice device) {
+ final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+ final HapClientProfile hap = profileManager.getHapClientProfile();
+ // Sync preset if device doesn't support synchronization on the remote side
+ if (hap != null && !hap.supportsSynchronizedPresets(device.getDevice())) {
+ final CachedBluetoothDevice mainDevice = findMainDevice(device);
+ if (mainDevice != null) {
+ int mainPresetIndex = hap.getActivePresetIndex(mainDevice.getDevice());
+ int presetIndex = hap.getActivePresetIndex(device.getDevice());
+ if (mainPresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE
+ && mainPresetIndex != presetIndex) {
+ if (DEBUG) {
+ Log.d(TAG, "syncing preset from " + presetIndex + "->"
+ + mainPresetIndex + ", device=" + device);
+ }
+ hap.selectPreset(device.getDevice(), mainPresetIndex);
+ }
+ }
+ }
+ }
+
private void setAudioRoutingConfig(CachedBluetoothDevice device) {
AudioDeviceAttributes hearingDeviceAttributes =
mRoutingHelper.getMatchedHearingDeviceAttributes(device);
@@ -326,7 +354,19 @@ public class HearingAidDeviceManager {
}
CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
+ if (device == null || mCachedDevices == null) {
+ return null;
+ }
+
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
+ if (isValidGroupId(cachedDevice.getGroupId())) {
+ Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
+ for (CachedBluetoothDevice memberDevice : memberSet) {
+ if (memberDevice != null && memberDevice.equals(device)) {
+ return cachedDevice;
+ }
+ }
+ }
if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.equals(device)) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java
index 8e3df8bcc2dd..2de2174a404b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java
@@ -48,17 +48,15 @@ public final class HearingAidStatsLogUtils {
private static final String BT_HEARING_AIDS_PAIRED_HISTORY = "bt_hearing_aids_paired_history";
private static final String BT_HEARING_AIDS_CONNECTED_HISTORY =
"bt_hearing_aids_connected_history";
- private static final String BT_HEARING_DEVICES_PAIRED_HISTORY =
+ private static final String BT_HEARABLE_DEVICES_PAIRED_HISTORY =
"bt_hearing_devices_paired_history";
- private static final String BT_HEARING_DEVICES_CONNECTED_HISTORY =
+ private static final String BT_HEARABLE_DEVICES_CONNECTED_HISTORY =
"bt_hearing_devices_connected_history";
- private static final String BT_HEARING_USER_CATEGORY = "bt_hearing_user_category";
-
private static final String HISTORY_RECORD_DELIMITER = ",";
static final String CATEGORY_HEARING_AIDS = "A11yHearingAidsUser";
static final String CATEGORY_NEW_HEARING_AIDS = "A11yNewHearingAidsUser";
- static final String CATEGORY_HEARING_DEVICES = "A11yHearingDevicesUser";
- static final String CATEGORY_NEW_HEARING_DEVICES = "A11yNewHearingDevicesUser";
+ static final String CATEGORY_HEARABLE_DEVICES = "A11yHearingDevicesUser";
+ static final String CATEGORY_NEW_HEARABLE_DEVICES = "A11yNewHearingDevicesUser";
static final int PAIRED_HISTORY_EXPIRED_DAY = 30;
static final int CONNECTED_HISTORY_EXPIRED_DAY = 7;
@@ -73,14 +71,14 @@ public final class HearingAidStatsLogUtils {
HistoryType.TYPE_UNKNOWN,
HistoryType.TYPE_HEARING_AIDS_PAIRED,
HistoryType.TYPE_HEARING_AIDS_CONNECTED,
- HistoryType.TYPE_HEARING_DEVICES_PAIRED,
- HistoryType.TYPE_HEARING_DEVICES_CONNECTED})
+ HistoryType.TYPE_HEARABLE_DEVICES_PAIRED,
+ HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED})
public @interface HistoryType {
int TYPE_UNKNOWN = -1;
int TYPE_HEARING_AIDS_PAIRED = 0;
int TYPE_HEARING_AIDS_CONNECTED = 1;
- int TYPE_HEARING_DEVICES_PAIRED = 2;
- int TYPE_HEARING_DEVICES_CONNECTED = 3;
+ int TYPE_HEARABLE_DEVICES_PAIRED = 2;
+ int TYPE_HEARABLE_DEVICES_CONNECTED = 3;
}
private static final HashMap<String, Integer> sDeviceAddressToBondEntryMap = new HashMap<>();
@@ -127,8 +125,8 @@ public final class HearingAidStatsLogUtils {
}
/**
- * Updates corresponding history if we found the device is a hearing device after profile state
- * changed.
+ * Updates corresponding history if we found the device is a hearing related device after
+ * profile state changed.
*
* @param context the request context
* @param cachedDevice the remote device
@@ -148,7 +146,7 @@ public final class HearingAidStatsLogUtils {
} else if (cachedDevice.getProfiles().stream().anyMatch(
p -> (p instanceof A2dpSinkProfile || p instanceof HeadsetProfile))) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
- HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_PAIRED);
+ HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_PAIRED);
}
removeFromJustBonded(cachedDevice.getAddress());
}
@@ -161,7 +159,7 @@ public final class HearingAidStatsLogUtils {
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_CONNECTED);
} else if (profile instanceof A2dpSinkProfile || profile instanceof HeadsetProfile) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
- HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
+ HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED);
}
}
}
@@ -169,18 +167,13 @@ public final class HearingAidStatsLogUtils {
/**
* Returns the user category if the user is already categorized. Otherwise, checks the
* history and sees if the user is categorized as one of {@link #CATEGORY_HEARING_AIDS},
- * {@link #CATEGORY_NEW_HEARING_AIDS}, {@link #CATEGORY_HEARING_DEVICES}, and
- * {@link #CATEGORY_NEW_HEARING_DEVICES}.
+ * {@link #CATEGORY_NEW_HEARING_AIDS}, {@link #CATEGORY_HEARABLE_DEVICES}, and
+ * {@link #CATEGORY_NEW_HEARABLE_DEVICES}.
*
* @param context the request context
* @return the category which user belongs to
*/
public static synchronized String getUserCategory(Context context) {
- String userCategory = getSharedPreferences(context).getString(BT_HEARING_USER_CATEGORY, "");
- if (!userCategory.isEmpty()) {
- return userCategory;
- }
-
LinkedList<Long> hearingAidsConnectedHistory = getHistory(context,
HistoryType.TYPE_HEARING_AIDS_CONNECTED);
if (hearingAidsConnectedHistory != null
@@ -192,29 +185,29 @@ public final class HearingAidStatsLogUtils {
// will be categorized as CATEGORY_HEARING_AIDS.
if (hearingAidsPairedHistory != null
&& hearingAidsPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
- userCategory = CATEGORY_NEW_HEARING_AIDS;
+ return CATEGORY_NEW_HEARING_AIDS;
} else {
- userCategory = CATEGORY_HEARING_AIDS;
+ return CATEGORY_HEARING_AIDS;
}
}
- LinkedList<Long> hearingDevicesConnectedHistory = getHistory(context,
- HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
- if (hearingDevicesConnectedHistory != null
- && hearingDevicesConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
- LinkedList<Long> hearingDevicesPairedHistory = getHistory(context,
- HistoryType.TYPE_HEARING_DEVICES_PAIRED);
+ LinkedList<Long> hearableDevicesConnectedHistory = getHistory(context,
+ HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED);
+ if (hearableDevicesConnectedHistory != null
+ && hearableDevicesConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
+ LinkedList<Long> hearableDevicesPairedHistory = getHistory(context,
+ HistoryType.TYPE_HEARABLE_DEVICES_PAIRED);
// Since paired history will be cleared after 30 days. If there's any record within 30
- // days, the user will be categorized as CATEGORY_NEW_HEARING_DEVICES. Otherwise, the
- // user will be categorized as CATEGORY_HEARING_DEVICES.
- if (hearingDevicesPairedHistory != null
- && hearingDevicesPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
- userCategory = CATEGORY_NEW_HEARING_DEVICES;
+ // days, the user will be categorized as CATEGORY_NEW_HEARABLE_DEVICES. Otherwise, the
+ // user will be categorized as CATEGORY_HEARABLE_DEVICES.
+ if (hearableDevicesPairedHistory != null
+ && hearableDevicesPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
+ return CATEGORY_NEW_HEARABLE_DEVICES;
} else {
- userCategory = CATEGORY_HEARING_DEVICES;
+ return CATEGORY_HEARABLE_DEVICES;
}
}
- return userCategory;
+ return "";
}
/**
@@ -245,7 +238,7 @@ public final class HearingAidStatsLogUtils {
}
/**
- * Adds current timestamp into BT hearing devices related history.
+ * Adds current timestamp into BT hearing related devices history.
* @param context the request context
* @param type the type of history to store the data. See {@link HistoryType}.
*/
@@ -279,13 +272,13 @@ public final class HearingAidStatsLogUtils {
static synchronized LinkedList<Long> getHistory(Context context, @HistoryType int type) {
String spName = HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type);
if (BT_HEARING_AIDS_PAIRED_HISTORY.equals(spName)
- || BT_HEARING_DEVICES_PAIRED_HISTORY.equals(spName)) {
+ || BT_HEARABLE_DEVICES_PAIRED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDay(history, PAIRED_HISTORY_EXPIRED_DAY);
return history;
} else if (BT_HEARING_AIDS_CONNECTED_HISTORY.equals(spName)
- || BT_HEARING_DEVICES_CONNECTED_HISTORY.equals(spName)) {
+ || BT_HEARABLE_DEVICES_CONNECTED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDay(history, CONNECTED_HISTORY_EXPIRED_DAY);
@@ -352,9 +345,9 @@ public final class HearingAidStatsLogUtils {
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_CONNECTED, BT_HEARING_AIDS_CONNECTED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
- HistoryType.TYPE_HEARING_DEVICES_PAIRED, BT_HEARING_DEVICES_PAIRED_HISTORY);
+ HistoryType.TYPE_HEARABLE_DEVICES_PAIRED, BT_HEARABLE_DEVICES_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
- HistoryType.TYPE_HEARING_DEVICES_CONNECTED, BT_HEARING_DEVICES_CONNECTED_HISTORY);
+ HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED, BT_HEARABLE_DEVICES_CONNECTED_HISTORY);
}
private HearingAidStatsLogUtils() {}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 4055986e8a57..8dfeb559a8b5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -408,6 +408,8 @@ public class LocalBluetoothProfileManager {
boolean needDispatchProfileConnectionState = true;
if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
|| cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ mDeviceManager.syncDeviceWithinHearingAidSetIfNeeded(cachedDevice, newState,
+ mProfile.getProfileId());
needDispatchProfileConnectionState = !mDeviceManager
.onProfileConnectionStateChangedIfProcessed(cachedDevice, newState,
mProfile.getProfileId());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
index 68f471dd4e4f..d198136447a5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
@@ -45,14 +45,13 @@ val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?>
.buffer(capacity = Channel.CONFLATED)
/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
-val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
+val MediaSessionManager.defaultRemoteSessionChanged: Flow<MediaSession.Token?>
get() =
callbackFlow {
val callback =
object : MediaSessionManager.RemoteSessionCallback {
- override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
- launch { send(sessionToken) }
- }
+ override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) =
+ Unit
override fun onDefaultRemoteSessionChanged(
sessionToken: MediaSession.Token?
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
index e4ac9fe686a3..195ccfcd328d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt
@@ -21,6 +21,7 @@ import android.media.session.MediaSessionManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.bluetooth.headsetAudioModeChanges
import com.android.settingslib.media.session.activeMediaChanges
+import com.android.settingslib.media.session.defaultRemoteSessionChanged
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
import kotlin.coroutines.CoroutineContext
@@ -59,6 +60,9 @@ class MediaControllerRepositoryImpl(
override val activeSessions: StateFlow<List<MediaController>> =
merge(
+ mediaSessionManager.defaultRemoteSessionChanged.map {
+ mediaSessionManager.getActiveSessions(null)
+ },
mediaSessionManager.activeMediaChanges.filterNotNull(),
localBluetoothManager?.headsetAudioModeChanges?.map {
mediaSessionManager.getActiveSessions(null)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index b356f542f480..b4bd4826ea08 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -1703,6 +1703,30 @@ public class CachedBluetoothDeviceTest {
}
@Test
+ public void setName_memberDeviceNameIsSet() {
+ when(mDevice.getAlias()).thenReturn(DEVICE_NAME);
+ when(mSubDevice.getAlias()).thenReturn(DEVICE_NAME);
+
+ mCachedDevice.addMemberDevice(mSubCachedDevice);
+ mCachedDevice.setName(DEVICE_ALIAS);
+
+ verify(mDevice).setAlias(DEVICE_ALIAS);
+ verify(mSubDevice).setAlias(DEVICE_ALIAS);
+ }
+
+ @Test
+ public void setName_subDeviceNameIsSet() {
+ when(mDevice.getAlias()).thenReturn(DEVICE_NAME);
+ when(mSubDevice.getAlias()).thenReturn(DEVICE_NAME);
+
+ mCachedDevice.setSubDevice(mSubCachedDevice);
+ mCachedDevice.setName(DEVICE_ALIAS);
+
+ verify(mDevice).setAlias(DEVICE_ALIAS);
+ verify(mSubDevice).setAlias(DEVICE_ALIAS);
+ }
+
+ @Test
public void getProfileConnectionState_nullProfile_returnDisconnected() {
assertThat(mCachedDevice.getProfileConnectionState(null)).isEqualTo(
BluetoothProfile.STATE_DISCONNECTED);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index aa5a2984e70c..4188d2ec7aaa 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -72,14 +72,18 @@ public class HearingAidDeviceManagerTest {
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
- private final static long HISYNCID1 = 10;
- private final static long HISYNCID2 = 11;
- private final static String DEVICE_NAME_1 = "TestName_1";
- private final static String DEVICE_NAME_2 = "TestName_2";
- private final static String DEVICE_ALIAS_1 = "TestAlias_1";
- private final static String DEVICE_ALIAS_2 = "TestAlias_2";
- private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11";
- private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22";
+ private static final long HISYNCID1 = 10;
+ private static final long HISYNCID2 = 11;
+ private static final int GROUP_ID_1 = 20;
+ private static final int GROUP_ID_2 = 21;
+ private static final int PRESET_INDEX_1 = 1;
+ private static final int PRESET_INDEX_2 = 2;
+ private static final String DEVICE_NAME_1 = "TestName_1";
+ private static final String DEVICE_NAME_2 = "TestName_2";
+ private static final String DEVICE_ALIAS_1 = "TestAlias_1";
+ private static final String DEVICE_ALIAS_2 = "TestAlias_2";
+ private static final String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11";
+ private static final String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22";
private final BluetoothClass DEVICE_CLASS =
createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE);
private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -295,6 +299,7 @@ public class HearingAidDeviceManagerTest {
mHearingAidDeviceManager.setSubDeviceIfNeeded(mCachedDevice2);
assertThat(mCachedDevice1.getSubDevice()).isEqualTo(mCachedDevice2);
+ verify(mDevice2).setAlias(DEVICE_ALIAS_1);
}
/**
@@ -706,14 +711,73 @@ public class HearingAidDeviceManagerTest {
}
@Test
- public void findMainDevice() {
+ public void findMainDevice_sameHiSyncId() {
when(mCachedDevice1.getHiSyncId()).thenReturn(HISYNCID1);
when(mCachedDevice2.getHiSyncId()).thenReturn(HISYNCID1);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
mCachedDevice1.setSubDevice(mCachedDevice2);
- assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).
- isEqualTo(mCachedDevice1);
+ assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo(
+ mCachedDevice1);
+ }
+
+ @Test
+ public void findMainDevice_sameGroupId() {
+ when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+ when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+ mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+ assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo(
+ mCachedDevice1);
+ }
+
+ @Test
+ public void syncDeviceWithinSet_synchronized_differentPresetIndex_shouldNotSync() {
+ when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1);
+ when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2);
+ when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(true);
+ when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(true);
+ when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+ when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+ mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+ mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1);
+
+ verify(mHapClientProfile, never()).selectPreset(any(), anyInt());
+ }
+
+ @Test
+ public void syncDeviceWithinSet_unsynchronized_samePresetIndex_shouldNotSync() {
+ when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1);
+ when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_1);
+ when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false);
+ when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false);
+ when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+ when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+ mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+ mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1);
+
+ verify(mHapClientProfile, never()).selectPreset(any(), anyInt());
+ }
+
+ @Test
+ public void syncDeviceWithinSet_unsynchronized_differentPresetIndex_shouldSync() {
+ when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1);
+ when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2);
+ when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false);
+ when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false);
+ when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+ when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+ mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+ mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice2);
+
+ verify(mHapClientProfile).selectPreset(mDevice2, PRESET_INDEX_1);
}
private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtilsTest.java
index bd5a022e44e5..cd1672104c5a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtilsTest.java
@@ -145,20 +145,29 @@ public class HearingAidStatsLogUtilsTest {
}
@Test
- public void getUserCategory_hearingDevicesUser() {
- prepareHearingDevicesUserHistory();
+ public void getUserCategory_hearableDevicesUser() {
+ prepareHearableDevicesUserHistory();
assertThat(HearingAidStatsLogUtils.getUserCategory(mContext)).isEqualTo(
- HearingAidStatsLogUtils.CATEGORY_HEARING_DEVICES);
+ HearingAidStatsLogUtils.CATEGORY_HEARABLE_DEVICES);
}
@Test
- public void getUserCategory_newHearingDevicesUser() {
- prepareHearingDevicesUserHistory();
+ public void getUserCategory_newHearableDevicesUser() {
+ prepareHearableDevicesUserHistory();
prepareNewUserHistory();
assertThat(HearingAidStatsLogUtils.getUserCategory(mContext)).isEqualTo(
- HearingAidStatsLogUtils.CATEGORY_NEW_HEARING_DEVICES);
+ HearingAidStatsLogUtils.CATEGORY_NEW_HEARABLE_DEVICES);
+ }
+
+ @Test
+ public void getUserCategory_bothHearingAidsAndHearableDevicesUser_returnHearingAidsUser() {
+ prepareHearingAidsUserHistory();
+ prepareHearableDevicesUserHistory();
+
+ assertThat(HearingAidStatsLogUtils.getUserCategory(mContext)).isEqualTo(
+ HearingAidStatsLogUtils.CATEGORY_HEARING_AIDS);
}
private long convertToStartOfDayTime(long timestamp) {
@@ -176,12 +185,12 @@ public class HearingAidStatsLogUtilsTest {
}
}
- private void prepareHearingDevicesUserHistory() {
+ private void prepareHearableDevicesUserHistory() {
final long todayStartOfDay = convertToStartOfDayTime(System.currentTimeMillis());
for (int i = CONNECTED_HISTORY_EXPIRED_DAY - 1; i >= 0; i--) {
final long data = todayStartOfDay - TimeUnit.DAYS.toMillis(i);
HearingAidStatsLogUtils.addToHistory(mContext,
- HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED, data);
+ HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_CONNECTED, data);
}
}
@@ -191,6 +200,6 @@ public class HearingAidStatsLogUtilsTest {
HearingAidStatsLogUtils.addToHistory(mContext,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_PAIRED, data);
HearingAidStatsLogUtils.addToHistory(mContext,
- HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_PAIRED, data);
+ HearingAidStatsLogUtils.HistoryType.TYPE_HEARABLE_DEVICES_PAIRED, data);
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
index cef083584744..6ff90ba4b391 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothPan;
@@ -55,7 +56,9 @@ import java.util.List;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
public class LocalBluetoothProfileManagerTest {
- private final static long HISYNCID = 10;
+ private static final long HISYNCID = 10;
+
+ private static final int GROUP_ID = 1;
@Mock
private LocalBluetoothManager mBtManager;
@Mock
@@ -201,7 +204,8 @@ public class LocalBluetoothProfileManagerTest {
* CachedBluetoothDeviceManager method
*/
@Test
- public void stateChangedHandler_receiveHAPConnectionStateChanged_shouldDispatchDeviceManager() {
+ public void
+ stateChangedHandler_receiveHearingAidConnectionStateChanged_dispatchDeviceManager() {
mShadowBluetoothAdapter.setSupportedProfiles(generateList(
new int[] {BluetoothProfile.HEARING_AID}));
mProfileManager.updateLocalProfiles();
@@ -219,6 +223,28 @@ public class LocalBluetoothProfileManagerTest {
}
/**
+ * Verify BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED with uuid intent will dispatch
+ * to {@link CachedBluetoothDeviceManager} method
+ */
+ @Test
+ public void stateChangedHandler_receiveHapClientConnectionStateChanged_dispatchDeviceManager() {
+ mShadowBluetoothAdapter.setSupportedProfiles(generateList(
+ new int[] {BluetoothProfile.HAP_CLIENT}));
+ mProfileManager.updateLocalProfiles();
+ when(mCachedBluetoothDevice.getGroupId()).thenReturn(GROUP_ID);
+
+ mIntent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+ mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+ mIntent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING);
+ mIntent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
+
+ mContext.sendBroadcast(mIntent);
+
+ verify(mDeviceManager).syncDeviceWithinHearingAidSetIfNeeded(mCachedBluetoothDevice,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.HAP_CLIENT);
+ }
+
+ /**
* Verify BluetoothPan.ACTION_CONNECTION_STATE_CHANGED intent with uuid will dispatch to
* profile connection state changed callback
*/
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 796e3914f3c1..d2e5a13adfce 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -4,13 +4,13 @@ set noparent
dsandler@android.com
-aaronjli@google.com
achalke@google.com
acul@google.com
adamcohen@google.com
aioana@google.com
alexflo@google.com
andonian@google.com
+amiko@google.com
aroederer@google.com
arteiro@google.com
asc@google.com
@@ -39,7 +39,6 @@ hwwang@google.com
hyunyoungs@google.com
ikateryna@google.com
iyz@google.com
-jamesoleary@google.com
jbolinger@google.com
jdemeulenaere@google.com
jeffdq@google.com
@@ -82,6 +81,7 @@ pixel@google.com
pomini@google.com
princedonkor@google.com
rahulbanerjee@google.com
+rgl@google.com
roosa@google.com
saff@google.com
santie@google.com
@@ -110,6 +110,3 @@ yeinj@google.com
yuandizhou@google.com
yurilin@google.com
zakcohen@google.com
-
-#Android TV
-rgl@google.com
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 0c89a5dcbcf4..deab81856afa 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -59,13 +59,16 @@
]
}
],
-
+
"auto-end-to-end-postsubmit": [
{
"name": "AndroidAutomotiveHomeTests",
"options" : [
{
"include-filter": "android.platform.tests.HomeTest"
+ },
+ {
+ "exclude-filter": "android.platform.tests.HomeTest#testAssistantWidget"
}
]
},
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index c8f9135e9431..991ce12df2e5 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -31,7 +31,6 @@ import static com.android.systemui.accessibility.accessibilitymenu.Accessibility
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.Instrumentation;
@@ -90,6 +89,7 @@ public class AccessibilityMenuServiceTest {
private static Instrumentation sInstrumentation;
private static UiAutomation sUiAutomation;
private static UiDevice sUiDevice;
+ private static String sLockSettings;
private static final AtomicInteger sLastGlobalAction = new AtomicInteger(NO_GLOBAL_ACTION);
private static final AtomicBoolean sOpenBlocked = new AtomicBoolean(false);
@@ -108,6 +108,11 @@ public class AccessibilityMenuServiceTest {
sUiAutomation.adoptShellPermissionIdentity(
UiAutomation.ALL_PERMISSIONS.toArray(new String[0]));
sUiDevice = UiDevice.getInstance(sInstrumentation);
+ sLockSettings = sUiDevice.executeShellCommand("locksettings get-disabled");
+ Log.i(TAG, "locksettings get-disabled returns " + sLockSettings);
+ // Some test in the test class requires the device to be in lock screen
+ // ensure we have locksettings enabled before running the tests
+ sUiDevice.executeShellCommand("locksettings set-disabled false");
final Context context = sInstrumentation.getTargetContext();
sAccessibilityManager = context.getSystemService(AccessibilityManager.class);
@@ -157,9 +162,10 @@ public class AccessibilityMenuServiceTest {
}
@AfterClass
- public static void classTeardown() {
+ public static void classTeardown() throws IOException {
Settings.Secure.putString(sInstrumentation.getTargetContext().getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "");
+ sUiDevice.executeShellCommand("locksettings set-disabled " + sLockSettings);
}
@Before
@@ -184,17 +190,17 @@ public class AccessibilityMenuServiceTest {
return root != null && root.getPackageName().toString().equals(PACKAGE_NAME);
}
- private static void wakeUpScreen() throws IOException {
+ private static void wakeUpScreen() {
sUiDevice.pressKeyCode(KeyEvent.KEYCODE_WAKEUP);
WaitUtils.waitForValueToSettle("Screen On", AccessibilityMenuServiceTest::isScreenOn);
- assertWithMessage("Screen is on").that(isScreenOn()).isTrue();
+ WaitUtils.ensureThat("Screen is on", AccessibilityMenuServiceTest::isScreenOn);
}
- private static void closeScreen() throws Throwable {
+ private static void closeScreen() {
// go/adb-cheats#lock-screen
sUiDevice.pressKeyCode(KeyEvent.KEYCODE_SLEEP);
WaitUtils.waitForValueToSettle("Screen Off", AccessibilityMenuServiceTest::isScreenOff);
- assertWithMessage("Screen is off").that(isScreenOff()).isTrue();
+ WaitUtils.ensureThat("Screen is off", AccessibilityMenuServiceTest::isScreenOff);
WaitUtils.ensureThat(
"Screen is locked", () -> sKeyguardManager.isKeyguardLocked());
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index fd90bd945e0a..de090f4b2a6d 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -581,6 +581,16 @@ flag {
}
flag {
+ name: "contextual_tips_assistant_dismiss_fix"
+ namespace: "systemui"
+ description: "Improve assistant dismiss signal accuracy for contextual tips."
+ bug: "334759504"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "shaderlib_loading_effect_refactor"
namespace: "systemui"
description: "Extend shader library to provide the common loading effects."
@@ -796,7 +806,7 @@ flag {
name: "dream_input_session_pilfer_once"
namespace: "systemui"
description: "Pilfer at most once per input session"
- bug: "324600132"
+ bug: "333596426"
metadata {
purpose: PURPOSE_BUGFIX
}
diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp
index addcaf4f6319..04ac748d0c78 100644
--- a/packages/SystemUI/checks/Android.bp
+++ b/packages/SystemUI/checks/Android.bp
@@ -38,8 +38,9 @@ java_test_host {
defaults: ["AndroidLintCheckerTestDefaults"],
srcs: ["tests/**/*.kt"],
data: [
- ":framework",
":androidx.annotation_annotation",
+ ":dagger2",
+ ":framework",
":kotlinx-coroutines-core",
],
static_libs: [
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SingletonAndroidComponentDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SingletonAndroidComponentDetector.kt
new file mode 100644
index 000000000000..68ec1ee8af97
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SingletonAndroidComponentDetector.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.AnnotationInfo
+import com.android.tools.lint.detector.api.AnnotationUsageInfo
+import com.android.tools.lint.detector.api.AnnotationUsageType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+
+/**
+ * Prevents binding Activities, Services, and BroadcastReceivers as Singletons in the Dagger graph.
+ *
+ * It is OK to mark a BroadcastReceiver as singleton as long as it is being constructed/injected and
+ * registered directly in the code. If instead it is declared in the manifest, and we let Android
+ * construct it for us, we also need to let Android destroy it for us, so don't allow marking it as
+ * singleton.
+ */
+class SingletonAndroidComponentDetector : Detector(), SourceCodeScanner {
+ override fun applicableAnnotations(): List<String> {
+ return listOf(
+ "com.android.systemui.dagger.SysUISingleton",
+ )
+ }
+
+ override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean =
+ type == AnnotationUsageType.DEFINITION
+
+ override fun visitAnnotationUsage(
+ context: JavaContext,
+ element: UElement,
+ annotationInfo: AnnotationInfo,
+ usageInfo: AnnotationUsageInfo
+ ) {
+ if (element !is UAnnotation) {
+ return
+ }
+
+ val parent = element.uastParent ?: return
+
+ if (isInvalidBindingMethod(parent)) {
+ context.report(
+ ISSUE,
+ element,
+ context.getLocation(element),
+ "Do not bind Activities, Services, or BroadcastReceivers as Singleton."
+ )
+ } else if (isInvalidClassDeclaration(parent)) {
+ context.report(
+ ISSUE,
+ element,
+ context.getLocation(element),
+ "Do not mark Activities or Services as Singleton."
+ )
+ }
+ }
+
+ private fun isInvalidBindingMethod(parent: UElement): Boolean {
+ if (parent !is UMethod) {
+ return false
+ }
+
+ if (
+ parent.returnType?.canonicalText !in
+ listOf(
+ "android.app.Activity",
+ "android.app.Service",
+ "android.content.BroadcastReceiver",
+ )
+ ) {
+ return false
+ }
+
+ if (
+ !MULTIBIND_ANNOTATIONS.all { it in parent.annotations.map { it.qualifiedName } } &&
+ !MULTIPROVIDE_ANNOTATIONS.all { it in parent.annotations.map { it.qualifiedName } }
+ ) {
+ return false
+ }
+ return true
+ }
+
+ private fun isInvalidClassDeclaration(parent: UElement): Boolean {
+ if (parent !is UClass) {
+ return false
+ }
+
+ if (
+ parent.javaPsi.superClass?.qualifiedName !in
+ listOf(
+ "android.app.Activity",
+ "android.app.Service",
+ // Fine to mark BroadcastReceiver as singleton in this scenario
+ )
+ ) {
+ return false
+ }
+
+ return true
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE: Issue =
+ Issue.create(
+ id = "SingletonAndroidComponent",
+ briefDescription = "Activity, Service, or BroadcastReceiver marked as Singleton",
+ explanation =
+ """Activities, Services, and BroadcastReceivers are created and destroyed by
+ the Android System Server. Marking them with a Dagger scope
+ results in them being cached and reused by Dagger. Trying to reuse a
+ component like this will make for a very bad time.""",
+ category = Category.CORRECTNESS,
+ priority = 10,
+ severity = Severity.ERROR,
+ moreInfo =
+ "https://developer.android.com/guide/components/activities/process-lifecycle",
+ // Note that JAVA_FILE_SCOPE also includes Kotlin source files.
+ implementation =
+ Implementation(
+ SingletonAndroidComponentDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ private val MULTIBIND_ANNOTATIONS =
+ listOf("dagger.Binds", "dagger.multibindings.IntoMap", "dagger.multibindings.ClassKey")
+
+ val MULTIPROVIDE_ANNOTATIONS =
+ listOf(
+ "dagger.Provides",
+ "dagger.multibindings.IntoMap",
+ "dagger.multibindings.ClassKey"
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index e93264c8e7b3..cecbc474a18a 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -40,6 +40,7 @@ class SystemUIIssueRegistry : IssueRegistry() {
RegisterReceiverViaContextDetector.ISSUE,
SoftwareBitmapDetector.ISSUE,
NonInjectedServiceDetector.ISSUE,
+ SingletonAndroidComponentDetector.ISSUE,
StaticSettingsProviderDetector.ISSUE,
DemotingTestWithoutBugDetector.ISSUE,
TestFunctionNameViolationDetector.ISSUE,
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index e1cca88a8fe0..8396f3fee892 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -21,8 +21,9 @@ import java.io.File
internal val libraryNames =
arrayOf(
- "framework.jar",
"androidx.annotation_annotation.jar",
+ "dagger2.jar",
+ "framework.jar",
"kotlinx-coroutines-core.jar",
)
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SingletonAndroidComponentDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SingletonAndroidComponentDetectorTest.kt
new file mode 100644
index 000000000000..0606af80f27a
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SingletonAndroidComponentDetectorTest.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class SingletonAndroidComponentDetectorTest : SystemUILintDetectorTest() {
+ override fun getDetector(): Detector = SingletonAndroidComponentDetector()
+
+ override fun getIssues(): List<Issue> = listOf(SingletonAndroidComponentDetector.ISSUE)
+
+ @Test
+ fun testBindsServiceAsSingleton() {
+ lint()
+ .files(
+ TestFiles.kotlin(
+ """
+ package test.pkg
+
+ import android.app.Service
+ import com.android.systemui.dagger.SysUISingleton
+ import dagger.Binds
+ import dagger.Module
+ import dagger.multibindings.ClassKey
+ import dagger.multibindings.IntoMap
+
+ @Module
+ interface BadModule {
+ @SysUISingleton
+ @Binds
+ @IntoMap
+ @ClassKey(SingletonService::class)
+ fun bindSingletonService(service: SingletonService): Service
+ }
+ """
+ .trimIndent()
+ ),
+ *stubs
+ )
+ .issues(SingletonAndroidComponentDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/BadModule.kt:12: Error: Do not bind Activities, Services, or BroadcastReceivers as Singleton. [SingletonAndroidComponent]
+ @SysUISingleton
+ ~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testProvidesBroadcastReceiverAsSingleton() {
+ lint()
+ .files(
+ TestFiles.kotlin(
+ """
+ package test.pkg
+
+ import android.content.BroadcastReceiver
+ import com.android.systemui.dagger.SysUISingleton
+ import dagger.Provides
+ import dagger.Module
+ import dagger.multibindings.ClassKey
+ import dagger.multibindings.IntoMap
+
+ @Module
+ abstract class BadModule {
+ @SysUISingleton
+ @Provides
+ @IntoMap
+ @ClassKey(SingletonBroadcastReceiver::class)
+ fun providesSingletonBroadcastReceiver(br: SingletonBroadcastReceiver): BroadcastReceiver {
+ return br
+ }
+ }
+ """
+ .trimIndent()
+ ),
+ *stubs
+ )
+ .issues(SingletonAndroidComponentDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/BadModule.kt:12: Error: Do not bind Activities, Services, or BroadcastReceivers as Singleton. [SingletonAndroidComponent]
+ @SysUISingleton
+ ~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+ @Test
+ fun testMarksActivityAsSingleton() {
+ lint()
+ .files(
+ TestFiles.kotlin(
+ """
+ package test.pkg
+
+ import android.app.Activity
+ import com.android.systemui.dagger.SysUISingleton
+
+ @SysUISingleton
+ class BadActivity : Activity() {
+ }
+ """
+ .trimIndent()
+ ),
+ *stubs
+ )
+ .issues(SingletonAndroidComponentDetector.ISSUE)
+ .run()
+ .expect(
+ """
+ src/test/pkg/BadActivity.kt:6: Error: Do not mark Activities or Services as Singleton. [SingletonAndroidComponent]
+ @SysUISingleton
+ ~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+ @Test
+ fun testMarksBroadcastReceiverAsSingleton() {
+ lint()
+ .files(
+ TestFiles.kotlin(
+ """
+ package test.pkg
+
+ import android.content.BroadcastReceiver
+ import com.android.systemui.dagger.SysUISingleton
+
+ @SysUISingleton
+ class SingletonReceveiver : BroadcastReceiver() {
+ }
+ """
+ .trimIndent()
+ ),
+ *stubs
+ )
+ .issues(SingletonAndroidComponentDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ // Define stubs for Android imports. The tests don't run on Android so
+ // they don't "see" any of Android specific classes. We need to define
+ // the method parameters for proper resolution.
+ private val singletonStub: TestFile =
+ java(
+ """
+ package com.android.systemui.dagger;
+
+ public @interface SysUISingleton {
+ }
+ """
+ )
+
+ private val stubs = arrayOf(singletonStub) + androidStubs
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt
new file mode 100644
index 000000000000..9b736b8edcbf
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.scene
+
+import com.android.systemui.notifications.ui.composable.NotificationsShadeScene
+import com.android.systemui.scene.shared.model.Scene
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+interface NotificationsShadeSceneModule {
+
+ @Binds @IntoSet fun notificationsShade(scene: NotificationsShadeScene): Scene
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt
new file mode 100644
index 000000000000..3d7401d8f263
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.scene
+
+import com.android.systemui.qs.ui.composable.QuickSettingsShadeScene
+import com.android.systemui.scene.shared.model.Scene
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+interface QuickSettingsShadeSceneModule {
+
+ @Binds @IntoSet fun quickSettingsShade(scene: QuickSettingsShadeScene): Scene
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index d59f1f5bbe25..f73b6cd156f3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -28,6 +28,7 @@ import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
@@ -39,6 +40,10 @@ object Bouncer {
val Background = ElementKey("BouncerBackground")
val Content = ElementKey("BouncerContent")
}
+
+ object TestTags {
+ const val Root = "bouncer_root"
+ }
}
/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
@@ -78,7 +83,9 @@ private fun SceneScope.BouncerScene(
BouncerContent(
viewModel,
dialogFactory,
- Modifier.element(Bouncer.Elements.Content).fillMaxSize()
+ Modifier.sysuiResTag(Bouncer.TestTags.Root)
+ .element(Bouncer.Elements.Content)
+ .fillMaxSize()
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index c34f2fd26d0c..2dcd0ff05c73 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -52,6 +52,7 @@ import androidx.compose.ui.unit.dp
import com.android.compose.PlatformIconButton
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.common.ui.compose.SelectedUserAwareInputConnection
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.res.R
/** UI for the input part of a password-requiring version of the bouncer. */
@@ -105,6 +106,7 @@ internal fun PasswordBouncer(
),
modifier =
modifier
+ .sysuiResTag("bouncer_text_entry")
.focusRequester(focusRequester)
.onFocusChanged { viewModel.onTextFieldFocusChanged(it.isFocused) }
.drawBehind {
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 7af8408938a0..d7e9c10e7224 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
@@ -52,6 +52,7 @@ import com.android.compose.modifiers.thenIf
import com.android.internal.R
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
+import com.android.systemui.compose.modifiers.sysuiResTag
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
@@ -234,6 +235,7 @@ internal fun PatternBouncer(
Canvas(
modifier
+ .sysuiResTag("bouncer_pattern_root")
// Because the width also includes spacing to the left and right of the leftmost and
// rightmost dots in the grid and because UX mocks specify the width without that
// spacing, the actual width needs to be defined slightly bigger than the UX mock width.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index a592aa95195f..79b57ca74f7d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -469,6 +469,8 @@ private fun BoxScope.CommunalHubLazyGrid(
size = size,
selected = selected && !isDragging,
widgetConfigurator = widgetConfigurator,
+ index = index,
+ contentListState = contentListState
)
}
} else {
@@ -478,6 +480,8 @@ private fun BoxScope.CommunalHubLazyGrid(
viewModel = viewModel,
size = size,
selected = false,
+ index = index,
+ contentListState = contentListState
)
}
}
@@ -782,10 +786,21 @@ private fun CommunalContent(
selected: Boolean,
modifier: Modifier = Modifier,
widgetConfigurator: WidgetConfigurator? = null,
+ index: Int,
+ contentListState: ContentListState,
) {
when (model) {
is CommunalContentModel.WidgetContent.Widget ->
- WidgetContent(viewModel, model, size, selected, widgetConfigurator, modifier)
+ WidgetContent(
+ viewModel,
+ model,
+ size,
+ selected,
+ widgetConfigurator,
+ modifier,
+ index,
+ contentListState
+ )
is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
is CommunalContentModel.WidgetContent.DisabledWidget ->
DisabledWidgetPlaceholder(model, viewModel, modifier)
@@ -883,6 +898,8 @@ private fun WidgetContent(
selected: Boolean,
widgetConfigurator: WidgetConfigurator?,
modifier: Modifier = Modifier,
+ index: Int,
+ contentListState: ContentListState,
) {
val context = LocalContext.current
val isFocusable by viewModel.isFocusable.collectAsState(initial = false)
@@ -891,6 +908,11 @@ private fun WidgetContent(
model.providerInfo.loadLabel(context.packageManager).toString().trim()
}
val clickActionLabel = stringResource(R.string.accessibility_action_label_select_widget)
+ val removeWidgetActionLabel = stringResource(R.string.accessibility_action_label_remove_widget)
+ val placeWidgetActionLabel = stringResource(R.string.accessibility_action_label_place_widget)
+ val selectedKey by viewModel.selectedKey.collectAsState()
+ val selectedIndex =
+ selectedKey?.let { key -> contentListState.list.indexOfFirst { it.key == key } }
Box(
modifier =
modifier
@@ -907,6 +929,36 @@ private fun WidgetContent(
Modifier.semantics {
contentDescription = accessibilityLabel
onClick(label = clickActionLabel, action = null)
+ val deleteAction =
+ CustomAccessibilityAction(removeWidgetActionLabel) {
+ contentListState.onRemove(index)
+ contentListState.onSaveList()
+ true
+ }
+ val selectWidgetAction =
+ CustomAccessibilityAction(clickActionLabel) {
+ val currentWidgetKey =
+ index?.let {
+ keyAtIndexIfEditable(contentListState.list, index)
+ }
+ viewModel.setSelectedKey(currentWidgetKey)
+ true
+ }
+
+ val actions = mutableListOf(deleteAction, selectWidgetAction)
+
+ if (selectedIndex != null && selectedIndex != index) {
+ actions.add(
+ CustomAccessibilityAction(placeWidgetActionLabel) {
+ contentListState.onMove(selectedIndex!!, index)
+ contentListState.onSaveList()
+ viewModel.setSelectedKey(null)
+ true
+ }
+ )
+ }
+
+ customActions = actions
}
}
) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 722032c19553..63c70c97ed07 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -36,6 +36,7 @@ import androidx.compose.ui.unit.IntOffset
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.modifiers.thenIf
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
@@ -79,7 +80,7 @@ constructor(
}
SceneTransitionLayout(
- modifier = modifier,
+ modifier = modifier.sysuiResTag("keyguard_clock_container"),
currentScene = currentScene,
onChangeScene = {},
transitions = ClockTransition.defaultClockTransitions,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
index 2ba78cfd7785..fdf82ca026b1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt
@@ -30,11 +30,15 @@ import com.android.compose.nestedscroll.PriorityNestedScrollConnection
*/
fun NotificationScrimNestedScrollConnection(
scrimOffset: () -> Float,
- onScrimOffsetChanged: (Float) -> Unit,
+ snapScrimOffset: (Float) -> Unit,
+ animateScrimOffset: (Float) -> Unit,
minScrimOffset: () -> Float,
maxScrimOffset: Float,
contentHeight: () -> Float,
minVisibleScrimHeight: () -> Float,
+ isCurrentGestureOverscroll: () -> Boolean,
+ onStart: (Float) -> Unit = {},
+ onStop: (Float) -> Unit = {},
): PriorityNestedScrollConnection {
return PriorityNestedScrollConnection(
orientation = Orientation.Vertical,
@@ -49,7 +53,7 @@ fun NotificationScrimNestedScrollConnection(
// scrolling down and content is done scrolling to top. After that, the scrim
// needs to collapse; collapse the scrim until it is at the maxScrimOffset.
canStartPostScroll = { offsetAvailable, _ ->
- offsetAvailable > 0 && scrimOffset() < maxScrimOffset
+ offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll())
},
canStartPostFling = { false },
canContinueScroll = {
@@ -57,7 +61,7 @@ fun NotificationScrimNestedScrollConnection(
minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
},
canScrollOnFling = true,
- onStart = { /* do nothing */},
+ onStart = { offsetAvailable -> onStart(offsetAvailable) },
onScroll = { offsetAvailable ->
val currentHeight = scrimOffset()
val amountConsumed =
@@ -68,10 +72,16 @@ fun NotificationScrimNestedScrollConnection(
val amountLeft = minScrimOffset() - currentHeight
offsetAvailable.coerceAtLeast(amountLeft)
}
- onScrimOffsetChanged(currentHeight + amountConsumed)
+ snapScrimOffset(currentHeight + amountConsumed)
amountConsumed
},
// Don't consume the velocity on pre/post fling
- onStop = { 0f },
+ onStop = { velocityAvailable ->
+ onStop(velocityAvailable)
+ if (scrimOffset() < minScrimOffset()) {
+ animateScrimOffset(minScrimOffset())
+ }
+ 0f
+ },
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 6e987bd03483..16ae5b1e1562 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -18,6 +18,7 @@
package com.android.systemui.notifications.ui.composable
import android.util.Log
+import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
@@ -39,8 +40,8 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -77,6 +78,7 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
+import kotlinx.coroutines.launch
object Notifications {
object Elements {
@@ -159,11 +161,13 @@ fun SceneScope.NotificationScrollingStack(
shouldPunchHoleBehindScrim: Boolean,
modifier: Modifier = Modifier,
) {
+ val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
val screenCornerRadius = LocalScreenCornerRadius.current
val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
val scrollState = rememberScrollState()
val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f)
+ val isCurrentGestureOverscroll = viewModel.isCurrentGestureOverscroll.collectAsState(false)
val expansionFraction by viewModel.expandFraction.collectAsState(0f)
val navBarHeight =
@@ -180,7 +184,7 @@ fun SceneScope.NotificationScrollingStack(
// When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY,
// which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the
// entire height of the scrim is visible on screen.
- val scrimOffset = remember { mutableStateOf(0f) }
+ val scrimOffset = remember { Animatable(0f) }
// set the bounds to null when the scrim disappears
DisposableEffect(Unit) { onDispose { viewModel.onScrimBoundsChanged(null) } }
@@ -204,7 +208,7 @@ fun SceneScope.NotificationScrollingStack(
// expanded, reset scrim offset.
LaunchedEffect(stackHeight, scrimOffset) {
snapshotFlow { stackHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f }
- .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f }
+ .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.snapTo(0f) }
}
// if we receive scroll delta from NSSL, offset the scrim and placeholder accordingly.
@@ -214,7 +218,7 @@ fun SceneScope.NotificationScrollingStack(
val minOffset = minScrimOffset()
if (scrimOffset.value > minOffset) {
val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f)
- scrimOffset.value = (scrimOffset.value - delta).coerceAtLeast(minOffset)
+ scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset))
if (remainingDelta > 0f) {
scrollState.scrollBy(remainingDelta)
}
@@ -296,20 +300,30 @@ fun SceneScope.NotificationScrollingStack(
modifier =
Modifier.verticalNestedScrollToScene(
topBehavior = NestedScrollBehavior.EdgeWithPreview,
+ isExternalOverscrollGesture = { isCurrentGestureOverscroll.value }
)
.nestedScroll(
remember(
scrimOffset,
maxScrimTop,
minScrimTop,
+ isCurrentGestureOverscroll,
) {
NotificationScrimNestedScrollConnection(
scrimOffset = { scrimOffset.value },
- onScrimOffsetChanged = { scrimOffset.value = it },
+ snapScrimOffset = { value ->
+ coroutineScope.launch { scrimOffset.snapTo(value) }
+ },
+ animateScrimOffset = { value ->
+ coroutineScope.launch { scrimOffset.animateTo(value) }
+ },
minScrimOffset = minScrimOffset,
maxScrimOffset = 0f,
contentHeight = { stackHeight.value },
minVisibleScrimHeight = minVisibleScrimHeight,
+ isCurrentGestureOverscroll = {
+ isCurrentGestureOverscroll.value
+ },
)
}
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
new file mode 100644
index 000000000000..1c675e339941
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notifications.ui.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.ui.composable.OverlayShade
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class NotificationsShadeScene
+@Inject
+constructor(
+ viewModel: NotificationsShadeSceneViewModel,
+ private val overlayShadeViewModel: OverlayShadeViewModel,
+) : ComposableScene {
+
+ override val key = Scenes.NotificationsShade
+
+ override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+ viewModel.destinationScenes
+
+ @Composable
+ override fun SceneScope.Content(
+ modifier: Modifier,
+ ) {
+ OverlayShade(
+ viewModel = overlayShadeViewModel,
+ modifier = modifier,
+ horizontalArrangement = Arrangement.Start,
+ ) {
+ Text(
+ text = "Notifications list",
+ modifier = Modifier.padding(NotificationsShade.Dimensions.Padding)
+ )
+ }
+ }
+}
+
+object NotificationsShade {
+ object Dimensions {
+ val Padding = 16.dp
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index eedff89e7a45..5f84dd47a240 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -17,6 +17,11 @@
package com.android.systemui.qs.footer.ui.compose
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
@@ -87,10 +92,24 @@ import kotlinx.coroutines.launch
fun SceneScope.FooterActionsWithAnimatedVisibility(
viewModel: FooterActionsViewModel,
isCustomizing: Boolean,
+ customizingAnimationDuration: Int,
lifecycleOwner: LifecycleOwner,
modifier: Modifier = Modifier,
) {
- AnimatedVisibility(visible = !isCustomizing, modifier = modifier.fillMaxWidth()) {
+ AnimatedVisibility(
+ visible = !isCustomizing,
+ enter =
+ expandVertically(
+ animationSpec = tween(customizingAnimationDuration),
+ initialHeight = { 0 },
+ ) + fadeIn(tween(customizingAnimationDuration)),
+ exit =
+ shrinkVertically(
+ animationSpec = tween(customizingAnimationDuration),
+ targetHeight = { 0 },
+ ) + fadeOut(tween(customizingAnimationDuration)),
+ modifier = modifier.fillMaxWidth()
+ ) {
QuickSettingsTheme {
// This view has its own horizontal padding
// TODO(b/321716470) This should use a lifecycle tied to the scene.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index a87a8df290b7..46be6b898b8d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -162,7 +162,8 @@ private fun QuickSettingsContent(
modifier: Modifier = Modifier,
) {
val qsView by qsSceneAdapter.qsView.collectAsState(null)
- val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState()
+ val isCustomizing by
+ qsSceneAdapter.isCustomizerShowing.collectAsState(qsSceneAdapter.isCustomizerShowing.value)
QuickSettingsTheme {
val context = LocalContext.current
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index a32cc04df8db..4c0f2e11f76f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -19,12 +19,15 @@ package com.android.systemui.qs.ui.composable
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
@@ -178,6 +181,9 @@ private fun SceneScope.QuickSettingsScene(
}
) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+ val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
+ val customizingAnimationDuration by
+ viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsState()
val screenHeight = LocalRawScreenHeight.current
BackHandler(
@@ -217,6 +223,18 @@ private fun SceneScope.QuickSettingsScene(
val navBarBottomHeight =
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val density = LocalDensity.current
+ val bottomPadding by
+ animateDpAsState(
+ targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
+ animationSpec = tween(customizingAnimationDuration),
+ label = "animateQSSceneBottomPaddingAsState"
+ )
+ val topPadding by
+ animateDpAsState(
+ targetValue = if (isCustomizing) ShadeHeader.Dimensions.CollapsedHeight else 0.dp,
+ animationSpec = tween(customizingAnimationDuration),
+ label = "animateQSSceneTopPaddingAsState"
+ )
LaunchedEffect(navBarBottomHeight, density) {
with(density) {
@@ -236,17 +254,14 @@ private fun SceneScope.QuickSettingsScene(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.fillMaxSize()
- .then(
- if (isCustomizing) {
- Modifier.padding(top = 48.dp)
- } else {
- Modifier.padding(bottom = navBarBottomHeight)
- }
+ .padding(
+ top = topPadding.coerceAtLeast(0.dp),
+ bottom = bottomPadding.coerceAtLeast(0.dp)
)
) {
Box(modifier = Modifier.fillMaxSize().weight(1f)) {
val shadeHeaderAndQuickSettingsModifier =
- if (isCustomizing) {
+ if (isCustomizerShowing) {
Modifier.fillMaxHeight().align(Alignment.TopCenter)
} else {
Modifier.verticalNestedScrollToScene()
@@ -269,15 +284,22 @@ private fun SceneScope.QuickSettingsScene(
visible = !isCustomizing,
enter =
expandVertically(
- animationSpec = tween(100),
- initialHeight = { collapsedHeaderHeight },
- ) + fadeIn(tween(100)),
+ animationSpec = tween(customizingAnimationDuration),
+ expandFrom = Alignment.Top,
+ ) +
+ slideInVertically(
+ animationSpec = tween(customizingAnimationDuration),
+ ) +
+ fadeIn(tween(customizingAnimationDuration)),
exit =
shrinkVertically(
- animationSpec = tween(100),
- targetHeight = { collapsedHeaderHeight },
+ animationSpec = tween(customizingAnimationDuration),
shrinkTowards = Alignment.Top,
- ) + fadeOut(tween(100)),
+ ) +
+ slideOutVertically(
+ animationSpec = tween(customizingAnimationDuration),
+ ) +
+ fadeOut(tween(customizingAnimationDuration)),
) {
ExpandedShadeHeader(
viewModel = viewModel.shadeHeaderViewModel,
@@ -303,7 +325,7 @@ private fun SceneScope.QuickSettingsScene(
viewModel.qsSceneAdapter,
{ viewModel.qsSceneAdapter.qsHeight },
isSplitShade = false,
- modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
+ modifier = Modifier.sysuiResTag("expanded_qs_scroll_view")
)
MediaCarousel(
@@ -318,6 +340,7 @@ private fun SceneScope.QuickSettingsScene(
FooterActionsWithAnimatedVisibility(
viewModel = footerActionsViewModel,
isCustomizing = isCustomizing,
+ customizingAnimationDuration = customizingAnimationDuration,
lifecycleOwner = lifecycleOwner,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
new file mode 100644
index 000000000000..636c6c3b7d14
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.ui.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.ui.composable.OverlayShade
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class QuickSettingsShadeScene
+@Inject
+constructor(
+ viewModel: QuickSettingsShadeSceneViewModel,
+ private val overlayShadeViewModel: OverlayShadeViewModel,
+) : ComposableScene {
+
+ override val key = Scenes.QuickSettingsShade
+
+ override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+ viewModel.destinationScenes
+
+ @Composable
+ override fun SceneScope.Content(
+ modifier: Modifier,
+ ) {
+ OverlayShade(
+ viewModel = overlayShadeViewModel,
+ modifier = modifier,
+ horizontalArrangement = Arrangement.End,
+ ) {
+ Text(
+ text = "Quick settings grid",
+ modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding)
+ )
+ }
+ }
+}
+
+object QuickSettingsShade {
+ object Dimensions {
+ val Padding = 16.dp
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt
new file mode 100644
index 000000000000..dc5891915bfc
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/shared/SessionStorage.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.scene.session.shared
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/** Data store for [Session][com.android.systemui.scene.session.ui.composable.Session]. */
+class SessionStorage {
+ private var _storage by mutableStateOf(hashMapOf<String, StorageEntry>())
+
+ /**
+ * Data store containing all state retained for invocations of
+ * [rememberSession][com.android.systemui.scene.session.ui.composable.Session.rememberSession]
+ */
+ val storage: MutableMap<String, StorageEntry>
+ get() = _storage
+
+ /**
+ * Storage for an individual invocation of
+ * [rememberSession][com.android.systemui.scene.session.ui.composable.Session.rememberSession]
+ */
+ class StorageEntry(val keys: Array<out Any?>, var stored: Any?)
+
+ /** Clears the data store; any downstream usage within `@Composable`s will be recomposed. */
+ fun clear() {
+ _storage = hashMapOf()
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
new file mode 100644
index 000000000000..924aa540aa7f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/session/ui/composable/Session.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.scene.session.ui.composable
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.currentCompositeKeyHash
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.SaverScope
+import androidx.compose.runtime.saveable.mapSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import com.android.systemui.scene.session.shared.SessionStorage
+import com.android.systemui.util.kotlin.mapValuesNotNullTo
+
+/**
+ * An explicit storage for remembering composable state outside of the lifetime of a composition.
+ *
+ * Specifically, this allows easy conversion of standard
+ * [remember][androidx.compose.runtime.remember] invocations to ones that are preserved beyond the
+ * callsite's existence in the composition.
+ *
+ * ```kotlin
+ * @Composable
+ * fun Parent() {
+ * val session = remember { Session() }
+ * ...
+ * if (someCondition) {
+ * Child(session)
+ * }
+ * }
+ *
+ * @Composable
+ * fun Child(session: Session) {
+ * val state by session.rememberSession { mutableStateOf(0f) }
+ * ...
+ * }
+ * ```
+ */
+interface Session {
+ /**
+ * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had
+ * in the previous composition, otherwise produce and remember a new value by calling [init].
+ *
+ * @param inputs A set of inputs such that, when any of them have changed, will cause the state
+ * to reset and [init] to be rerun
+ * @param key An optional key to be used as a key for the saved value. If `null`, we use the one
+ * automatically generated by the Compose runtime which is unique for the every exact code
+ * location in the composition tree
+ * @param init A factory function to create the initial value of this state
+ * @see androidx.compose.runtime.remember
+ */
+ @Composable fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T
+}
+
+/** Returns a new [Session], optionally backed by the provided [SessionStorage]. */
+fun Session(storage: SessionStorage = SessionStorage()): Session = SessionImpl(storage)
+
+/**
+ * Remember the value returned by [init] if all [inputs] are equal (`==`) to the values they had in
+ * the previous composition, otherwise produce and remember a new value by calling [init].
+ *
+ * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
+ * reset and [init] to be rerun
+ * @param key An optional key to be used as a key for the saved value. If not provided we use the
+ * one automatically generated by the Compose runtime which is unique for the every exact code
+ * location in the composition tree
+ * @param init A factory function to create the initial value of this state
+ * @see androidx.compose.runtime.remember
+ */
+@Composable
+fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: () -> T): T =
+ rememberSession(key, inputs, init = init)
+
+/**
+ * An explicit storage for remembering composable state outside of the lifetime of a composition.
+ *
+ * Specifically, this allows easy conversion of standard [rememberSession] invocations to ones that
+ * are preserved beyond the callsite's existence in the composition.
+ *
+ * ```kotlin
+ * @Composable
+ * fun Parent() {
+ * val session = rememberSaveableSession()
+ * ...
+ * if (someCondition) {
+ * Child(session)
+ * }
+ * }
+ *
+ * @Composable
+ * fun Child(session: SaveableSession) {
+ * val state by session.rememberSaveableSession { mutableStateOf(0f) }
+ * ...
+ * }
+ * ```
+ */
+interface SaveableSession : Session {
+ /**
+ * Remember the value produced by [init].
+ *
+ * It behaves similarly to [rememberSession], but the stored value will survive the activity or
+ * process recreation using the saved instance state mechanism (for example it happens when the
+ * screen is rotated in the Android application).
+ *
+ * @param inputs A set of inputs such that, when any of them have changed, will cause the state
+ * to reset and [init] to be rerun
+ * @param saver The [Saver] object which defines how the state is saved and restored.
+ * @param key An optional key to be used as a key for the saved value. If not provided we use
+ * the automatically generated by the Compose runtime which is unique for the every exact code
+ * location in the composition tree
+ * @param init A factory function to create the initial value of this state
+ * @see rememberSaveable
+ */
+ @Composable
+ fun <T : Any> rememberSaveableSession(
+ vararg inputs: Any?,
+ saver: Saver<T, out Any>,
+ key: String?,
+ init: () -> T,
+ ): T
+}
+
+/**
+ * Returns a new [SaveableSession] that is preserved across configuration changes.
+ *
+ * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
+ * reset.
+ * @param key An optional key to be used as a key for the saved value. If not provided we use the
+ * automatically generated by the Compose runtime which is unique for the every exact code
+ * location in the composition tree.
+ */
+@Composable
+fun rememberSaveableSession(
+ vararg inputs: Any?,
+ key: String? = null,
+): SaveableSession =
+ rememberSaveable(inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }
+
+private class SessionImpl(
+ private val storage: SessionStorage = SessionStorage(),
+) : Session {
+ @Composable
+ override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T {
+ val storage = storage.storage
+ val compositeKey = currentCompositeKeyHash
+ // key is the one provided by the user or the one generated by the compose runtime
+ val finalKey =
+ if (!key.isNullOrEmpty()) {
+ key
+ } else {
+ compositeKey.toString(MAX_SUPPORTED_RADIX)
+ }
+ if (finalKey !in storage) {
+ val value = init()
+ SideEffect { storage[finalKey] = SessionStorage.StorageEntry(inputs, value) }
+ return value
+ }
+ val entry = storage[finalKey]!!
+ if (!inputs.contentEquals(entry.keys)) {
+ val value = init()
+ SideEffect { entry.stored = value }
+ return value
+ }
+ @Suppress("UNCHECKED_CAST") return entry.stored as T
+ }
+}
+
+private class SaveableSessionImpl(
+ saveableStorage: MutableMap<String, StorageEntry> = mutableMapOf(),
+ sessionStorage: SessionStorage = SessionStorage(),
+) : SaveableSession, Session by Session(sessionStorage) {
+
+ var saveableStorage: MutableMap<String, StorageEntry> by mutableStateOf(saveableStorage)
+
+ @Composable
+ override fun <T : Any> rememberSaveableSession(
+ vararg inputs: Any?,
+ saver: Saver<T, out Any>,
+ key: String?,
+ init: () -> T,
+ ): T {
+ val compositeKey = currentCompositeKeyHash
+ // key is the one provided by the user or the one generated by the compose runtime
+ val finalKey =
+ if (!key.isNullOrEmpty()) {
+ key
+ } else {
+ compositeKey.toString(MAX_SUPPORTED_RADIX)
+ }
+
+ @Suppress("UNCHECKED_CAST") (saver as Saver<T, Any>)
+
+ if (finalKey !in saveableStorage) {
+ val value = init()
+ SideEffect { saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver) }
+ return value
+ }
+ when (val entry = saveableStorage[finalKey]!!) {
+ is StorageEntry.Unrestored -> {
+ val value = saver.restore(entry.unrestored) ?: init()
+ SideEffect {
+ saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
+ }
+ return value
+ }
+ is StorageEntry.Restored<*> -> {
+ if (!inputs.contentEquals(entry.inputs)) {
+ val value = init()
+ SideEffect {
+ saveableStorage[finalKey] = StorageEntry.Restored(inputs, value, saver)
+ }
+ return value
+ }
+ @Suppress("UNCHECKED_CAST") return entry.stored as T
+ }
+ }
+ }
+
+ sealed class StorageEntry {
+ class Unrestored(val unrestored: Any) : StorageEntry()
+
+ class Restored<T>(val inputs: Array<out Any?>, var stored: T, val saver: Saver<T, Any>) :
+ StorageEntry() {
+ fun SaverScope.saveEntry() {
+ with(saver) { stored?.let { save(it) } }
+ }
+ }
+ }
+
+ object SessionSaver :
+ Saver<SaveableSessionImpl, Any> by mapSaver(
+ save = { sessionScope: SaveableSessionImpl ->
+ sessionScope.saveableStorage.mapValues { (k, v) ->
+ when (v) {
+ is StorageEntry.Unrestored -> v.unrestored
+ is StorageEntry.Restored<*> -> {
+ with(v) { saveEntry() }
+ }
+ }
+ }
+ },
+ restore = { savedMap: Map<String, Any?> ->
+ SaveableSessionImpl(
+ saveableStorage =
+ savedMap.mapValuesNotNullTo(mutableMapOf()) { (k, v) ->
+ v?.let { StorageEntry.Unrestored(v) }
+ }
+ )
+ }
+ )
+}
+
+private const val MAX_SUPPORTED_RADIX = 36
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index d7b10a961f91..7eaebc21355d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -5,7 +5,6 @@ import com.android.compose.animation.scene.transitions
import com.android.systemui.bouncer.ui.composable.Bouncer
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly
import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
@@ -39,20 +38,6 @@ val SceneContainerTransitions = transitions {
from(
Scenes.Gone,
to = Scenes.Shade,
- key = CollapseShadeInstantly,
- ) {
- goneToShadeTransition(durationScale = 0.0)
- }
- from(
- Scenes.Gone,
- to = Scenes.QuickSettings,
- key = CollapseShadeInstantly,
- ) {
- goneToQuickSettingsTransition(durationScale = 0.0)
- }
- from(
- Scenes.Gone,
- to = Scenes.Shade,
key = SlightlyFasterShadeCollapse,
) {
goneToShadeTransition(durationScale = 0.9)
@@ -64,13 +49,6 @@ val SceneContainerTransitions = transitions {
from(
Scenes.Lockscreen,
to = Scenes.Shade,
- key = CollapseShadeInstantly,
- ) {
- lockscreenToShadeTransition(durationScale = 0.0)
- }
- from(
- Scenes.Lockscreen,
- to = Scenes.Shade,
key = SlightlyFasterShadeCollapse,
) {
lockscreenToShadeTransition(durationScale = 0.9)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
index 05f8f4b38176..4b4b7ed33458 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
@@ -62,4 +62,10 @@ class SceneTransitionLayoutDataSource(
coroutineScope = coroutineScope,
)
}
+
+ override fun snapToScene(toScene: SceneKey) {
+ state.snapToScene(
+ scene = toScene,
+ )
+ }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index d3b3d1585f83..0bd38a1daf43 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -63,6 +63,7 @@ import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
@@ -100,6 +101,10 @@ object ShadeHeader {
val ColorScheme.shadeHeaderText: Color
get() = Color.White
}
+
+ object TestTags {
+ const val Root = "shade_header_root"
+ }
}
@Composable
@@ -131,7 +136,7 @@ fun SceneScope.CollapsedShadeHeader(
// This layout assumes it is globally positioned at (0, 0) and is the
// same size as the screen.
Layout(
- modifier = modifier,
+ modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
contents =
listOf(
{
@@ -261,7 +266,7 @@ fun SceneScope.ExpandedShadeHeader(
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsState()
- Box(modifier = modifier) {
+ Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) {
if (isPrivacyChipVisible) {
Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
PrivacyChip(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 72b80260c30d..ef5d4e1c30a6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -17,7 +17,9 @@
package com.android.systemui.shade.ui.composable
import android.view.ViewGroup
+import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.clipScrollableContainer
@@ -301,6 +303,9 @@ private fun SceneScope.SplitShade(
modifier: Modifier = Modifier,
) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+ val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
+ val customizingAnimationDuration by
+ viewModel.qsSceneAdapter.customizerAnimationDuration.collectAsState()
val lifecycleOwner = LocalLifecycleOwner.current
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
@@ -320,6 +325,12 @@ private fun SceneScope.SplitShade(
.collectAsState(0f)
val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
+ val bottomPadding by
+ animateDpAsState(
+ targetValue = if (isCustomizing) 0.dp else navBarBottomHeight,
+ animationSpec = tween(customizingAnimationDuration),
+ label = "animateQSSceneBottomPaddingAsState"
+ )
val density = LocalDensity.current
LaunchedEffect(navBarBottomHeight, density) {
with(density) {
@@ -390,16 +401,13 @@ private fun SceneScope.SplitShade(
)
Column(
verticalArrangement = Arrangement.Top,
- modifier =
- Modifier.fillMaxSize().thenIf(!isCustomizing) {
- Modifier.padding(bottom = navBarBottomHeight)
- },
+ modifier = Modifier.fillMaxSize().padding(bottom = bottomPadding),
) {
Column(
modifier =
Modifier.fillMaxSize()
.weight(1f)
- .thenIf(!isCustomizing) {
+ .thenIf(!isCustomizerShowing) {
Modifier.verticalNestedScrollToScene()
.verticalScroll(
quickSettingsScrollState,
@@ -432,6 +440,7 @@ private fun SceneScope.SplitShade(
FooterActionsWithAnimatedVisibility(
viewModel = footerActionsViewModel,
isCustomizing = isCustomizing,
+ customizingAnimationDuration = customizingAnimationDuration,
lifecycleOwner = lifecycleOwner,
modifier =
Modifier.align(Alignment.CenterHorizontally)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
index 00225fc3577a..0f6d51d3e3ea 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
@@ -54,6 +54,13 @@ constructor(
override fun VolumePanelComposeScope.Content(modifier: Modifier) {
val slice by viewModel.buttonSlice.collectAsState()
val label = stringResource(R.string.volume_panel_noise_control_title)
+ val isClickable = viewModel.isClickable(slice)
+ val onClick =
+ if (isClickable) {
+ { ancPopup.show(null) }
+ } else {
+ null
+ }
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -69,8 +76,9 @@ constructor(
}
.clip(RoundedCornerShape(28.dp)),
slice = slice,
+ isEnabled = onClick != null,
onWidthChanged = viewModel::onButtonSliceWidthChanged,
- onClick = { ancPopup.show(null) }
+ onClick = onClick,
)
Text(
modifier = Modifier.clearAndSetSemantics {},
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt
index 74af3ca19266..fc5d212a0be7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt
@@ -32,6 +32,7 @@ import com.android.systemui.res.R
fun SliceAndroidView(
slice: Slice?,
modifier: Modifier = Modifier,
+ isEnabled: Boolean = true,
onWidthChanged: ((Int) -> Unit)? = null,
onClick: (() -> Unit)? = null,
) {
@@ -40,7 +41,6 @@ fun SliceAndroidView(
factory = { context: Context ->
ClickableSliceView(
ContextThemeWrapper(context, R.style.Widget_SliceView_VolumePanel),
- onClick,
)
.apply {
mode = SliceView.MODE_LARGE
@@ -50,12 +50,14 @@ fun SliceAndroidView(
if (onWidthChanged != null) {
addOnLayoutChangeListener(OnWidthChangedLayoutListener(onWidthChanged))
}
- if (onClick != null) {
- setOnClickListener { onClick() }
- }
}
},
- update = { sliceView: SliceView -> sliceView.slice = slice }
+ update = { sliceView: ClickableSliceView ->
+ sliceView.slice = slice
+ sliceView.onClick = onClick
+ sliceView.isEnabled = isEnabled
+ sliceView.isClickable = isEnabled
+ }
)
}
@@ -86,10 +88,9 @@ class OnWidthChangedLayoutListener(private val widthChanged: (Int) -> Unit) :
* first.
*/
@SuppressLint("ViewConstructor") // only used in this class
-private class ClickableSliceView(
- context: Context,
- private val onClick: (() -> Unit)?,
-) : SliceView(context) {
+private class ClickableSliceView(context: Context) : SliceView(context) {
+
+ var onClick: (() -> Unit)? = null
init {
if (onClick != null) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt
index 874c0a299d2b..12debbc9c011 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/button/ui/composable/ToggleButtonComponent.kt
@@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.button.ui.composable
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -91,7 +92,8 @@ class ToggleButtonComponent(
},
onClick = { onCheckedChange(!viewModel.isActive) },
shape = RoundedCornerShape(28.dp),
- colors = colors
+ colors = colors,
+ contentPadding = PaddingValues(0.dp)
) {
Icon(modifier = Modifier.size(24.dp), icon = viewModel.icon)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
index 6f2ed8178801..ded63a107e70 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt
@@ -86,7 +86,10 @@ constructor(
modifier =
Modifier.fillMaxWidth().height(80.dp).semantics {
liveRegion = LiveRegionMode.Polite
- this.onClick(label = clickLabel) { false }
+ this.onClick(label = clickLabel) {
+ viewModel.onBarClick(null)
+ true
+ }
},
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(28.dp),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 9f5ab3c0e284..a46f4e5fef1a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -66,7 +66,9 @@ fun VolumeSlider(
// provide a not animated value to the a11y because it fails to announce the
// settled value when it changes rapidly.
- progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
+ if (state.isEnabled) {
+ progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
+ }
setProgress { targetValue ->
val targetDirection =
when {
diff --git a/packages/SystemUI/compose/scene/OWNERS b/packages/SystemUI/compose/scene/OWNERS
index 33a59c2bcab3..dac37eeb3e8c 100644
--- a/packages/SystemUI/compose/scene/OWNERS
+++ b/packages/SystemUI/compose/scene/OWNERS
@@ -2,12 +2,13 @@ set noparent
# Bug component: 1184816
+amiko@google.com
jdemeulenaere@google.com
omarmt@google.com
# SysUI Dr No's.
# Don't send reviews here.
-dsandler@android.com
cinek@google.com
+dsandler@android.com
juliacr@google.com
pixel@google.com \ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 6b289f3c66a3..b5e93131f828 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -47,8 +47,11 @@ internal fun CoroutineScope.animateToScene(
}
return when (transitionState) {
- is TransitionState.Idle -> animate(layoutState, target, transitionKey)
+ is TransitionState.Idle ->
+ animate(layoutState, target, transitionKey, isInitiatedByUserInput = false)
is TransitionState.Transition -> {
+ val isInitiatedByUserInput = transitionState.isInitiatedByUserInput
+
// A transition is currently running: first check whether `transition.toScene` or
// `transition.fromScene` is the same as our target scene, in which case the transition
// can be accelerated or reversed to end up in the target state.
@@ -68,8 +71,14 @@ internal fun CoroutineScope.animateToScene(
} else {
// The transition is in progress: start the canned animation at the same
// progress as it was in.
- // TODO(b/290184746): Also take the current velocity into account.
- animate(layoutState, target, transitionKey, startProgress = progress)
+ animate(
+ layoutState,
+ target,
+ transitionKey,
+ isInitiatedByUserInput,
+ initialProgress = progress,
+ initialVelocity = transitionState.progressVelocity,
+ )
}
} else if (transitionState.fromScene == target) {
// There is a transition from [target] to another scene: simply animate the same
@@ -83,19 +92,52 @@ internal fun CoroutineScope.animateToScene(
layoutState.finishTransition(transitionState, target)
null
} else {
- // TODO(b/290184746): Also take the current velocity into account.
animate(
layoutState,
target,
transitionKey,
- startProgress = progress,
+ isInitiatedByUserInput,
+ initialProgress = progress,
+ initialVelocity = transitionState.progressVelocity,
reversed = true,
)
}
} else {
// Generic interruption; the current transition is neither from or to [target].
- // TODO(b/290930950): Better handle interruptions here.
- animate(layoutState, target, transitionKey)
+ val interruptionResult =
+ layoutState.transitions.interruptionHandler.onInterruption(
+ transitionState,
+ target,
+ )
+ ?: DefaultInterruptionHandler.onInterruption(transitionState, target)
+
+ val animateFrom = interruptionResult.animateFrom
+ if (
+ animateFrom != transitionState.toScene &&
+ animateFrom != transitionState.fromScene
+ ) {
+ error(
+ "InterruptionResult.animateFrom must be either the fromScene " +
+ "(${transitionState.fromScene.debugName}) or the toScene " +
+ "(${transitionState.toScene.debugName}) of the interrupted transition."
+ )
+ }
+
+ // If we were A => B and that we are now animating A => C, add a transition B => A
+ // to the list of transitions so that B "disappears back to A".
+ val chain = interruptionResult.chain
+ if (chain && animateFrom != transitionState.currentScene) {
+ animateToScene(layoutState, animateFrom, transitionKey = null)
+ }
+
+ animate(
+ layoutState,
+ target,
+ transitionKey,
+ isInitiatedByUserInput,
+ fromScene = animateFrom,
+ chain = chain,
+ )
}
}
}
@@ -103,32 +145,31 @@ internal fun CoroutineScope.animateToScene(
private fun CoroutineScope.animate(
layoutState: BaseSceneTransitionLayoutState,
- target: SceneKey,
+ targetScene: SceneKey,
transitionKey: TransitionKey?,
- startProgress: Float = 0f,
+ isInitiatedByUserInput: Boolean,
+ initialProgress: Float = 0f,
+ initialVelocity: Float = 0f,
reversed: Boolean = false,
+ fromScene: SceneKey = layoutState.transitionState.currentScene,
+ chain: Boolean = true,
): TransitionState.Transition {
- val fromScene = layoutState.transitionState.currentScene
- val isUserInput =
- (layoutState.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput
- ?: false
-
val targetProgress = if (reversed) 0f else 1f
val transition =
if (reversed) {
OneOffTransition(
- fromScene = target,
+ fromScene = targetScene,
toScene = fromScene,
- currentScene = target,
- isInitiatedByUserInput = isUserInput,
+ currentScene = targetScene,
+ isInitiatedByUserInput = isInitiatedByUserInput,
isUserInputOngoing = false,
)
} else {
OneOffTransition(
fromScene = fromScene,
- toScene = target,
- currentScene = target,
- isInitiatedByUserInput = isUserInput,
+ toScene = targetScene,
+ currentScene = targetScene,
+ isInitiatedByUserInput = isInitiatedByUserInput,
isUserInputOngoing = false,
)
}
@@ -136,7 +177,7 @@ private fun CoroutineScope.animate(
// Change the current layout state to start this new transition. This will compute the
// TransformationSpec associated to this transition, which we need to initialize the Animatable
// that will actually animate it.
- layoutState.startTransition(transition, transitionKey)
+ layoutState.startTransition(transition, transitionKey, chain)
// The transition now contains the transformation spec that we should use to instantiate the
// Animatable.
@@ -144,19 +185,19 @@ private fun CoroutineScope.animate(
val visibilityThreshold =
(animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
val animatable =
- Animatable(startProgress, visibilityThreshold = visibilityThreshold).also {
+ Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
transition.animatable = it
}
// Animate the progress to its target value.
transition.job =
- launch { animatable.animateTo(targetProgress, animationSpec) }
+ launch { animatable.animateTo(targetProgress, animationSpec, initialVelocity) }
.apply {
invokeOnCompletion {
// Settle the state to Idle(target). Note that this will do nothing if this
// transition was replaced/interrupted by another one, and this also runs if
// this coroutine is cancelled, i.e. if [this] coroutine scope is cancelled.
- layoutState.finishTransition(transition, target)
+ layoutState.finishTransition(transition, targetScene)
}
}
@@ -185,6 +226,9 @@ private class OneOffTransition(
override val progress: Float
get() = animatable.value
+ override val progressVelocity: Float
+ get() = animatable.velocity
+
override fun finish(): Job = job
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f78ed2fdcaf6..67589909ac03 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -579,6 +579,18 @@ private class SwipeTransition(
return offset / distance
}
+ override val progressVelocity: Float
+ get() {
+ val animatable = offsetAnimation?.animatable ?: return 0f
+ val distance = distance()
+ if (distance == DistanceUnspecified) {
+ return 0f
+ }
+
+ val velocityInDistanceUnit = animatable.velocity
+ return velocityInDistanceUnit / distance.absoluteValue
+ }
+
override val isInitiatedByUserInput = true
override var bouncingScene: SceneKey? = null
@@ -865,6 +877,7 @@ internal class NestedScrollHandlerImpl(
private val orientation: Orientation,
private val topOrLeftBehavior: NestedScrollBehavior,
private val bottomOrRightBehavior: NestedScrollBehavior,
+ private val isExternalOverscrollGesture: () -> Boolean,
) {
private val layoutState = layoutImpl.state
private val draggableHandler = layoutImpl.draggableHandler(orientation)
@@ -920,7 +933,8 @@ internal class NestedScrollHandlerImpl(
return PriorityNestedScrollConnection(
orientation = orientation,
canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
- canChangeScene = offsetBeforeStart == 0f
+ canChangeScene =
+ if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
val canInterceptSwipeTransition =
canChangeScene &&
@@ -950,7 +964,8 @@ internal class NestedScrollHandlerImpl(
else -> return@PriorityNestedScrollConnection false
}
- val isZeroOffset = offsetBeforeStart == 0f
+ val isZeroOffset =
+ if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
val canStart =
when (behavior) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index ca643231e874..20742ee77fff 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -329,10 +329,9 @@ private fun elementTransition(
if (transition == null && previousTransition != null) {
// The transition was just finished.
- element.sceneStates.values.forEach { sceneState ->
- sceneState.offsetInterruptionDelta = Offset.Zero
- sceneState.scaleInterruptionDelta = Scale.Zero
- sceneState.alphaInterruptionDelta = 0f
+ element.sceneStates.values.forEach {
+ it.clearValuesBeforeInterruption()
+ it.clearInterruptionDeltas()
}
}
@@ -375,12 +374,22 @@ private fun prepareInterruption(element: Element) {
sceneState.scaleBeforeInterruption = lastScale
sceneState.alphaBeforeInterruption = lastAlpha
- sceneState.offsetInterruptionDelta = Offset.Zero
- sceneState.scaleInterruptionDelta = Scale.Zero
- sceneState.alphaInterruptionDelta = 0f
+ sceneState.clearInterruptionDeltas()
}
}
+private fun Element.SceneState.clearInterruptionDeltas() {
+ offsetInterruptionDelta = Offset.Zero
+ scaleInterruptionDelta = Scale.Zero
+ alphaInterruptionDelta = 0f
+}
+
+private fun Element.SceneState.clearValuesBeforeInterruption() {
+ offsetBeforeInterruption = Offset.Unspecified
+ scaleBeforeInterruption = Scale.Unspecified
+ alphaBeforeInterruption = Element.AlphaUnspecified
+}
+
/**
* Compute what [value] should be if we take the
* [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into
@@ -744,7 +753,11 @@ private fun ApproachMeasureScope.place(
// No need to place the element in this scene if we don't want to draw it anyways.
if (!shouldPlaceElement(layoutImpl, scene, element, transition)) {
sceneState.lastOffset = Offset.Unspecified
- sceneState.offsetBeforeInterruption = Offset.Unspecified
+ sceneState.lastScale = Scale.Unspecified
+ sceneState.lastAlpha = Element.AlphaUnspecified
+
+ sceneState.clearValuesBeforeInterruption()
+ sceneState.clearInterruptionDeltas()
return
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
new file mode 100644
index 000000000000..54c64fd721ec
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.animation.scene
+
+/**
+ * A handler to specify how a transition should be interrupted.
+ *
+ * @see DefaultInterruptionHandler
+ * @see SceneTransitionsBuilder.interruptionHandler
+ */
+interface InterruptionHandler {
+ /**
+ * This function is called when [interrupted] is interrupted: it is currently animating between
+ * [interrupted.fromScene] and [interrupted.toScene], and we will now animate to
+ * [newTargetScene].
+ *
+ * If this returns `null`, then the [default behavior][DefaultInterruptionHandler] will be used:
+ * we will animate from [interrupted.currentScene] and chaining will be enabled (see
+ * [InterruptionResult] for more information about chaining).
+ *
+ * @see InterruptionResult
+ */
+ fun onInterruption(
+ interrupted: TransitionState.Transition,
+ newTargetScene: SceneKey,
+ ): InterruptionResult?
+}
+
+/**
+ * The result of an interruption that specifies how we should handle a transition A => B now that we
+ * have to animate to C.
+ *
+ * For instance, if the interrupted transition was A => B and currentScene = B:
+ * - animateFrom = B && chain = true => there will be 2 transitions running in parallel, A => B and
+ * B => C.
+ * - animateFrom = A && chain = true => there will be 2 transitions running in parallel, B => A and
+ * A => C.
+ * - animateFrom = B && chain = false => there will be 1 transition running, B => C.
+ * - animateFrom = A && chain = false => there will be 1 transition running, A => C.
+ */
+class InterruptionResult(
+ /**
+ * The scene we should animate from when transitioning to C.
+ *
+ * Important: This **must** be either [TransitionState.Transition.fromScene] or
+ * [TransitionState.Transition.toScene] of the transition that was interrupted.
+ */
+ val animateFrom: SceneKey,
+
+ /**
+ * Whether chaining is enabled, i.e. if the new transition to C should run in parallel with the
+ * previous one(s) or if it should be the only remaining transition that is running.
+ */
+ val chain: Boolean = true,
+)
+
+/**
+ * The default interruption handler: we animate from [TransitionState.Transition.currentScene] and
+ * chaining is enabled.
+ */
+object DefaultInterruptionHandler : InterruptionHandler {
+ override fun onInterruption(
+ interrupted: TransitionState.Transition,
+ newTargetScene: SceneKey,
+ ): InterruptionResult {
+ return InterruptionResult(
+ animateFrom = interrupted.currentScene,
+ chain = true,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index 5a2f85ad163c..1fa6b3f7d6c0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -75,6 +75,7 @@ internal fun Modifier.nestedScrollToScene(
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) =
this then
NestedScrollToSceneElement(
@@ -82,6 +83,7 @@ internal fun Modifier.nestedScrollToScene(
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
private data class NestedScrollToSceneElement(
@@ -89,6 +91,7 @@ private data class NestedScrollToSceneElement(
private val orientation: Orientation,
private val topOrLeftBehavior: NestedScrollBehavior,
private val bottomOrRightBehavior: NestedScrollBehavior,
+ private val isExternalOverscrollGesture: () -> Boolean,
) : ModifierNodeElement<NestedScrollToSceneNode>() {
override fun create() =
NestedScrollToSceneNode(
@@ -96,6 +99,7 @@ private data class NestedScrollToSceneElement(
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
override fun update(node: NestedScrollToSceneNode) {
@@ -104,6 +108,7 @@ private data class NestedScrollToSceneElement(
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
}
@@ -121,6 +126,7 @@ private class NestedScrollToSceneNode(
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) : DelegatingNode() {
private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
scenePriorityNestedScrollConnection(
@@ -128,6 +134,7 @@ private class NestedScrollToSceneNode(
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
private var nestedScrollNode: DelegatableNode =
@@ -150,6 +157,7 @@ private class NestedScrollToSceneNode(
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) {
// Clean up the old nested scroll connection
priorityNestedScrollConnection.reset()
@@ -162,6 +170,7 @@ private class NestedScrollToSceneNode(
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
nestedScrollNode =
nestedScrollModifierNode(
@@ -177,11 +186,13 @@ private fun scenePriorityNestedScrollConnection(
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) =
NestedScrollHandlerImpl(
layoutImpl = layoutImpl,
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
.connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 339868c9fbc9..6fef33c797d9 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -141,23 +141,27 @@ internal class SceneScopeImpl(
override fun Modifier.horizontalNestedScrollToScene(
leftBehavior: NestedScrollBehavior,
rightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
): Modifier =
nestedScrollToScene(
layoutImpl = layoutImpl,
orientation = Orientation.Horizontal,
topOrLeftBehavior = leftBehavior,
bottomOrRightBehavior = rightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
override fun Modifier.verticalNestedScrollToScene(
topBehavior: NestedScrollBehavior,
- bottomBehavior: NestedScrollBehavior
+ bottomBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
): Modifier =
nestedScrollToScene(
layoutImpl = layoutImpl,
orientation = Orientation.Vertical,
topOrLeftBehavior = topBehavior,
bottomOrRightBehavior = bottomBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
override fun Modifier.noResizeDuringTransitions(): Modifier {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index c7c874c1185d..11e711ace971 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -250,6 +250,7 @@ interface BaseSceneScope : ElementStateScope {
fun Modifier.horizontalNestedScrollToScene(
leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+ isExternalOverscrollGesture: () -> Boolean = { false },
): Modifier
/**
@@ -262,6 +263,7 @@ interface BaseSceneScope : ElementStateScope {
fun Modifier.verticalNestedScrollToScene(
topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+ isExternalOverscrollGesture: () -> Boolean = { false },
): Modifier
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 5fda77a3e0ae..4e3a03293796 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -117,6 +117,9 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState
coroutineScope: CoroutineScope,
transitionKey: TransitionKey? = null,
): TransitionState.Transition?
+
+ /** Immediately snap to the given [scene]. */
+ fun snapToScene(scene: SceneKey)
}
/**
@@ -227,6 +230,9 @@ sealed interface TransitionState {
*/
abstract val progress: Float
+ /** The current velocity of [progress], in progress units. */
+ abstract val progressVelocity: Float
+
/** Whether the transition was triggered by user input rather than being programmatic. */
abstract val isInitiatedByUserInput: Boolean
@@ -422,13 +428,18 @@ internal abstract class BaseSceneTransitionLayoutState(
}
/**
- * Start a new [transition], instantly interrupting any ongoing transition if there was one.
+ * Start a new [transition].
+ *
+ * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and
+ * will run in parallel to the current transitions. If [chain] is `false`, then the list of
+ * [currentTransitions] will be cleared and [transition] will be the only running transition.
*
* Important: you *must* call [finishTransition] once the transition is finished.
*/
internal fun startTransition(
transition: TransitionState.Transition,
transitionKey: TransitionKey?,
+ chain: Boolean = true,
) {
// Compute the [TransformationSpec] when the transition starts.
val fromScene = transition.fromScene
@@ -471,26 +482,10 @@ internal abstract class BaseSceneTransitionLayoutState(
finishTransition(currentState, currentState.currentScene)
}
- // Check that we don't have too many concurrent transitions.
- if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) {
- Log.wtf(
- TAG,
- buildString {
- appendLine("Potential leak detected in SceneTransitionLayoutState!")
- appendLine(
- " Some transition(s) never called STLState.finishTransition()."
- )
- appendLine(" Transitions (size=${transitionStates.size}):")
- transitionStates.fastForEach { state ->
- val transition = state as TransitionState.Transition
- val from = transition.fromScene
- val to = transition.toScene
- val indicator =
- if (finishedTransitions.contains(transition)) "x" else " "
- appendLine(" [$indicator] $from => $to ($transition)")
- }
- }
- )
+ val tooManyTransitions = transitionStates.size >= MAX_CONCURRENT_TRANSITIONS
+ val clearCurrentTransitions = !chain || tooManyTransitions
+ if (clearCurrentTransitions) {
+ if (tooManyTransitions) logTooManyTransitions()
// Force finish all transitions.
while (currentTransitions.isNotEmpty()) {
@@ -511,6 +506,24 @@ internal abstract class BaseSceneTransitionLayoutState(
}
}
+ private fun logTooManyTransitions() {
+ Log.wtf(
+ TAG,
+ buildString {
+ appendLine("Potential leak detected in SceneTransitionLayoutState!")
+ appendLine(" Some transition(s) never called STLState.finishTransition().")
+ appendLine(" Transitions (size=${transitionStates.size}):")
+ transitionStates.fastForEach { state ->
+ val transition = state as TransitionState.Transition
+ val from = transition.fromScene
+ val to = transition.toScene
+ val indicator = if (finishedTransitions.contains(transition)) "x" else " "
+ appendLine(" [$indicator] $from => $to ($transition)")
+ }
+ }
+ )
+ }
+
private fun cancelActiveTransitionLinks() {
for ((link, linkedTransition) in activeTransitionLinks) {
link.target.finishTransition(linkedTransition, linkedTransition.currentScene)
@@ -735,6 +748,17 @@ internal class MutableSceneTransitionLayoutStateImpl(
override fun CoroutineScope.onChangeScene(scene: SceneKey) {
setTargetScene(scene, coroutineScope = this)
}
+
+ override fun snapToScene(scene: SceneKey) {
+ // Force finish all transitions.
+ while (currentTransitions.isNotEmpty()) {
+ val transition = transitionStates[0] as TransitionState.Transition
+ finishTransition(transition, transition.currentScene)
+ }
+
+ check(transitionStates.size == 1)
+ transitionStates[0] = TransitionState.Idle(scene)
+ }
}
private const val TAG = "SceneTransitionLayoutState"
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index b46614397ff4..0f6a1d276578 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -44,6 +44,7 @@ internal constructor(
internal val defaultSwipeSpec: SpringSpec<Float>,
internal val transitionSpecs: List<TransitionSpecImpl>,
internal val overscrollSpecs: List<OverscrollSpecImpl>,
+ internal val interruptionHandler: InterruptionHandler,
) {
private val transitionCache =
mutableMapOf<
@@ -145,6 +146,7 @@ internal constructor(
defaultSwipeSpec = DefaultSwipeSpec,
transitionSpecs = emptyList(),
overscrollSpecs = emptyList(),
+ interruptionHandler = DefaultInterruptionHandler,
)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 6bc397e86cfa..a4682ff2a885 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -40,6 +40,12 @@ interface SceneTransitionsBuilder {
var defaultSwipeSpec: SpringSpec<Float>
/**
+ * The [InterruptionHandler] used when transitions are interrupted. Defaults to
+ * [DefaultInterruptionHandler].
+ */
+ var interruptionHandler: InterruptionHandler
+
+ /**
* Define the default animation to be played when transitioning [to] the specified scene, from
* any scene. For the animation specification to apply only when transitioning between two
* specific scenes, use [from] instead.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index 1c9080fa085d..802ab1f2eebb 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -47,12 +47,14 @@ internal fun transitionsImpl(
return SceneTransitions(
impl.defaultSwipeSpec,
impl.transitionSpecs,
- impl.transitionOverscrollSpecs
+ impl.transitionOverscrollSpecs,
+ impl.interruptionHandler,
)
}
private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec
+ override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler
val transitionSpecs = mutableListOf<TransitionSpecImpl>()
val transitionOverscrollSpecs = mutableListOf<OverscrollSpecImpl>()
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
index 73393a1ab0cf..79f126d24561 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt
@@ -45,5 +45,8 @@ internal class LinkedTransition(
override val progress: Float
get() = originalTransition.progress
+ override val progressVelocity: Float
+ get() = originalTransition.progressVelocity
+
override fun finish(): Job = originalTransition.finish()
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 1fd1bf4a56e2..8625482d5f71 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -32,12 +32,11 @@ import com.android.compose.animation.scene.NestedScrollBehavior.EdgeWithPreview
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
-import com.android.compose.animation.scene.TransitionState.Idle
import com.android.compose.animation.scene.TransitionState.Transition
+import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.MonotonicClockTestScope
import com.android.compose.test.runMonotonicClockTest
import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
@@ -103,12 +102,16 @@ class DraggableHandlerTest {
val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
- fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) =
+ fun nestedScrollConnection(
+ nestedScrollBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: Boolean = false
+ ) =
NestedScrollHandlerImpl(
layoutImpl = layoutImpl,
orientation = draggableHandler.orientation,
topOrLeftBehavior = nestedScrollBehavior,
bottomOrRightBehavior = nestedScrollBehavior,
+ isExternalOverscrollGesture = { isExternalOverscrollGesture }
)
.connection
@@ -145,10 +148,8 @@ class DraggableHandlerTest {
}
fun assertIdle(currentScene: SceneKey) {
- assertThat(transitionState).isInstanceOf(Idle::class.java)
- assertWithMessage("currentScene does not match")
- .that(transitionState.currentScene)
- .isEqualTo(currentScene)
+ assertThat(transitionState).isIdle()
+ assertThat(transitionState).hasCurrentScene(currentScene)
}
fun assertTransition(
@@ -158,34 +159,12 @@ class DraggableHandlerTest {
progress: Float? = null,
isUserInputOngoing: Boolean? = null
) {
- assertThat(transitionState).isInstanceOf(Transition::class.java)
- val transition = transitionState as Transition
-
- if (currentScene != null)
- assertWithMessage("currentScene does not match")
- .that(transition.currentScene)
- .isEqualTo(currentScene)
-
- if (fromScene != null)
- assertWithMessage("fromScene does not match")
- .that(transition.fromScene)
- .isEqualTo(fromScene)
-
- if (toScene != null)
- assertWithMessage("toScene does not match")
- .that(transition.toScene)
- .isEqualTo(toScene)
-
- if (progress != null)
- assertWithMessage("progress does not match")
- .that(transition.progress)
- .isWithin(0f) // returns true when comparing 0.0f with -0.0f
- .of(progress)
-
- if (isUserInputOngoing != null)
- assertWithMessage("isUserInputOngoing does not match")
- .that(transition.isUserInputOngoing)
- .isEqualTo(isUserInputOngoing)
+ val transition = assertThat(transitionState).isTransition()
+ currentScene?.let { assertThat(transition).hasCurrentScene(it) }
+ fromScene?.let { assertThat(transition).hasFromScene(it) }
+ toScene?.let { assertThat(transition).hasToScene(it) }
+ progress?.let { assertThat(transition).hasProgress(it) }
+ isUserInputOngoing?.let { assertThat(transition).hasIsUserInputOngoing(it) }
}
fun onDragStarted(
@@ -801,6 +780,26 @@ class DraggableHandlerTest {
}
@Test
+ fun flingAfterScrollStartedByExternalOverscrollGesture() = runGestureTest {
+ val nestedScroll =
+ nestedScrollConnection(
+ nestedScrollBehavior = EdgeWithPreview,
+ isExternalOverscrollGesture = true
+ )
+
+ // scroll not consumed in child
+ nestedScroll.scroll(
+ available = downOffset(fractionOfScreen = 0.1f),
+ )
+
+ // scroll offsetY10 is all available for parents
+ nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
+ assertTransition(SceneA)
+
+ nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
+ }
+
+ @Test
fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
nestedScroll.preFling(available = Velocity(0f, velocityThreshold))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 92e1b2cd030c..e19dc965a394 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -20,7 +20,6 @@ import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
@@ -43,7 +42,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.approachLayout
@@ -64,6 +62,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -78,7 +77,6 @@ class ElementTest {
@get:Rule val rule = createComposeRule()
@Composable
- @OptIn(ExperimentalComposeUiApi::class)
private fun SceneScope.Element(
key: ElementKey,
size: Dp,
@@ -496,7 +494,6 @@ class ElementTest {
}
@Test
- @OptIn(ExperimentalFoundationApi::class)
fun elementModifierNodeIsRecycledInLazyLayouts() = runTest {
val nPages = 2
val pagerState = PagerState(currentPage = 0) { nPages }
@@ -654,8 +651,7 @@ class ElementTest {
}
}
- assertThat(state.currentTransition).isNull()
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(state.transitionState).isIdle()
// Swipe by half of verticalSwipeDistance.
rule.onRoot().performTouchInput {
@@ -691,9 +687,9 @@ class ElementTest {
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
fooElement.assertTopPositionInRootIsEqualTo(0.dp)
- val transition = state.currentTransition
+ val transition = assertThat(state.transitionState).isTransition()
assertThat(transition).isNotNull()
- assertThat(transition!!.progress).isEqualTo(0.5f)
+ assertThat(transition).hasProgress(0.5f)
assertThat(animatedFloat).isEqualTo(50f)
rule.onRoot().performTouchInput {
@@ -702,8 +698,8 @@ class ElementTest {
}
// Scroll 150% (Scene B overscroll by 50%)
- assertThat(transition.progress).isEqualTo(1.5f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(transition).hasProgress(1.5f)
+ assertThat(transition).hasOverscrollSpec()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
// animatedFloat cannot overflow (canOverflow = false)
assertThat(animatedFloat).isEqualTo(100f)
@@ -714,8 +710,8 @@ class ElementTest {
}
// Scroll 250% (Scene B overscroll by 150%)
- assertThat(transition.progress).isEqualTo(2.5f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(transition).hasProgress(2.5f)
+ assertThat(transition).hasOverscrollSpec()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
assertThat(animatedFloat).isEqualTo(100f)
}
@@ -766,8 +762,7 @@ class ElementTest {
}
}
- assertThat(state.currentTransition).isNull()
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(state.transitionState).isIdle()
val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
fooElement.assertTopPositionInRootIsEqualTo(0.dp)
@@ -779,10 +774,9 @@ class ElementTest {
moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
}
- val transition = state.currentTransition
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
- assertThat(transition).isNotNull()
- assertThat(transition!!.progress).isEqualTo(-0.5f)
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasOverscrollSpec()
+ assertThat(transition).hasProgress(-0.5f)
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
rule.onRoot().performTouchInput {
@@ -791,8 +785,8 @@ class ElementTest {
}
// Scroll 150% (Scene B overscroll by 50%)
- assertThat(transition.progress).isEqualTo(-1.5f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(transition).hasProgress(-1.5f)
+ assertThat(transition).hasOverscrollSpec()
fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
}
@@ -825,13 +819,12 @@ class ElementTest {
moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
}
- val transition = state.currentTransition
- assertThat(transition).isNotNull()
+ val transition = assertThat(state.transitionState).isTransition()
assertThat(animatedFloat).isEqualTo(100f)
// Scroll 150% (100% scroll + 50% overscroll)
- assertThat(transition!!.progress).isEqualTo(1.5f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(transition).hasProgress(1.5f)
+ assertThat(transition).hasOverscrollSpec()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f)
assertThat(animatedFloat).isEqualTo(100f)
@@ -841,8 +834,8 @@ class ElementTest {
}
// Scroll 250% (100% scroll + 150% overscroll)
- assertThat(transition.progress).isEqualTo(2.5f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(transition).hasProgress(2.5f)
+ assertThat(transition).hasOverscrollSpec()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
assertThat(animatedFloat).isEqualTo(100f)
}
@@ -882,13 +875,11 @@ class ElementTest {
moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
}
- val transition = state.currentTransition
- assertThat(transition).isNotNull()
- transition as TransitionState.HasOverscrollProperties
+ val transition = assertThat(state.transitionState).isTransition()
// Scroll 150% (100% scroll + 50% overscroll)
- assertThat(transition.progress).isEqualTo(1.5f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
+ assertThat(transition).hasProgress(1.5f)
+ assertThat(transition).hasOverscrollSpec()
fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f))
assertThat(animatedFloat).isEqualTo(100f)
@@ -900,8 +891,8 @@ class ElementTest {
rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f }
assertThat(transition.progress).isLessThan(1f)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
- assertThat(transition.bouncingScene).isEqualTo(transition.toScene)
+ assertThat(transition).hasOverscrollSpec()
+ assertThat(transition).hasBouncingScene(transition.toScene)
assertThat(animatedFloat).isEqualTo(100f)
}
@@ -980,13 +971,13 @@ class ElementTest {
val transitions = state.currentTransitions
assertThat(transitions).hasSize(2)
- assertThat(transitions[0].fromScene).isEqualTo(SceneA)
- assertThat(transitions[0].toScene).isEqualTo(SceneB)
- assertThat(transitions[0].progress).isEqualTo(0f)
+ assertThat(transitions[0]).hasFromScene(SceneA)
+ assertThat(transitions[0]).hasToScene(SceneB)
+ assertThat(transitions[0]).hasProgress(0f)
- assertThat(transitions[1].fromScene).isEqualTo(SceneB)
- assertThat(transitions[1].toScene).isEqualTo(SceneC)
- assertThat(transitions[1].progress).isEqualTo(0f)
+ assertThat(transitions[1]).hasFromScene(SceneB)
+ assertThat(transitions[1]).hasToScene(SceneC)
+ assertThat(transitions[1]).hasProgress(0f)
// First frame: both are at x = 0dp. For the whole transition, Foo is at y = 0dp and Bar is
// at y = layoutSize - elementSoze = 100dp.
@@ -1049,24 +1040,30 @@ class ElementTest {
Box(modifier.element(TestElements.Foo).size(fooSize))
}
+ lateinit var layoutImpl: SceneTransitionLayoutImpl
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(
+ state,
+ Modifier.size(layoutSize),
+ onLayoutImpl = { layoutImpl = it },
+ ) {
// In scene A, Foo is aligned at the TopStart.
scene(SceneA) {
Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
}
+ // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming
+ // from B. We put it before (below) scene B so that we can check that interruptions
+ // values and deltas are properly cleared once all transitions are done.
+ scene(SceneC) {
+ Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) }
+ }
+
// In scene B, Foo is aligned at the TopEnd, so it moves horizontally when coming
// from A.
scene(SceneB) {
Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) }
}
-
- // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming
- // from B.
- scene(SceneC) {
- Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) }
- }
}
}
@@ -1115,7 +1112,7 @@ class ElementTest {
// Interruption progress is at 100% and bToC is at 0%, so Foo should be at the same offset
// as right before the interruption.
rule
- .onNode(isElement(TestElements.Foo, SceneC))
+ .onNode(isElement(TestElements.Foo, SceneB))
.assertPositionInRootIsEqualTo(offsetInAToB.x, offsetInAToB.y)
// Move the transition forward at 30% and set the interruption progress to 50%.
@@ -1130,7 +1127,7 @@ class ElementTest {
)
rule.waitForIdle()
rule
- .onNode(isElement(TestElements.Foo, SceneC))
+ .onNode(isElement(TestElements.Foo, SceneB))
.assertPositionInRootIsEqualTo(
offsetInBToCWithInterruption.x,
offsetInBToCWithInterruption.y,
@@ -1140,7 +1137,24 @@ class ElementTest {
bToCProgress = 1f
interruptionProgress = 0f
rule
- .onNode(isElement(TestElements.Foo, SceneC))
+ .onNode(isElement(TestElements.Foo, SceneB))
.assertPositionInRootIsEqualTo(offsetInC.x, offsetInC.y)
+
+ // Manually finish the transition.
+ state.finishTransition(aToB, SceneB)
+ state.finishTransition(bToC, SceneC)
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+
+ // The interruption values should be unspecified and deltas should be set to zero.
+ val foo = layoutImpl.elements.getValue(TestElements.Foo)
+ assertThat(foo.sceneStates.keys).containsExactly(SceneC)
+ val stateInC = foo.sceneStates.getValue(SceneC)
+ assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified)
+ assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified)
+ assertThat(stateInC.alphaBeforeInterruption).isEqualTo(Element.AlphaUnspecified)
+ assertThat(stateInC.offsetInterruptionDelta).isEqualTo(Offset.Zero)
+ assertThat(stateInC.scaleInterruptionDelta).isEqualTo(Scale.Zero)
+ assertThat(stateInC.alphaInterruptionDelta).isEqualTo(0f)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
new file mode 100644
index 000000000000..85d4165b4bf6
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.animation.scene
+
+import androidx.compose.animation.core.tween
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
+import com.android.compose.test.runMonotonicClockTest
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class InterruptionHandlerTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun default() = runMonotonicClockTest {
+ val state =
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ transitions { /* default interruption handler */},
+ )
+
+ state.setTargetScene(SceneB, coroutineScope = this)
+ state.setTargetScene(SceneC, coroutineScope = this)
+
+ assertThat(state.currentTransitions)
+ .comparingElementsUsing(FromToCurrentTriple)
+ .containsExactly(
+ // A to B.
+ Triple(SceneA, SceneB, SceneB),
+
+ // B to C.
+ Triple(SceneB, SceneC, SceneC),
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun chainingDisabled() = runMonotonicClockTest {
+ val state =
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ transitions {
+ // Handler that animates from currentScene (default) but disables chaining.
+ interruptionHandler =
+ object : InterruptionHandler {
+ override fun onInterruption(
+ interrupted: TransitionState.Transition,
+ newTargetScene: SceneKey
+ ): InterruptionResult {
+ return InterruptionResult(
+ animateFrom = interrupted.currentScene,
+ chain = false,
+ )
+ }
+ }
+ },
+ )
+
+ state.setTargetScene(SceneB, coroutineScope = this)
+ state.setTargetScene(SceneC, coroutineScope = this)
+
+ assertThat(state.currentTransitions)
+ .comparingElementsUsing(FromToCurrentTriple)
+ .containsExactly(
+ // B to C.
+ Triple(SceneB, SceneC, SceneC),
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun animateFromOtherScene() = runMonotonicClockTest {
+ val duration = 500
+ val state =
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ transitions {
+ // Handler that animates from the scene that is not currentScene.
+ interruptionHandler =
+ object : InterruptionHandler {
+ override fun onInterruption(
+ interrupted: TransitionState.Transition,
+ newTargetScene: SceneKey
+ ): InterruptionResult {
+ return InterruptionResult(
+ animateFrom =
+ if (interrupted.currentScene == interrupted.toScene) {
+ interrupted.fromScene
+ } else {
+ interrupted.toScene
+ }
+ )
+ }
+ }
+
+ from(SceneA, to = SceneB) { spec = tween(duration) }
+ },
+ )
+
+ // Animate to B and advance the transition a little bit so that progress > visibility
+ // threshold and that reversing from B back to A won't immediately snap to A.
+ state.setTargetScene(SceneB, coroutineScope = this)
+ testScheduler.advanceTimeBy(duration / 2L)
+
+ state.setTargetScene(SceneC, coroutineScope = this)
+
+ assertThat(state.currentTransitions)
+ .comparingElementsUsing(FromToCurrentTriple)
+ .containsExactly(
+ // Initial transition A to B. This transition will never be consumed by anyone given
+ // that it has the same (from, to) pair as the next transition.
+ Triple(SceneA, SceneB, SceneB),
+
+ // Initial transition reversed, B back to A.
+ Triple(SceneA, SceneB, SceneA),
+
+ // A to C.
+ Triple(SceneA, SceneC, SceneC),
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun animateToFromScene() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {})
+
+ // Fake a transition from A to B that has a non 0 velocity.
+ val progressVelocity = 1f
+ val aToB =
+ transition(
+ from = SceneA,
+ to = SceneB,
+ current = { SceneB },
+ // Progress must be > visibility threshold otherwise we will directly snap to A.
+ progress = { 0.5f },
+ progressVelocity = { progressVelocity },
+ onFinish = { launch {} },
+ )
+ state.startTransition(aToB, transitionKey = null)
+
+ // Animate back to A. The previous transition is reversed, i.e. it has the same (from, to)
+ // pair, and its velocity is used when animating the progress back to 0.
+ val bToA = checkNotNull(state.setTargetScene(SceneA, coroutineScope = this))
+ testScheduler.runCurrent()
+ assertThat(bToA).hasFromScene(SceneA)
+ assertThat(bToA).hasToScene(SceneB)
+ assertThat(bToA).hasCurrentScene(SceneA)
+ assertThat(bToA).hasProgressVelocity(progressVelocity)
+ }
+
+ @Test
+ fun animateToToScene() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {})
+
+ // Fake a transition from A to B with current scene = A that has a non 0 velocity.
+ val progressVelocity = -1f
+ val aToB =
+ transition(
+ from = SceneA,
+ to = SceneB,
+ current = { SceneA },
+ progressVelocity = { progressVelocity },
+ onFinish = { launch {} },
+ )
+ state.startTransition(aToB, transitionKey = null)
+
+ // Animate to B. The previous transition is reversed, i.e. it has the same (from, to) pair,
+ // and its velocity is used when animating the progress to 1.
+ val bToA = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this))
+ testScheduler.runCurrent()
+ assertThat(bToA).hasFromScene(SceneA)
+ assertThat(bToA).hasToScene(SceneB)
+ assertThat(bToA).hasCurrentScene(SceneB)
+ assertThat(bToA).hasProgressVelocity(progressVelocity)
+ }
+
+ companion object {
+ val FromToCurrentTriple =
+ Correspondence.transforming(
+ { transition: TransitionState.Transition? ->
+ Triple(transition?.fromScene, transition?.toScene, transition?.currentScene)
+ },
+ "(from, to, current) triple"
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
index 224ffe29a1b8..9523896e5c00 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt
@@ -43,6 +43,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.assertSizeIsEqualTo
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -157,8 +158,8 @@ class MovableElementTest {
fromSceneZIndex: Float,
toSceneZIndex: Float
): SceneKey {
- assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition).hasFromScene(TestScenes.SceneA)
+ assertThat(transition).hasToScene(TestScenes.SceneB)
assertThat(fromSceneZIndex).isEqualTo(0)
assertThat(toSceneZIndex).isEqualTo(1)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index 93e94f8f95a2..d2c8bd6928ee 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.TestScenes.SceneD
+import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.animation.scene.transition.link.StateLink
import com.android.compose.test.runMonotonicClockTest
import com.google.common.truth.Truth.assertThat
@@ -322,8 +323,8 @@ class SceneTransitionLayoutStateTest {
// Go back to A.
state.setTargetScene(SceneA, coroutineScope = this)
testScheduler.advanceUntilIdle()
- assertThat(state.currentTransition).isNull()
- assertThat(state.transitionState.currentScene).isEqualTo(SceneA)
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
// Specific transition from A to B.
assertThat(
@@ -477,23 +478,24 @@ class SceneTransitionLayoutStateTest {
overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
}
)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneA is NOT defined
progress.value = -0.1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
// scroll from SceneA to SceneB
progress.value = 0.5f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
progress.value = 1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneB is defined
progress.value = 1.1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
- assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneB)
+ val overscrollSpec = assertThat(transition).hasOverscrollSpec()
+ assertThat(overscrollSpec.scene).isEqualTo(SceneB)
}
@Test
@@ -507,23 +509,25 @@ class SceneTransitionLayoutStateTest {
overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
}
)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneA is defined
progress.value = -0.1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull()
- assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneA)
+ val overscrollSpec = assertThat(transition).hasOverscrollSpec()
+ assertThat(overscrollSpec.scene).isEqualTo(SceneA)
// scroll from SceneA to SceneB
progress.value = 0.5f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
progress.value = 1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneB is NOT defined
progress.value = 1.1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
}
@Test
@@ -534,22 +538,24 @@ class SceneTransitionLayoutStateTest {
progress = { progress.value },
sceneTransitions = transitions {}
)
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneA is NOT defined
progress.value = -0.1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
// scroll from SceneA to SceneB
progress.value = 0.5f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
progress.value = 1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
// overscroll for SceneB is NOT defined
progress.value = 1.1f
- assertThat(state.currentTransition?.currentOverscrollSpec).isNull()
+ assertThat(transition).hasNoOverscrollSpec()
}
@Test
@@ -629,4 +635,19 @@ class SceneTransitionLayoutStateTest {
Log.setWtfHandler(originalHandler)
}
}
+
+ @Test
+ fun snapToScene() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutState(SceneA)
+
+ // Transition to B.
+ state.setTargetScene(SceneB, coroutineScope = this)
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasCurrentScene(SceneB)
+
+ // Snap to C.
+ state.snapToScene(SceneC)
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneC)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 7836581c86e8..692c18bb8ac5 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -51,6 +51,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.assertSizeIsEqualTo
import com.android.compose.test.subjects.DpOffsetSubject
import com.android.compose.test.subjects.assertThat
@@ -147,34 +148,34 @@ class SceneTransitionLayoutTest {
rule.onNodeWithText("SceneA").assertIsDisplayed()
rule.onNodeWithText("SceneB").assertDoesNotExist()
rule.onNodeWithText("SceneC").assertDoesNotExist()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Change to scene B. Only that scene is displayed.
currentScene = SceneB
rule.onNodeWithText("SceneA").assertDoesNotExist()
rule.onNodeWithText("SceneB").assertIsDisplayed()
rule.onNodeWithText("SceneC").assertDoesNotExist()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
}
@Test
fun testBack() {
rule.setContent { TestContent() }
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
rule.activity.onBackPressed()
rule.waitForIdle()
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB)
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
}
@Test
fun testTransitionState() {
rule.setContent { TestContent() }
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// We will advance the clock manually.
rule.mainClock.autoAdvance = false
@@ -182,45 +183,38 @@ class SceneTransitionLayoutTest {
// Change the current scene. Until composition is triggered, this won't change the layout
// state.
currentScene = SceneB
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// On the next frame, we will recompose because currentScene changed, which will start the
// transition (i.e. it will change the transitionState to be a Transition) in a
// LaunchedEffect.
rule.mainClock.advanceTimeByFrame()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
- val transition = layoutState.transitionState as TransitionState.Transition
- assertThat(transition.fromScene).isEqualTo(SceneA)
- assertThat(transition.toScene).isEqualTo(SceneB)
- assertThat(transition.progress).isEqualTo(0f)
+ val transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+ assertThat(transition).hasProgress(0f)
// Then, on the next frame, the animator we started gets its initial value and clock
// starting time. We are now at progress = 0f.
rule.mainClock.advanceTimeByFrame()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((layoutState.transitionState as TransitionState.Transition).progress)
- .isEqualTo(0f)
+ assertThat(transition).hasProgress(0f)
// The test transition lasts 480ms. 240ms after the start of the transition, we are at
// progress = 0.5f.
rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((layoutState.transitionState as TransitionState.Transition).progress)
- .isEqualTo(0.5f)
+ assertThat(transition).hasProgress(0.5f)
// (240-16) ms later, i.e. one frame before the transition is finished, we are at
// progress=(480-16)/480.
rule.mainClock.advanceTimeBy(TestTransitionDuration / 2 - 16)
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((layoutState.transitionState as TransitionState.Transition).progress)
- .isEqualTo((TestTransitionDuration - 16) / 480f)
+ assertThat(transition).hasProgress((TestTransitionDuration - 16) / 480f)
// one frame (16ms) later, the transition is finished and we are in the idle state in scene
// B.
rule.mainClock.advanceTimeByFrame()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
}
@Test
@@ -261,8 +255,8 @@ class SceneTransitionLayoutTest {
// 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
// use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is
// going to (x = 0, y = 0), so the offset should now be half what it was.
- assertThat((layoutState.transitionState as TransitionState.Transition).progress)
- .isEqualTo(0.5f)
+ var transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
sharedFoo.assertWidthIsEqualTo(75.dp)
sharedFoo.assertHeightIsEqualTo(75.dp)
sharedFoo.assertPositionInRootIsEqualTo(
@@ -290,8 +284,8 @@ class SceneTransitionLayoutTest {
val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
sharedFoo = rule.onNode(isElement(TestElements.Foo, SceneC))
- assertThat((layoutState.transitionState as TransitionState.Transition).progress)
- .isEqualTo(interpolatedProgress)
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasProgress(interpolatedProgress)
sharedFoo.assertWidthIsEqualTo(expectedSize)
sharedFoo.assertHeightIsEqualTo(expectedSize)
sharedFoo.assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
@@ -305,16 +299,16 @@ class SceneTransitionLayoutTest {
// Wait for the transition to C to finish.
rule.mainClock.advanceTimeBy(TestTransitionDuration)
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneC)
// Go back to scene A. This should happen instantly (once the animation started, i.e. after
// 2 frames) given that we use a snap() animation spec.
currentScene = SceneA
rule.mainClock.advanceTimeByFrame()
rule.mainClock.advanceTimeByFrame()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
}
@Test
@@ -384,7 +378,9 @@ class SceneTransitionLayoutTest {
rule.mainClock.advanceTimeByFrame()
rule.mainClock.advanceTimeBy(duration / 2)
rule.waitForIdle()
- assertThat(state.currentTransition?.progress).isEqualTo(0.5f)
+
+ var transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
// A and B are composed.
rule.onNodeWithTag("aRoot").assertExists()
@@ -396,7 +392,9 @@ class SceneTransitionLayoutTest {
rule.mainClock.advanceTimeByFrame()
rule.mainClock.advanceTimeByFrame()
rule.waitForIdle()
- assertThat(state.currentTransition?.progress).isEqualTo(0f)
+
+ transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0f)
// A, B and C are composed.
rule.onNodeWithTag("aRoot").assertExists()
@@ -405,7 +403,7 @@ class SceneTransitionLayoutTest {
// Let A => B finish.
rule.mainClock.advanceTimeBy(duration / 2L)
- assertThat(state.currentTransition?.progress).isEqualTo(0.5f)
+ assertThat(transition).hasProgress(0.5f)
rule.waitForIdle()
// B and C are composed.
@@ -416,8 +414,8 @@ class SceneTransitionLayoutTest {
// Let B => C finish.
rule.mainClock.advanceTimeBy(duration / 2L)
rule.mainClock.advanceTimeByFrame()
- assertThat(state.currentTransition).isNull()
rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
// Only C is composed.
rule.onNodeWithTag("aRoot").assertDoesNotExist()
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index f034c184b794..1dd9322b3ad5 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -38,6 +38,9 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.subjects.assertThat
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -65,7 +68,7 @@ class SwipeToSceneTest {
@get:Rule val rule = createComposeRule()
private fun layoutState(
- initialScene: SceneKey = TestScenes.SceneA,
+ initialScene: SceneKey = SceneA,
transitions: SceneTransitions = EmptyTestTransitions,
) = MutableSceneTransitionLayoutState(initialScene, transitions)
@@ -80,22 +83,21 @@ class SwipeToSceneTest {
modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.debugName),
) {
scene(
- TestScenes.SceneA,
+ SceneA,
userActions =
if (swipesEnabled())
mapOf(
- Swipe.Left to TestScenes.SceneB,
+ Swipe.Left to SceneB,
Swipe.Down to TestScenes.SceneC,
- Swipe.Up to TestScenes.SceneB,
+ Swipe.Up to SceneB,
)
else emptyMap(),
) {
Box(Modifier.fillMaxSize())
}
scene(
- TestScenes.SceneB,
- userActions =
- if (swipesEnabled()) mapOf(Swipe.Right to TestScenes.SceneA) else emptyMap(),
+ SceneB,
+ userActions = if (swipesEnabled()) mapOf(Swipe.Right to SceneA) else emptyMap(),
) {
Box(Modifier.fillMaxSize())
}
@@ -104,11 +106,10 @@ class SwipeToSceneTest {
userActions =
if (swipesEnabled())
mapOf(
- Swipe.Down to TestScenes.SceneA,
- Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
- Swipe(SwipeDirection.Right, fromSource = Edge.Left) to
- TestScenes.SceneB,
- Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
+ Swipe.Down to SceneA,
+ Swipe(SwipeDirection.Down, pointerCount = 2) to SceneB,
+ Swipe(SwipeDirection.Right, fromSource = Edge.Left) to SceneB,
+ Swipe(SwipeDirection.Down, fromSource = Edge.Top) to SceneB,
)
else emptyMap(),
) {
@@ -129,8 +130,8 @@ class SwipeToSceneTest {
TestContent(layoutState)
}
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Drag left (i.e. from right to left) by 55dp. We pick 55dp here because 56dp is the
// positional threshold from which we commit the gesture.
@@ -144,31 +145,27 @@ class SwipeToSceneTest {
// We should be at a progress = 55dp / LayoutWidth given that we use the layout size in
// the gesture axis as swipe distance.
- var transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
- assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
- assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
- assertThat(transition.isInitiatedByUserInput).isTrue()
+ var transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasProgress(55.dp / LayoutWidth)
+ assertThat(transition).isInitiatedByUserInput()
// Release the finger. We should now be animating back to A (currentScene = SceneA) given
// that 55dp < positional threshold.
rule.onRoot().performTouchInput { up() }
- transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
- assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
- assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
- assertThat(transition.isInitiatedByUserInput).isTrue()
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasProgress(55.dp / LayoutWidth)
+ assertThat(transition).isInitiatedByUserInput()
// Wait for the animation to finish. We should now be in scene A.
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Now we do the same but vertically and with a drag distance of 56dp, which is >=
// positional threshold.
@@ -178,31 +175,27 @@ class SwipeToSceneTest {
}
// Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight
- transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
- assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
- assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
- assertThat(transition.isInitiatedByUserInput).isTrue()
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(TestScenes.SceneC)
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasProgress(56.dp / LayoutHeight)
+ assertThat(transition).isInitiatedByUserInput()
// Release the finger. We should now be animating to C (currentScene = SceneC) given
// that 56dp >= positional threshold.
rule.onRoot().performTouchInput { up() }
- transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
- assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
- assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
- assertThat(transition.isInitiatedByUserInput).isTrue()
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(TestScenes.SceneC)
+ assertThat(transition).hasCurrentScene(TestScenes.SceneC)
+ assertThat(transition).hasProgress(56.dp / LayoutHeight)
+ assertThat(transition).isInitiatedByUserInput()
// Wait for the animation to finish. We should now be in scene C.
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
}
@Test
@@ -216,8 +209,8 @@ class SwipeToSceneTest {
TestContent(layoutState)
}
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Swipe left (i.e. from right to left) using a velocity of 124 dp/s. We pick 124 dp/s here
// because 125 dp/s is the velocity threshold from which we commit the gesture. We also use
@@ -233,18 +226,16 @@ class SwipeToSceneTest {
// We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity
// threshold.
- var transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
- assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
- assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+ var transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasProgress(55.dp / LayoutWidth)
// Wait for the animation to finish. We should now be in scene A.
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Now we do the same but vertically and with a swipe velocity of 126dp, which is >
// velocity threshold. Note that in theory we could have used 125 dp (= velocity threshold)
@@ -259,18 +250,16 @@ class SwipeToSceneTest {
}
// We should be animating to C (currentScene = SceneC).
- transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneA)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
- assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
- assertThat(transition.progress).isEqualTo(55.dp / LayoutHeight)
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(TestScenes.SceneC)
+ assertThat(transition).hasCurrentScene(TestScenes.SceneC)
+ assertThat(transition).hasProgress(55.dp / LayoutHeight)
// Wait for the animation to finish. We should now be in scene C.
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
}
@Test
@@ -286,8 +275,8 @@ class SwipeToSceneTest {
TestContent(layoutState)
}
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
// Swipe down with two fingers.
rule.onRoot().performTouchInput {
@@ -298,18 +287,16 @@ class SwipeToSceneTest {
}
// We are transitioning to B because we used 2 fingers.
- val transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneC)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ val transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(TestScenes.SceneC)
+ assertThat(transition).hasToScene(SceneB)
// Release the fingers and wait for the animation to end. We are back to C because we only
// swiped 10dp.
rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } }
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
}
@Test
@@ -325,8 +312,8 @@ class SwipeToSceneTest {
TestContent(layoutState)
}
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
// Swipe down from the top edge.
rule.onRoot().performTouchInput {
@@ -335,18 +322,16 @@ class SwipeToSceneTest {
}
// We are transitioning to B (and not A) because we started from the top edge.
- var transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneC)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ var transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(TestScenes.SceneC)
+ assertThat(transition).hasToScene(SceneB)
// Release the fingers and wait for the animation to end. We are back to C because we only
// swiped 10dp.
rule.onRoot().performTouchInput { up() }
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
// Swipe right from the left edge.
rule.onRoot().performTouchInput {
@@ -355,18 +340,16 @@ class SwipeToSceneTest {
}
// We are transitioning to B (and not A) because we started from the left edge.
- transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneC)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasFromScene(TestScenes.SceneC)
+ assertThat(transition).hasToScene(SceneB)
// Release the fingers and wait for the animation to end. We are back to C because we only
// swiped 10dp.
rule.onRoot().performTouchInput { up() }
rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC)
}
@Test
@@ -380,7 +363,7 @@ class SwipeToSceneTest {
layoutState(
transitions =
transitions {
- from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ from(SceneA, to = SceneB) {
distance = FixedDistance(verticalSwipeDistance)
}
}
@@ -395,12 +378,12 @@ class SwipeToSceneTest {
modifier = Modifier.size(LayoutWidth, LayoutHeight)
) {
scene(
- TestScenes.SceneA,
- userActions = mapOf(Swipe.Down to TestScenes.SceneB),
+ SceneA,
+ userActions = mapOf(Swipe.Down to SceneB),
) {
Spacer(Modifier.fillMaxSize())
}
- scene(TestScenes.SceneB) { Spacer(Modifier.fillMaxSize()) }
+ scene(SceneB) { Spacer(Modifier.fillMaxSize()) }
}
}
@@ -413,9 +396,9 @@ class SwipeToSceneTest {
}
// We should be at 50%
- val transition = layoutState.currentTransition
+ val transition = assertThat(layoutState.transitionState).isTransition()
assertThat(transition).isNotNull()
- assertThat(transition!!.progress).isEqualTo(0.5f)
+ assertThat(transition).hasProgress(0.5f)
}
@Test
@@ -434,15 +417,14 @@ class SwipeToSceneTest {
}
// We should still correctly compute that we are swiping down to scene C.
- var transition = layoutState.currentTransition
- assertThat(transition).isNotNull()
- assertThat(transition?.toScene).isEqualTo(TestScenes.SceneC)
+ var transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasToScene(TestScenes.SceneC)
// Release the finger, animating back to scene A.
rule.onRoot().performTouchInput { up() }
rule.waitForIdle()
- assertThat(layoutState.currentTransition).isNull()
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Swipe up by exactly touchSlop, so that the drag overSlop is 0f.
rule.onRoot().performTouchInput {
@@ -451,15 +433,14 @@ class SwipeToSceneTest {
}
// We should still correctly compute that we are swiping up to scene B.
- transition = layoutState.currentTransition
- assertThat(transition).isNotNull()
- assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasToScene(SceneB)
// Release the finger, animating back to scene A.
rule.onRoot().performTouchInput { up() }
rule.waitForIdle()
- assertThat(layoutState.currentTransition).isNull()
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(layoutState.transitionState).isIdle()
+ assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
// Swipe left by exactly touchSlop, so that the drag overSlop is 0f.
rule.onRoot().performTouchInput {
@@ -468,14 +449,13 @@ class SwipeToSceneTest {
}
// We should still correctly compute that we are swiping down to scene B.
- transition = layoutState.currentTransition
- assertThat(transition).isNotNull()
- assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB)
+ transition = assertThat(layoutState.transitionState).isTransition()
+ assertThat(transition).hasToScene(SceneB)
}
@Test
fun swipeEnabledLater() {
- val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+ val layoutState = MutableSceneTransitionLayoutState(SceneA)
var swipesEnabled by mutableStateOf(false)
var touchSlop = 0f
rule.setContent {
@@ -509,34 +489,32 @@ class SwipeToSceneTest {
fun transitionKey() {
val transitionkey = TransitionKey(debugName = "foo")
val state =
- MutableSceneTransitionLayoutState(
- TestScenes.SceneA,
+ MutableSceneTransitionLayoutStateImpl(
+ SceneA,
transitions {
- from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) }
- from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) {
+ from(SceneA, to = SceneB) { fade(TestElements.Foo) }
+ from(SceneA, to = SceneB, key = transitionkey) {
fade(TestElements.Foo)
fade(TestElements.Bar)
}
}
)
- as MutableSceneTransitionLayoutStateImpl
var touchSlop = 0f
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
SceneTransitionLayout(state, Modifier.size(LayoutWidth, LayoutHeight)) {
scene(
- TestScenes.SceneA,
+ SceneA,
userActions =
mapOf(
- Swipe.Down to TestScenes.SceneB,
- Swipe.Up to
- UserActionResult(TestScenes.SceneB, transitionKey = transitionkey)
+ Swipe.Down to SceneB,
+ Swipe.Up to UserActionResult(SceneB, transitionKey = transitionkey)
)
) {
Box(Modifier.fillMaxSize())
}
- scene(TestScenes.SceneB) { Box(Modifier.fillMaxSize()) }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
}
}
@@ -546,12 +524,12 @@ class SwipeToSceneTest {
moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
}
- assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+ assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1)
// Move the pointer up to swipe to scene B using the new transition.
rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) }
- assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
+ assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue()
assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2)
}
@@ -567,19 +545,17 @@ class SwipeToSceneTest {
// the difference between the bottom of the scene and the bottom of the element,
// so that we use the offset and size of the element as well as the size of the
// scene.
- val fooSize = TestElements.Foo.targetSize(TestScenes.SceneB) ?: return 0f
- val fooOffset = TestElements.Foo.targetOffset(TestScenes.SceneB) ?: return 0f
- val sceneSize = TestScenes.SceneB.targetSize() ?: return 0f
+ val fooSize = TestElements.Foo.targetSize(SceneB) ?: return 0f
+ val fooOffset = TestElements.Foo.targetOffset(SceneB) ?: return 0f
+ val sceneSize = SceneB.targetSize() ?: return 0f
return sceneSize.height - fooOffset.y - fooSize.height
}
}
val state =
MutableSceneTransitionLayoutState(
- TestScenes.SceneA,
- transitions {
- from(TestScenes.SceneA, to = TestScenes.SceneB) { distance = swipeDistance }
- }
+ SceneA,
+ transitions { from(SceneA, to = SceneB) { distance = swipeDistance } }
)
val layoutSize = 200.dp
@@ -591,10 +567,10 @@ class SwipeToSceneTest {
touchSlop = LocalViewConfiguration.current.touchSlop
SceneTransitionLayout(state, Modifier.size(layoutSize)) {
- scene(TestScenes.SceneA, userActions = mapOf(Swipe.Up to TestScenes.SceneB)) {
+ scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) {
Box(Modifier.fillMaxSize())
}
- scene(TestScenes.SceneB) {
+ scene(SceneB) {
Box(Modifier.fillMaxSize()) {
Box(Modifier.offset(y = fooYOffset).element(TestElements.Foo).size(fooSize))
}
@@ -611,7 +587,9 @@ class SwipeToSceneTest {
}
rule.waitForIdle()
- assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue()
- assertThat(state.currentTransition!!.progress).isWithin(0.01f).of(0.5f)
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+ assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
index c49a5b85ebe3..a609be48a225 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt
@@ -29,6 +29,7 @@ fun transition(
to: SceneKey,
current: () -> SceneKey = { from },
progress: () -> Float = { 0f },
+ progressVelocity: () -> Float = { 0f },
interruptionProgress: () -> Float = { 100f },
isInitiatedByUserInput: Boolean = false,
isUserInputOngoing: Boolean = false,
@@ -42,6 +43,8 @@ fun transition(
get() = current()
override val progress: Float
get() = progress()
+ override val progressVelocity: Float
+ get() = progressVelocity()
override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
override val isUserInputOngoing: Boolean = isUserInputOngoing
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
new file mode 100644
index 000000000000..348989218ce9
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.animation.scene.subjects
+
+import com.android.compose.animation.scene.OverscrollSpec
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionState
+import com.google.common.truth.Fact.simpleFact
+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
+
+/** Assert on a [TransitionState]. */
+fun assertThat(state: TransitionState): TransitionStateSubject {
+ return Truth.assertAbout(TransitionStateSubject.transitionStates()).that(state)
+}
+
+/** Assert on a [TransitionState.Transition]. */
+fun assertThat(transitions: TransitionState.Transition): TransitionSubject {
+ return Truth.assertAbout(TransitionSubject.transitions()).that(transitions)
+}
+
+class TransitionStateSubject
+private constructor(
+ metadata: FailureMetadata,
+ private val actual: TransitionState,
+) : Subject(metadata, actual) {
+ fun hasCurrentScene(sceneKey: SceneKey) {
+ check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
+ }
+
+ fun isIdle(): TransitionState.Idle {
+ if (actual !is TransitionState.Idle) {
+ failWithActual(simpleFact("expected to be TransitionState.Idle"))
+ }
+
+ return actual as TransitionState.Idle
+ }
+
+ fun isTransition(): TransitionState.Transition {
+ if (actual !is TransitionState.Transition) {
+ failWithActual(simpleFact("expected to be TransitionState.Transition"))
+ }
+
+ return actual as TransitionState.Transition
+ }
+
+ companion object {
+ fun transitionStates() = Factory { metadata, actual: TransitionState ->
+ TransitionStateSubject(metadata, actual)
+ }
+ }
+}
+
+class TransitionSubject
+private constructor(
+ metadata: FailureMetadata,
+ private val actual: TransitionState.Transition,
+) : Subject(metadata, actual) {
+ fun hasCurrentScene(sceneKey: SceneKey) {
+ check("currentScene").that(actual.currentScene).isEqualTo(sceneKey)
+ }
+
+ fun hasFromScene(sceneKey: SceneKey) {
+ check("fromScene").that(actual.fromScene).isEqualTo(sceneKey)
+ }
+
+ fun hasToScene(sceneKey: SceneKey) {
+ check("toScene").that(actual.toScene).isEqualTo(sceneKey)
+ }
+
+ fun hasProgress(progress: Float, tolerance: Float = 0f) {
+ check("progress").that(actual.progress).isWithin(tolerance).of(progress)
+ }
+
+ fun hasProgressVelocity(progressVelocity: Float, tolerance: Float = 0f) {
+ check("progressVelocity")
+ .that(actual.progressVelocity)
+ .isWithin(tolerance)
+ .of(progressVelocity)
+ }
+
+ fun isInitiatedByUserInput() {
+ check("isInitiatedByUserInput").that(actual.isInitiatedByUserInput).isTrue()
+ }
+
+ fun hasIsUserInputOngoing(isUserInputOngoing: Boolean) {
+ check("isUserInputOngoing").that(actual.isUserInputOngoing).isEqualTo(isUserInputOngoing)
+ }
+
+ fun hasOverscrollSpec(): OverscrollSpec {
+ check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNotNull()
+ return actual.currentOverscrollSpec!!
+ }
+
+ fun hasNoOverscrollSpec() {
+ check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNull()
+ }
+
+ fun hasBouncingScene(scene: SceneKey) {
+ if (actual !is TransitionState.HasOverscrollProperties) {
+ failWithActual(simpleFact("expected to be TransitionState.HasOverscrollProperties"))
+ }
+
+ check("bouncingScene")
+ .that((actual as TransitionState.HasOverscrollProperties).bouncingScene)
+ .isEqualTo(scene)
+ }
+
+ companion object {
+ fun transitions() = Factory { metadata, actual: TransitionState.Transition ->
+ TransitionSubject(metadata, actual)
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt
new file mode 100644
index 000000000000..c0d481c6e659
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.EnabledOnRavenwood
+class OneHandedModeRepositoryImplTest : SysuiTestCase() {
+
+ private val testUser1 = UserHandle.of(1)!!
+ private val testUser2 = UserHandle.of(2)!!
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val settings: FakeSettings = FakeSettings()
+
+ private val underTest: OneHandedModeRepository =
+ OneHandedModeRepositoryImpl(
+ testDispatcher,
+ scope.backgroundScope,
+ settings,
+ )
+
+ @Test
+ fun isEnabled_settingNotInitialized_returnsFalseByDefault() =
+ scope.runTest {
+ val actualValue by collectLastValue(underTest.isEnabled(testUser1))
+
+ runCurrent()
+
+ assertThat(actualValue).isFalse()
+ }
+
+ @Test
+ fun isEnabled_initiallyGetsSettingsValue() =
+ scope.runTest {
+ val actualValue by collectLastValue(underTest.isEnabled(testUser1))
+
+ settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
+ runCurrent()
+
+ assertThat(actualValue).isTrue()
+ }
+
+ @Test
+ fun isEnabled_settingUpdated_valueUpdated() =
+ scope.runTest {
+ val actualValue by collectLastValue(underTest.isEnabled(testUser1))
+ runCurrent()
+ assertThat(actualValue).isFalse()
+
+ settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
+ runCurrent()
+
+ assertThat(actualValue).isTrue()
+ runCurrent()
+
+ settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
+ runCurrent()
+ assertThat(actualValue).isFalse()
+ }
+
+ @Test
+ fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() =
+ scope.runTest {
+ val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1))
+ val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2))
+
+ settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier)
+ settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier)
+ runCurrent()
+ assertThat(lastValueUser1).isFalse()
+ assertThat(lastValueUser2).isFalse()
+
+ settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier)
+ runCurrent()
+ assertThat(lastValueUser1).isTrue()
+ assertThat(lastValueUser2).isFalse()
+ }
+
+ @Test
+ fun setEnabled() =
+ scope.runTest {
+ val success = underTest.setIsEnabled(true, testUser1)
+ runCurrent()
+ assertThat(success).isTrue()
+
+ val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
+ assertThat(actualValue).isEqualTo(ENABLED)
+ }
+
+ @Test
+ fun setDisabled() =
+ scope.runTest {
+ val success = underTest.setIsEnabled(false, testUser1)
+ runCurrent()
+ assertThat(success).isTrue()
+
+ val actualValue = settings.getIntForUser(SETTING_NAME, testUser1.identifier)
+ assertThat(actualValue).isEqualTo(DISABLED)
+ }
+
+ companion object {
+ private const val SETTING_NAME = Settings.Secure.ONE_HANDED_MODE_ENABLED
+ private const val DISABLED = 0
+ private const val ENABLED = 1
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index dfc04ff248ec..456fb79d0536 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -57,6 +57,8 @@ import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.activityStarter
@@ -1090,6 +1092,78 @@ class CommunalInteractorTest : SysuiTestCase() {
.isEqualTo(USER_INFO_WORK.id)
}
+ @Test
+ fun showCommunalFromOccluded_enteredOccludedFromHub() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+ assertThat(showCommunalFromOccluded).isFalse()
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ testScope
+ )
+
+ assertThat(showCommunalFromOccluded).isTrue()
+ }
+
+ @Test
+ fun showCommunalFromOccluded_enteredOccludedFromLockscreen() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+ assertThat(showCommunalFromOccluded).isFalse()
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ testScope
+ )
+
+ assertThat(showCommunalFromOccluded).isFalse()
+ }
+
+ @Test
+ fun showCommunalFromOccluded_communalBecomesUnavailableWhileOccluded() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+ assertThat(showCommunalFromOccluded).isFalse()
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ testScope
+ )
+ runCurrent()
+ kosmos.setCommunalAvailable(false)
+
+ assertThat(showCommunalFromOccluded).isFalse()
+ }
+
+ @Test
+ fun showCommunalFromOccluded_showBouncerWhileOccluded() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+ val showCommunalFromOccluded by collectLastValue(underTest.showCommunalFromOccluded)
+ assertThat(showCommunalFromOccluded).isFalse()
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ testScope
+ )
+ runCurrent()
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ testScope
+ )
+
+ assertThat(showCommunalFromOccluded).isTrue()
+ }
+
private fun smartspaceTimer(id: String, timestamp: Long = 0L): SmartspaceTarget {
val timer = mock(SmartspaceTarget::class.java)
whenever(timer.smartspaceTargetId).thenReturn(id)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 20beabb983da..2546f27cb351 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -41,6 +41,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -144,6 +145,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val testScope = kosmos.testScope
private val fakeUserRepository = kosmos.fakeUserRepository
+ private val fakeExecutor = kosmos.fakeExecutor
private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?>
private lateinit var detectStatus: FlowValue<FaceDetectionStatus?>
private lateinit var authRunning: FlowValue<Boolean?>
@@ -220,12 +222,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
testScope.backgroundScope,
testDispatcher,
testDispatcher,
+ fakeExecutor,
sessionTracker,
uiEventLogger,
FaceAuthenticationLogger(logcatLogBuffer("DeviceEntryFaceAuthRepositoryLog")),
biometricSettingsRepository,
deviceEntryFingerprintAuthRepository,
- trustRepository,
keyguardRepository,
powerInteractor,
keyguardInteractor,
@@ -292,6 +294,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
fun faceLockoutStatusIsPropagated() =
testScope.runTest {
initCollectors()
+ fakeExecutor.runAllReady()
verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture())
allPreconditionsToRunFaceAuthAreTrue()
@@ -1177,6 +1180,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
+ fakeExecutor.runAllReady()
verify(faceManager, atLeastOnce())
.addLockoutResetCallback(faceLockoutResetCallback.capture())
trustRepository.setCurrentUserTrusted(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 412292554e73..bf0939c6c46f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -235,7 +235,13 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
.isEqualTo(
listOf(
// The initial transition will also get sent when collect started
- TransitionStep(OFF, LOCKSCREEN, 0f, STARTED),
+ TransitionStep(
+ OFF,
+ LOCKSCREEN,
+ 0f,
+ STARTED,
+ ownerName = "KeyguardTransitionRepository(boot)"
+ ),
steps[0],
steps[3],
steps[6]
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
index 31b67b43adc4..f52c66e24907 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -16,36 +16,57 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
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.kosmos.testScope
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@ExperimentalCoroutinesApi
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class AodToLockscreenTransitionViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class AodToLockscreenTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
val repository = kosmos.fakeKeyguardTransitionRepository
- val shadeRepository = kosmos.fakeShadeRepository
+ val shadeTestUtil by lazy { kosmos.shadeTestUtil }
val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
- val underTest = kosmos.aodToLockscreenTransitionViewModel
+ lateinit var underTest: AodToLockscreenTransitionViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ @Before
+ fun setup() {
+ underTest = kosmos.aodToLockscreenTransitionViewModel
+ }
@Test
fun deviceEntryParentViewShows() =
@@ -65,7 +86,7 @@ class AodToLockscreenTransitionViewModelTest : SysuiTestCase() {
testScope.runTest {
val alpha by collectLastValue(underTest.notificationAlpha)
- shadeRepository.setQsExpansion(0.5f)
+ shadeTestUtil.setQsExpansion(0.5f)
runCurrent()
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
@@ -81,7 +102,7 @@ class AodToLockscreenTransitionViewModelTest : SysuiTestCase() {
testScope.runTest {
val alpha by collectLastValue(underTest.notificationAlpha)
- shadeRepository.setQsExpansion(0f)
+ shadeTestUtil.setQsExpansion(0f)
runCurrent()
repository.sendTransitionStep(step(0f, TransitionState.STARTED))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
index bef951554b50..e3ae3ba4cedd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
@@ -16,13 +16,14 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -32,31 +33,53 @@ import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@ExperimentalCoroutinesApi
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class LockscreenToAodTransitionViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class LockscreenToAodTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
}
private val testScope = kosmos.testScope
private val repository = kosmos.fakeKeyguardTransitionRepository
- private val shadeRepository = kosmos.shadeRepository
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
private val biometricSettingsRepository = kosmos.biometricSettingsRepository
- private val underTest = kosmos.lockscreenToAodTransitionViewModel
+
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ lateinit var underTest: LockscreenToAodTransitionViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ @Before
+ fun setup() {
+ underTest = kosmos.lockscreenToAodTransitionViewModel
+ }
@Test
fun backgroundViewAlpha_shadeNotExpanded() =
@@ -195,11 +218,11 @@ class LockscreenToAodTransitionViewModelTest : SysuiTestCase() {
private fun shadeExpanded(expanded: Boolean) {
if (expanded) {
- shadeRepository.setQsExpansion(1f)
+ shadeTestUtil.setQsExpansion(1f)
} else {
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- shadeRepository.setQsExpansion(0f)
- shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeTestUtil.setQsExpansion(0f)
+ shadeTestUtil.setLockscreenShadeExpansion(0f)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 8f04ec3814eb..adeb395fc142 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,24 +18,25 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.ShadeTestUtil
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
@@ -45,10 +46,12 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class LockscreenToDreamingTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -56,14 +59,27 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
}
private val testScope = kosmos.testScope
private lateinit var repository: FakeKeyguardTransitionRepository
- private lateinit var shadeRepository: ShadeRepository
+ private lateinit var shadeTestUtil: ShadeTestUtil
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var underTest: LockscreenToDreamingTransitionViewModel
+ // add to init block
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
@Before
fun setUp() {
repository = kosmos.fakeKeyguardTransitionRepository
- shadeRepository = kosmos.shadeRepository
+ shadeTestUtil = kosmos.shadeTestUtil
keyguardRepository = kosmos.fakeKeyguardRepository
underTest = kosmos.lockscreenToDreamingTransitionViewModel
}
@@ -177,11 +193,11 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
private fun shadeExpanded(expanded: Boolean) {
if (expanded) {
- shadeRepository.setQsExpansion(1f)
+ shadeTestUtil.setQsExpansion(1f)
} else {
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- shadeRepository.setQsExpansion(0f)
- shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeTestUtil.setQsExpansion(0f)
+ shadeTestUtil.setLockscreenShadeExpansion(0f)
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index b120f8776d9d..f8da74fdd742 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,27 +18,28 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
-import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.ShadeTestUtil
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
@@ -48,25 +49,40 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
private val testScope = kosmos.testScope
private lateinit var repository: FakeKeyguardTransitionRepository
- private lateinit var shadeRepository: ShadeRepository
+ private lateinit var shadeTestUtil: ShadeTestUtil
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var configurationRepository: FakeConfigurationRepository
private lateinit var underTest: LockscreenToOccludedTransitionViewModel
+ // add to init block
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
@Before
fun setUp() {
repository = kosmos.fakeKeyguardTransitionRepository
- shadeRepository = kosmos.shadeRepository
+ shadeTestUtil = kosmos.shadeTestUtil
keyguardRepository = kosmos.fakeKeyguardRepository
configurationRepository = kosmos.fakeConfigurationRepository
underTest = kosmos.lockscreenToOccludedTransitionViewModel
@@ -200,11 +216,11 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() {
private fun shadeExpanded(expanded: Boolean) {
if (expanded) {
- shadeRepository.setQsExpansion(1f)
+ shadeTestUtil.setQsExpansion(1f)
} else {
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- shadeRepository.setQsExpansion(0f)
- shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeTestUtil.setQsExpansion(0f)
+ shadeTestUtil.setLockscreenShadeExpansion(0f)
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index 43ab93a18118..d5df159d6d1e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -16,11 +16,12 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -29,29 +30,50 @@ import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@ExperimentalCoroutinesApi
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class LockscreenToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterization?) :
+ SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
private val testScope = kosmos.testScope
private val repository = kosmos.fakeKeyguardTransitionRepository
- private val shadeRepository = kosmos.shadeRepository
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
private val keyguardRepository = kosmos.fakeKeyguardRepository
- private val underTest = kosmos.lockscreenToPrimaryBouncerTransitionViewModel
+ private lateinit var underTest: LockscreenToPrimaryBouncerTransitionViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ @Before
+ fun setup() {
+ underTest = kosmos.lockscreenToPrimaryBouncerTransitionViewModel
+ }
@Test
fun deviceEntryParentViewAlpha_shadeExpanded() =
@@ -119,11 +141,11 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() {
private fun shadeExpanded(expanded: Boolean) {
if (expanded) {
- shadeRepository.setQsExpansion(1f)
+ shadeTestUtil.setQsExpansion(1f)
} else {
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- shadeRepository.setQsExpansion(0f)
- shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeTestUtil.setQsExpansion(0f)
+ shadeTestUtil.setLockscreenShadeExpansion(0f)
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
new file mode 100644
index 000000000000..5661bd388757
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notifications.ui.viewmodel
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel
+import com.android.systemui.testKosmos
+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.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class NotificationsShadeSceneViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor = kosmos.sceneInteractor
+ private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
+
+ private val underTest = kosmos.notificationsShadeSceneViewModel
+
+ @Test
+ fun upTransitionSceneKey_deviceLocked_lockscreen() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun upTransitionSceneKey_deviceUnlocked_gone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+ unlockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ runCurrent()
+ sceneInteractor.changeScene(Scenes.Gone, "reason")
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
+ }
+
+ private fun TestScope.lockDevice() {
+ val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+ runCurrent()
+ }
+
+ private fun TestScope.unlockDevice() {
+ val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ sceneInteractor.changeScene(Scenes.Gone, "reason")
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
index 7e0e7d1f46e8..302ac35f1a8a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
@@ -16,8 +16,8 @@
package com.android.systemui.qs.pipeline.domain.interactor
-import android.view.View
import com.android.internal.logging.InstanceId
+import com.android.systemui.animation.Expandable
import com.android.systemui.plugins.qs.QSTile
class FakeQSTile(
@@ -56,11 +56,11 @@ class FakeQSTile(
callbacks.clear()
}
- override fun click(view: View?) {}
+ override fun click(expandable: Expandable?) {}
- override fun secondaryClick(view: View?) {}
+ override fun secondaryClick(expandable: Expandable?) {}
- override fun longClick(view: View?) {}
+ override fun longClick(expandable: Expandable?) {}
override fun userSwitch(currentUser: Int) {
user = currentUser
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
index 182a6040b3b3..d3095542e90f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingUserActionInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles.impl.fontscaling.domain.interactor
+import android.content.Context
import android.provider.Settings
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -23,6 +24,8 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.animation.LaunchableView
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
@@ -36,7 +39,6 @@ import com.android.systemui.statusbar.phone.SystemUIDialog
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.nullable
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth
import kotlinx.coroutines.test.runTest
@@ -63,6 +65,8 @@ class FontScalingUserActionInteractorTest : SysuiTestCase() {
@Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
@Mock private lateinit var dialog: SystemUIDialog
@Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var expandable: Expandable
+ @Mock private lateinit var controller: DialogTransitionAnimator.Controller
@Captor private lateinit var argumentCaptor: ArgumentCaptor<Runnable>
@@ -73,6 +77,9 @@ class FontScalingUserActionInteractorTest : SysuiTestCase() {
dialog = mock<SystemUIDialog>()
fontScalingDialogDelegate =
mock<FontScalingDialogDelegate> { whenever(createDialog()).thenReturn(dialog) }
+ controller = mock<DialogTransitionAnimator.Controller>()
+ expandable =
+ mock<Expandable> { whenever(dialogTransitionController(any())).thenReturn(controller) }
argumentCaptor = ArgumentCaptor.forClass(Runnable::class.java)
underTest =
@@ -90,9 +97,8 @@ class FontScalingUserActionInteractorTest : SysuiTestCase() {
fun clickTile_screenUnlocked_showDialogAnimationFromView() =
kosmos.testScope.runTest {
keyguardStateController.isShowing = false
- val testView = View(context)
- underTest.handleInput(click(FontScalingTileModel, view = testView))
+ underTest.handleInput(click(FontScalingTileModel, expandable = expandable))
verify(activityStarter)
.executeRunnableDismissingKeyguard(
@@ -103,17 +109,15 @@ class FontScalingUserActionInteractorTest : SysuiTestCase() {
eq(false)
)
argumentCaptor.value.run()
- verify(mDialogTransitionAnimator)
- .showFromView(any(), eq(testView), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@Test
fun clickTile_onLockScreen_neverShowDialogAnimationFromView_butShowsDialog() =
kosmos.testScope.runTest {
keyguardStateController.isShowing = true
- val testView = View(context)
- underTest.handleInput(click(FontScalingTileModel, view = testView))
+ underTest.handleInput(click(FontScalingTileModel, expandable = expandable))
verify(activityStarter)
.executeRunnableDismissingKeyguard(
@@ -124,8 +128,7 @@ class FontScalingUserActionInteractorTest : SysuiTestCase() {
eq(false)
)
argumentCaptor.value.run()
- verify(mDialogTransitionAnimator, never())
- .showFromView(any(), eq(testView), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator, never()).show(any(), any(), anyBoolean())
verify(dialog).show()
}
@@ -140,4 +143,8 @@ class FontScalingUserActionInteractorTest : SysuiTestCase() {
val expectedIntentAction = Settings.ACTION_TEXT_READING_SETTINGS
Truth.assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
}
+
+ private class FontScalingTileTestView(context: Context) : View(context), LaunchableView {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileDataInteractorTest.kt
new file mode 100644
index 000000000000..0761ee734830
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileDataInteractorTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.domain.interactor
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.oneHandedModeRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileDataInteractor
+import com.android.wm.shell.onehanded.OneHanded
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OneHandedModeTileDataInteractorTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val oneHandedModeRepository = kosmos.oneHandedModeRepository
+ private val underTest: OneHandedModeTileDataInteractor =
+ OneHandedModeTileDataInteractor(oneHandedModeRepository)
+
+ @Test
+ fun availability_matchesController() = runTest {
+ val expectedAvailability = OneHanded.sIsSupportOneHandedMode
+ val availability by collectLastValue(underTest.availability(testUser))
+
+ assertThat(availability).isEqualTo(expectedAvailability)
+ }
+
+ @Test
+ fun data_matchesRepository() = runTest {
+ val lastData by
+ collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
+ runCurrent()
+ assertThat(lastData!!.isEnabled).isFalse()
+
+ oneHandedModeRepository.setIsEnabled(true, testUser)
+ runCurrent()
+ assertThat(lastData!!.isEnabled).isTrue()
+
+ oneHandedModeRepository.setIsEnabled(false, testUser)
+ runCurrent()
+ assertThat(lastData!!.isEnabled).isFalse()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileUserActionInteractorTest.kt
new file mode 100644
index 000000000000..3f17d4cec643
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/domain/interactor/OneHandedModeTileUserActionInteractorTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.domain.interactor
+
+import android.os.UserHandle
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.FakeOneHandedModeRepository
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class OneHandedModeTileUserActionInteractorTest : SysuiTestCase() {
+
+ private val testUser = UserHandle.of(1)
+ private val repository = FakeOneHandedModeRepository()
+ private val inputHandler = FakeQSTileIntentUserInputHandler()
+
+ private val underTest =
+ OneHandedModeTileUserActionInteractor(
+ repository,
+ inputHandler,
+ )
+
+ @Test
+ fun handleClickWhenEnabled() = runTest {
+ val wasEnabled = true
+ repository.setIsEnabled(wasEnabled, testUser)
+
+ underTest.handleInput(
+ QSTileInputTestKtx.click(OneHandedModeTileModel(wasEnabled), testUser)
+ )
+
+ assertThat(repository.isEnabled(testUser).value).isEqualTo(!wasEnabled)
+ }
+
+ @Test
+ fun handleClickWhenDisabled() = runTest {
+ val wasEnabled = false
+ repository.setIsEnabled(wasEnabled, testUser)
+
+ underTest.handleInput(
+ QSTileInputTestKtx.click(OneHandedModeTileModel(wasEnabled), testUser)
+ )
+
+ assertThat(repository.isEnabled(testUser).value).isEqualTo(!wasEnabled)
+ }
+
+ @Test
+ fun handleLongClickWhenDisabled() = runTest {
+ val enabled = false
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(OneHandedModeTileModel(enabled), testUser)
+ )
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_ONE_HANDED_SETTINGS)
+ }
+ }
+
+ @Test
+ fun handleLongClickWhenEnabled() = runTest {
+ val enabled = true
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(OneHandedModeTileModel(enabled), testUser)
+ )
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_ONE_HANDED_SETTINGS)
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt
new file mode 100644
index 000000000000..7ef020da8b67
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tileimpl.SubtitleArrayMapping
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
+import com.android.systemui.qs.tiles.impl.onehanded.qsOneHandedModeTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OneHandedModeTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val config = kosmos.qsOneHandedModeTileConfig
+ private val subtitleArrayId = SubtitleArrayMapping.getSubtitleId(config.tileSpec.spec)
+ private val subtitleArray by lazy { context.resources.getStringArray(subtitleArrayId) }
+
+ private lateinit var mapper: OneHandedModeTileMapper
+
+ @Before
+ fun setup() {
+ mapper =
+ OneHandedModeTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(
+ com.android.internal.R.drawable.ic_qs_one_handed_mode,
+ TestStubDrawable()
+ )
+ }
+ .resources,
+ context.theme
+ )
+ }
+
+ @Test
+ fun disabledModel() {
+ val inputModel = OneHandedModeTileModel(false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createOneHandedModeTileState(
+ QSTileState.ActivationState.INACTIVE,
+ subtitleArray[1],
+ com.android.internal.R.drawable.ic_qs_one_handed_mode
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel() {
+ val inputModel = OneHandedModeTileModel(true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createOneHandedModeTileState(
+ QSTileState.ActivationState.ACTIVE,
+ subtitleArray[2],
+ com.android.internal.R.drawable.ic_qs_one_handed_mode
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createOneHandedModeTileState(
+ activationState: QSTileState.ActivationState,
+ secondaryLabel: String,
+ iconRes: Int,
+ ): QSTileState {
+ val label = context.getString(R.string.quick_settings_onehanded_label)
+ return QSTileState(
+ { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ label,
+ null,
+ QSTileState.SideViewIcon.None,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
index b9321d55e9dc..91f4ea8ffcc0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
@@ -18,12 +18,11 @@ package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor
import android.app.Dialog
import android.os.UserHandle
-import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -138,12 +137,17 @@ class ScreenRecordTileUserActionInteractorTest : SysuiTestCase() {
*/
@Test
fun handleClickFromView_whenDoingNothing_whenKeyguardNotShowing_showDialogFromView() = runTest {
- val view = mock<View>()
+ val expandable = mock<Expandable>()
+ val controller = mock<DialogTransitionAnimator.Controller>()
+ whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
+
kosmos.fakeKeyguardRepository.setKeyguardShowing(false)
val recordingModel = ScreenRecordTileModel.DoingNothing
- underTest.handleInput(QSTileInputTestKtx.click(recordingModel, UserHandle.CURRENT, view))
+ underTest.handleInput(
+ QSTileInputTestKtx.click(recordingModel, UserHandle.CURRENT, expandable)
+ )
val onStartRecordingClickedCaptor = argumentCaptor<Runnable>()
verify(recordingController)
.createScreenRecordDialog(
@@ -158,6 +162,6 @@ class ScreenRecordTileUserActionInteractorTest : SysuiTestCase() {
verify(keyguardDismissUtil)
.executeWhenUnlocked(onDismissActionCaptor.capture(), eq(false), eq(true))
onDismissActionCaptor.value.onDismiss()
- verify(dialogTransitionAnimator).showFromView(eq(dialog), eq(view), any(), eq(true))
+ verify(dialogTransitionAnimator).show(eq(dialog), eq(controller), eq(true))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index f2eb7f44600e..c660ff3a7297 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -273,21 +273,56 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
}
@Test
- fun customizing_QS() =
+ fun customizing_QS_noAnimations() =
testScope.runTest {
- val customizing by collectLastValue(underTest.isCustomizing)
+ val customizerState by collectLastValue(underTest.customizerState)
underTest.inflate(context)
runCurrent()
underTest.setState(QSSceneAdapter.State.QS)
- assertThat(customizing).isFalse()
+ assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
underTest.setCustomizerShowing(true)
- assertThat(customizing).isTrue()
+ assertThat(customizerState).isEqualTo(CustomizerState.Showing)
underTest.setCustomizerShowing(false)
- assertThat(customizing).isFalse()
+ assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
+ }
+
+ // This matches the calls made by QSCustomizer
+ @Test
+ fun customizing_QS_animations_correctStates() =
+ testScope.runTest {
+ val customizerState by collectLastValue(underTest.customizerState)
+ val animatingInDuration = 100L
+ val animatingOutDuration = 50L
+
+ underTest.inflate(context)
+ runCurrent()
+ underTest.setState(QSSceneAdapter.State.QS)
+
+ assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
+
+ // Start showing customizer with animation
+ underTest.setCustomizerAnimating(true)
+ underTest.setCustomizerShowing(true, animatingInDuration)
+ assertThat(customizerState)
+ .isEqualTo(CustomizerState.AnimatingIntoCustomizer(animatingInDuration))
+
+ // Finish animation
+ underTest.setCustomizerAnimating(false)
+ assertThat(customizerState).isEqualTo(CustomizerState.Showing)
+
+ // Start closing customizer with animation
+ underTest.setCustomizerAnimating(true)
+ underTest.setCustomizerShowing(false, animatingOutDuration)
+ assertThat(customizerState)
+ .isEqualTo(CustomizerState.AnimatingOutOfCustomizer(animatingOutDuration))
+
+ // Finish animation
+ underTest.setCustomizerAnimating(false)
+ assertThat(customizerState).isEqualTo(CustomizerState.Hidden)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
new file mode 100644
index 000000000000..034c2e9b6789
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.ui.viewmodel
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.ui.viewmodel.quickSettingsShadeSceneViewModel
+import com.android.systemui.testKosmos
+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.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor = kosmos.sceneInteractor
+ private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
+
+ private val underTest = kosmos.quickSettingsShadeSceneViewModel
+
+ @Test
+ fun upTransitionSceneKey_deviceLocked_lockscreen() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun upTransitionSceneKey_deviceUnlocked_gone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+ unlockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ runCurrent()
+ sceneInteractor.changeScene(Scenes.Gone, "reason")
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone)
+ }
+
+ private fun TestScope.lockDevice() {
+ val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+ runCurrent()
+ }
+
+ private fun TestScope.unlockDevice() {
+ val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ sceneInteractor.changeScene(Scenes.Gone, "reason")
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
index 883760c67310..df30c4bf1b5a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
@@ -70,6 +70,9 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
underTest.changeScene(Scenes.Shade)
assertThat(currentScene).isEqualTo(Scenes.Shade)
+
+ underTest.snapToScene(Scenes.QuickSettings)
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
}
@Test(expected = IllegalStateException::class)
@@ -79,6 +82,13 @@ class SceneContainerRepositoryTest : SysuiTestCase() {
underTest.changeScene(Scenes.Shade)
}
+ @Test(expected = IllegalStateException::class)
+ fun snapToScene_noSuchSceneInContainer_throws() {
+ kosmos.sceneKeys = listOf(Scenes.QuickSettings, Scenes.Lockscreen)
+ val underTest = kosmos.sceneContainerRepository
+ underTest.snapToScene(Scenes.Shade)
+ }
+
@Test
fun isVisible() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index c16d5224e0ba..2fa94effdbd4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -126,6 +126,71 @@ class SceneInteractorTest : SysuiTestCase() {
}
@Test
+ fun snapToScene_toUnknownScene_doesNothing() =
+ testScope.runTest {
+ val sceneKeys =
+ listOf(
+ Scenes.QuickSettings,
+ Scenes.Shade,
+ Scenes.Lockscreen,
+ Scenes.Gone,
+ Scenes.Communal,
+ )
+ val navigationDistances =
+ mapOf(
+ Scenes.Gone to 0,
+ Scenes.Lockscreen to 0,
+ Scenes.Communal to 1,
+ Scenes.Shade to 2,
+ Scenes.QuickSettings to 3,
+ )
+ kosmos.sceneContainerConfig =
+ SceneContainerConfig(sceneKeys, Scenes.Lockscreen, navigationDistances)
+ underTest = kosmos.sceneInteractor
+ val currentScene by collectLastValue(underTest.currentScene)
+ val previousScene = currentScene
+ assertThat(previousScene).isNotEqualTo(Scenes.Bouncer)
+ underTest.snapToScene(Scenes.Bouncer, "reason")
+ assertThat(currentScene).isEqualTo(previousScene)
+ }
+
+ @Test
+ fun snapToScene() =
+ testScope.runTest {
+ underTest = kosmos.sceneInteractor
+
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ underTest.snapToScene(Scenes.Shade, "reason")
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ }
+
+ @Test
+ fun snapToScene_toGoneWhenUnl_doesNotThrow() =
+ testScope.runTest {
+ underTest = kosmos.sceneInteractor
+
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+
+ underTest.snapToScene(Scenes.Gone, "reason")
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun snapToScene_toGoneWhenStillLocked_throws() =
+ testScope.runTest {
+ underTest = kosmos.sceneInteractor
+ underTest.snapToScene(Scenes.Gone, "reason")
+ }
+
+ @Test
fun sceneChanged_inDataSource() =
testScope.runTest {
underTest = kosmos.sceneInteractor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 66f741620e44..d6e3879b899f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -20,7 +20,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
import static com.google.common.truth.Truth.assertThat;
@@ -79,12 +79,12 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
import java.util.List;
import java.util.concurrent.Executor;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@RunWith(ParameterizedAndroidJunit4.class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -341,7 +341,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) != 0).isTrue();
assertThat(
- (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING)
+ (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY)
!= 0)
.isTrue();
}
@@ -353,7 +353,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue();
assertThat(
- (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING)
+ (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY)
== 0)
.isTrue();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index 6e7e402a5d19..44c9695d8b03 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.shade.domain.startable
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
@@ -37,6 +39,7 @@ import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.shade.ShadeExpansionListener
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
@@ -49,7 +52,6 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
@@ -67,7 +69,7 @@ class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val fakeConfigurationRepository by lazy { kosmos.fakeConfigurationRepository }
private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
- private lateinit var underTest: ShadeStartable
+ private val underTest: ShadeStartable = kosmos.shadeStartable
companion object {
@JvmStatic
@@ -81,13 +83,9 @@ class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
mSetFlagsRule.setFlagsParameterization(flags!!)
}
- @Before
- fun setup() {
- underTest = kosmos.shadeStartable
- }
-
@Test
- fun hydrateShadeMode() =
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun hydrateShadeMode_dualShadeDisabled() =
testScope.runTest {
overrideResource(R.bool.config_use_split_notification_shade, false)
val shadeMode by collectLastValue(shadeInteractor.shadeMode)
@@ -105,6 +103,25 @@ class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
}
@Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun hydrateShadeMode_dualShadeEnabled() =
+ testScope.runTest {
+ overrideResource(R.bool.config_use_split_notification_shade, false)
+ val shadeMode by collectLastValue(shadeInteractor.shadeMode)
+
+ underTest.start()
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+
+ overrideResource(R.bool.config_use_split_notification_shade, true)
+ fakeConfigurationRepository.onAnyConfigurationChange()
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+
+ overrideResource(R.bool.config_use_split_notification_shade, false)
+ fakeConfigurationRepository.onAnyConfigurationChange()
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ }
+
+ @Test
@EnableSceneContainer
fun hydrateShadeExpansionStateManager() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
new file mode 100644
index 000000000000..35e4047109d5
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notifications.ui.composable.NotificationScrimNestedScrollConnection
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() {
+ private var isStarted = false
+ private var scrimOffset = 0f
+ private var contentHeight = 0f
+ private var isCurrentGestureOverscroll = false
+
+ private val scrollConnection =
+ NotificationScrimNestedScrollConnection(
+ scrimOffset = { scrimOffset },
+ snapScrimOffset = { _ -> },
+ animateScrimOffset = { _ -> },
+ minScrimOffset = { MIN_SCRIM_OFFSET },
+ maxScrimOffset = MAX_SCRIM_OFFSET,
+ contentHeight = { contentHeight },
+ minVisibleScrimHeight = { MIN_VISIBLE_SCRIM_HEIGHT },
+ isCurrentGestureOverscroll = { isCurrentGestureOverscroll },
+ onStart = { isStarted = true },
+ onStop = { isStarted = false },
+ )
+
+ @Test
+ fun onScrollUp_canStartPreScroll_contentNotExpanded_ignoreScroll() = runTest {
+ contentHeight = COLLAPSED_CONTENT_HEIGHT
+
+ val offsetConsumed =
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = -1f),
+ source = NestedScrollSource.Drag,
+ )
+
+ assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+ assertThat(isStarted).isEqualTo(false)
+ }
+
+ @Test
+ fun onScrollUp_canStartPreScroll_contentExpandedAtMinOffset_ignoreScroll() = runTest {
+ contentHeight = EXPANDED_CONTENT_HEIGHT
+ scrimOffset = MIN_SCRIM_OFFSET
+
+ val offsetConsumed =
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = -1f),
+ source = NestedScrollSource.Drag,
+ )
+
+ assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+ assertThat(isStarted).isEqualTo(false)
+ }
+
+ @Test
+ fun onScrollUp_canStartPreScroll_contentExpanded_consumeScroll() = runTest {
+ contentHeight = EXPANDED_CONTENT_HEIGHT
+
+ val availableOffset = Offset(x = 0f, y = -1f)
+ val offsetConsumed =
+ scrollConnection.onPreScroll(
+ available = availableOffset,
+ source = NestedScrollSource.Drag,
+ )
+
+ assertThat(offsetConsumed).isEqualTo(availableOffset)
+ assertThat(isStarted).isEqualTo(true)
+ }
+
+ @Test
+ fun onScrollUp_canStartPreScroll_contentExpanded_consumeScrollWithRemainder() = runTest {
+ contentHeight = EXPANDED_CONTENT_HEIGHT
+ scrimOffset = MIN_SCRIM_OFFSET + 1
+
+ val availableOffset = Offset(x = 0f, y = -2f)
+ val consumableOffset = Offset(x = 0f, y = -1f)
+ val offsetConsumed =
+ scrollConnection.onPreScroll(
+ available = availableOffset,
+ source = NestedScrollSource.Drag,
+ )
+
+ assertThat(offsetConsumed).isEqualTo(consumableOffset)
+ assertThat(isStarted).isEqualTo(true)
+ }
+
+ @Test
+ fun onScrollUp_canStartPostScroll_ignoreScroll() = runTest {
+ val offsetConsumed =
+ scrollConnection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(x = 0f, y = -1f),
+ source = NestedScrollSource.Drag,
+ )
+
+ assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+ assertThat(isStarted).isEqualTo(false)
+ }
+
+ @Test
+ fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest {
+ val offsetConsumed =
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = 1f),
+ source = NestedScrollSource.Drag,
+ )
+
+ assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+ assertThat(isStarted).isEqualTo(false)
+ }
+
+ @Test
+ fun onScrollDown_canStartPostScroll_consumeScroll() = runTest {
+ scrimOffset = MIN_SCRIM_OFFSET
+
+ val availableOffset = Offset(x = 0f, y = 1f)
+ val offsetConsumed =
+ scrollConnection.onPostScroll(
+ consumed = Offset.Zero,
+ available = availableOffset,
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(offsetConsumed).isEqualTo(availableOffset)
+ assertThat(isStarted).isEqualTo(true)
+ }
+
+ @Test
+ fun onScrollDown_canStartPostScroll_consumeScrollWithRemainder() = runTest {
+ scrimOffset = MAX_SCRIM_OFFSET - 1
+
+ val availableOffset = Offset(x = 0f, y = 2f)
+ val consumableOffset = Offset(x = 0f, y = 1f)
+ val offsetConsumed =
+ scrollConnection.onPostScroll(
+ consumed = Offset.Zero,
+ available = availableOffset,
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(offsetConsumed).isEqualTo(consumableOffset)
+ assertThat(isStarted).isEqualTo(true)
+ }
+
+ @Test
+ fun canStartPostScroll_atMaxOffset_ignoreScroll() = runTest {
+ scrimOffset = MAX_SCRIM_OFFSET
+
+ val offsetConsumed =
+ scrollConnection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(x = 0f, y = 1f),
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+ assertThat(isStarted).isEqualTo(false)
+ }
+
+ @Test
+ fun canStartPostScroll_externalOverscrollGesture_startButIgnoreScroll() = runTest {
+ scrimOffset = MAX_SCRIM_OFFSET
+ isCurrentGestureOverscroll = true
+
+ val offsetConsumed =
+ scrollConnection.onPostScroll(
+ consumed = Offset.Zero,
+ available = Offset(x = 0f, y = 1f),
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(offsetConsumed).isEqualTo(Offset.Zero)
+ assertThat(isStarted).isEqualTo(true)
+ }
+
+ @Test
+ fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest {
+ scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f
+ contentHeight = EXPANDED_CONTENT_HEIGHT
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = -1f),
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(isStarted).isEqualTo(true)
+
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = 1f),
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(isStarted).isEqualTo(true)
+ }
+
+ @Test
+ fun canContinueScroll_atMaxOffset_false() = runTest {
+ scrimOffset = MAX_SCRIM_OFFSET
+ contentHeight = EXPANDED_CONTENT_HEIGHT
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = -1f),
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(isStarted).isEqualTo(true)
+
+ scrollConnection.onPreScroll(
+ available = Offset(x = 0f, y = 1f),
+ source = NestedScrollSource.Drag
+ )
+
+ assertThat(isStarted).isEqualTo(false)
+ }
+
+ companion object {
+ const val MIN_SCRIM_OFFSET = -100f
+ const val MAX_SCRIM_OFFSET = 0f
+
+ const val EXPANDED_CONTENT_HEIGHT = 200f
+ const val COLLAPSED_CONTENT_HEIGHT = 40f
+
+ const val MIN_VISIBLE_SCRIM_HEIGHT = 50f
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index 78b76151e7e6..cbbc4d897048 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,116 +18,92 @@ package com.android.systemui.statusbar.notification.icon.ui.viewmodel
import android.graphics.Rect
import android.graphics.drawable.Icon
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
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.kosmos.testScope
import com.android.systemui.plugins.DarkIconDispatcher
-import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationIconViewStateRepository
-import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.headsUpNotificationIconViewStateRepository
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher
-import com.android.systemui.statusbar.phone.data.repository.FakeDarkIconRepository
-import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
-import com.android.systemui.user.domain.UserDomainLayerModule
+import com.android.systemui.statusbar.phone.data.repository.fakeDarkIconRepository
+import com.android.systemui.statusbar.phone.dozeParameters
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.ui.isAnimating
import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
-
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- BiometricsDomainLayerModule::class,
- UserDomainLayerModule::class,
- ]
- )
- interface TestComponent : SysUITestComponent<NotificationIconContainerStatusBarViewModel> {
-
- val activeNotificationsRepository: ActiveNotificationListRepository
- val darkIconRepository: FakeDarkIconRepository
- val deviceProvisioningRepository: FakeDeviceProvisioningRepository
- val headsUpViewStateRepository: HeadsUpNotificationIconViewStateRepository
- val keyguardTransitionRepository: FakeKeyguardTransitionRepository
- val keyguardRepository: FakeKeyguardRepository
- val powerRepository: FakePowerRepository
- val shadeRepository: FakeShadeRepository
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- mocks: TestMocksModule,
- featureFlags: FakeFeatureFlagsClassicModule,
- ): TestComponent
+@RunWith(ParameterizedAndroidJunit4::class)
+class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterization?) :
+ SysuiTestCase() {
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
}
}
- private val dozeParams: DozeParameters = mock()
-
- private val testComponent: TestComponent =
- DaggerNotificationIconContainerStatusBarViewModelTest_TestComponent.factory()
- .create(
- test = this,
- featureFlags =
- FakeFeatureFlagsClassicModule {
- set(Flags.FULL_SCREEN_USER_SWITCHER, value = false)
- },
- mocks =
- TestMocksModule(
- dozeParameters = dozeParams,
- ),
- )
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val powerRepository = kosmos.fakePowerRepository
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val darkIconRepository = kosmos.fakeDarkIconRepository
+ private val headsUpViewStateRepository = kosmos.headsUpNotificationIconViewStateRepository
+ private val activeNotificationsRepository = kosmos.activeNotificationListRepository
+
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private val dozeParams = kosmos.dozeParameters
+
+ lateinit var underTest: NotificationIconContainerStatusBarViewModel
@Before
fun setup() {
- testComponent.apply {
- keyguardRepository.setKeyguardShowing(false)
- powerRepository.updateWakefulness(
- rawState = WakefulnessState.AWAKE,
- lastWakeReason = WakeSleepReason.OTHER,
- lastSleepReason = WakeSleepReason.OTHER,
- )
- }
+ underTest = kosmos.notificationIconContainerStatusBarViewModel
+ keyguardRepository.setKeyguardShowing(false)
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.OTHER,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
}
@Test
fun animationsEnabled_isFalse_whenDeviceAsleepAndNotPulsing() =
- testComponent.runTest {
+ testScope.runTest {
powerRepository.updateWakefulness(
rawState = WakefulnessState.ASLEEP,
lastWakeReason = WakeSleepReason.POWER_BUTTON,
@@ -150,7 +126,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun animationsEnabled_isTrue_whenDeviceAsleepAndPulsing() =
- testComponent.runTest {
+ testScope.runTest {
powerRepository.updateWakefulness(
rawState = WakefulnessState.ASLEEP,
lastWakeReason = WakeSleepReason.POWER_BUTTON,
@@ -173,7 +149,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun animationsEnabled_isFalse_whenStartingToSleepAndNotControlScreenOff() =
- testComponent.runTest {
+ testScope.runTest {
powerRepository.updateWakefulness(
rawState = WakefulnessState.STARTING_TO_SLEEP,
lastWakeReason = WakeSleepReason.POWER_BUTTON,
@@ -194,7 +170,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun animationsEnabled_isTrue_whenStartingToSleepAndControlScreenOff() =
- testComponent.runTest {
+ testScope.runTest {
val animationsEnabled by collectLastValue(underTest.animationsEnabled)
assertThat(animationsEnabled).isTrue()
@@ -218,7 +194,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun animationsEnabled_isTrue_whenNotAsleep() =
- testComponent.runTest {
+ testScope.runTest {
powerRepository.updateWakefulness(
rawState = WakefulnessState.AWAKE,
lastWakeReason = WakeSleepReason.POWER_BUTTON,
@@ -236,7 +212,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun animationsEnabled_isTrue_whenKeyguardIsNotShowing() =
- testComponent.runTest {
+ testScope.runTest {
val animationsEnabled by collectLastValue(underTest.animationsEnabled)
keyguardTransitionRepository.sendTransitionStep(
@@ -257,7 +233,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun iconColors_testsDarkBounds() =
- testComponent.runTest {
+ testScope.runTest {
darkIconRepository.darkState.value =
SysuiDarkIconDispatcher.DarkChange(
emptyList(),
@@ -280,7 +256,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun iconColors_staticDrawableColor_notInDarkTintArea() =
- testComponent.runTest {
+ testScope.runTest {
darkIconRepository.darkState.value =
SysuiDarkIconDispatcher.DarkChange(
listOf(Rect(0, 0, 5, 5)),
@@ -295,7 +271,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun iconColors_notInDarkTintArea() =
- testComponent.runTest {
+ testScope.runTest {
darkIconRepository.darkState.value =
SysuiDarkIconDispatcher.DarkChange(
listOf(Rect(0, 0, 5, 5)),
@@ -309,9 +285,9 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun isolatedIcon_animateOnAppear_shadeCollapsed() =
- testComponent.runTest {
+ testScope.runTest {
val icon: Icon = mock()
- shadeRepository.setLegacyShadeExpansion(0f)
+ shadeTestUtil.setShadeExpansion(0f)
activeNotificationsRepository.activeNotifications.value =
ActiveNotificationsStore.Builder()
.apply {
@@ -336,9 +312,9 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun isolatedIcon_dontAnimateOnAppear_shadeExpanded() =
- testComponent.runTest {
+ testScope.runTest {
val icon: Icon = mock()
- shadeRepository.setLegacyShadeExpansion(.5f)
+ shadeTestUtil.setShadeExpansion(.5f)
activeNotificationsRepository.activeNotifications.value =
ActiveNotificationsStore.Builder()
.apply {
@@ -363,7 +339,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun isolatedIcon_updateWhenIconDataChanges() =
- testComponent.runTest {
+ testScope.runTest {
val icon: Icon = mock()
val isolatedIcon by collectLastValue(underTest.isolatedIcon)
runCurrent()
@@ -390,7 +366,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() {
@Test
fun isolatedIcon_lastMessageIsFromReply_notNull() =
- testComponent.runTest {
+ testScope.runTest {
val icon: Icon = mock()
headsUpViewStateRepository.isolatedNotification.value = "notif1"
activeNotificationsRepository.activeNotifications.value =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
index 632196ccf66d..2af2602c6f52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
@@ -21,6 +21,7 @@ import android.graphics.drawable.TestStubDrawable
import android.media.AudioDeviceInfo
import android.media.AudioDevicePort
import android.media.AudioManager
+import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.R
@@ -54,6 +55,7 @@ import org.junit.runner.RunWith
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
class AudioOutputInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt
index dc9613904e4e..dddf582908c7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt
@@ -61,6 +61,7 @@ class AncSliceRepositoryTest : SysuiTestCase() {
AncSliceRepositoryImpl(
localMediaRepositoryFactory,
testScope.testScheduler,
+ testScope.testScheduler,
sliceViewManager,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt
new file mode 100644
index 000000000000..9e86cedb6732
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.volume.panel.component.mediaoutput.domain.interactor
+
+import android.media.AudioAttributes
+import android.media.VolumeProvider
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.data.repository.FakeLocalMediaRepository
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.localMediaRepositoryFactory
+import com.android.systemui.volume.localPlaybackInfo
+import com.android.systemui.volume.localPlaybackStateBuilder
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+import com.android.systemui.volume.panel.shared.model.Result
+import com.android.systemui.volume.remoteMediaController
+import com.android.systemui.volume.remotePlaybackInfo
+import com.android.systemui.volume.remotePlaybackStateBuilder
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaOutputInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: MediaOutputInteractor
+
+ @Before
+ fun setUp() =
+ with(kosmos) {
+ localMediaRepositoryFactory.setLocalMediaRepository(
+ "local.test.pkg",
+ FakeLocalMediaRepository().apply {
+ updateCurrentConnectedDevice(
+ mock { whenever(name).thenReturn("local_media_device") }
+ )
+ },
+ )
+ localMediaRepositoryFactory.setLocalMediaRepository(
+ "remote.test.pkg",
+ FakeLocalMediaRepository().apply {
+ updateCurrentConnectedDevice(
+ mock { whenever(name).thenReturn("remote_media_device") }
+ )
+ },
+ )
+
+ underTest = kosmos.mediaOutputInteractor
+ }
+
+ @Test
+ fun noActiveMediaDeviceSessions_nulls() =
+ with(kosmos) {
+ testScope.runTest {
+ mediaControllerRepository.setActiveSessions(emptyList())
+
+ val activeMediaDeviceSessions by
+ collectLastValue(underTest.activeMediaDeviceSessions)
+ runCurrent()
+
+ assertThat(activeMediaDeviceSessions!!.local).isNull()
+ assertThat(activeMediaDeviceSessions!!.remote).isNull()
+ }
+ }
+
+ @Test
+ fun activeMediaDeviceSessions_areParsed() =
+ with(kosmos) {
+ testScope.runTest {
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ val activeMediaDeviceSessions by
+ collectLastValue(underTest.activeMediaDeviceSessions)
+ runCurrent()
+
+ with(activeMediaDeviceSessions!!.local!!) {
+ assertThat(packageName).isEqualTo("local.test.pkg")
+ assertThat(appLabel).isEqualTo("local_media_controller_label")
+ assertThat(canAdjustVolume).isTrue()
+ }
+ with(activeMediaDeviceSessions!!.remote!!) {
+ assertThat(packageName).isEqualTo("remote.test.pkg")
+ assertThat(appLabel).isEqualTo("remote_media_controller_label")
+ assertThat(canAdjustVolume).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun activeMediaDeviceSessions_volumeControlFixed_cantAdjustVolume() =
+ with(kosmos) {
+ testScope.runTest {
+ localPlaybackInfo =
+ MediaController.PlaybackInfo(
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ VolumeProvider.VOLUME_CONTROL_FIXED,
+ 0,
+ 0,
+ AudioAttributes.Builder().build(),
+ "",
+ )
+ remotePlaybackInfo =
+ MediaController.PlaybackInfo(
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ VolumeProvider.VOLUME_CONTROL_FIXED,
+ 0,
+ 0,
+ AudioAttributes.Builder().build(),
+ "",
+ )
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ val activeMediaDeviceSessions by
+ collectLastValue(underTest.activeMediaDeviceSessions)
+ runCurrent()
+
+ assertThat(activeMediaDeviceSessions!!.local!!.canAdjustVolume).isFalse()
+ assertThat(activeMediaDeviceSessions!!.remote!!.canAdjustVolume).isFalse()
+ }
+ }
+
+ @Test
+ fun activeLocalAndRemoteSession_defaultSession_local() =
+ with(kosmos) {
+ testScope.runTest {
+ localPlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f)
+ remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f)
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ val defaultActiveMediaSession by
+ collectLastValue(underTest.defaultActiveMediaSession)
+ val currentDevice by collectLastValue(underTest.currentConnectedDevice)
+ runCurrent()
+
+ with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) {
+ assertThat(packageName).isEqualTo("local.test.pkg")
+ assertThat(appLabel).isEqualTo("local_media_controller_label")
+ assertThat(canAdjustVolume).isTrue()
+ }
+ assertThat(currentDevice!!.name).isEqualTo("local_media_device")
+ }
+ }
+
+ @Test
+ fun activeRemoteSession_defaultSession_remote() =
+ with(kosmos) {
+ testScope.runTest {
+ localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f)
+ remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f)
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ val defaultActiveMediaSession by
+ collectLastValue(underTest.defaultActiveMediaSession)
+ val currentDevice by collectLastValue(underTest.currentConnectedDevice)
+ runCurrent()
+
+ with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) {
+ assertThat(packageName).isEqualTo("remote.test.pkg")
+ assertThat(appLabel).isEqualTo("remote_media_controller_label")
+ assertThat(canAdjustVolume).isTrue()
+ }
+ assertThat(currentDevice!!.name).isEqualTo("remote_media_device")
+ }
+ }
+
+ @Test
+ fun inactiveLocalAndRemoteSession_defaultSession_local() =
+ with(kosmos) {
+ testScope.runTest {
+ localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f)
+ remotePlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f)
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ val defaultActiveMediaSession by
+ collectLastValue(underTest.defaultActiveMediaSession)
+ val currentDevice by collectLastValue(underTest.currentConnectedDevice)
+ runCurrent()
+
+ with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) {
+ assertThat(packageName).isEqualTo("local.test.pkg")
+ assertThat(appLabel).isEqualTo("local_media_controller_label")
+ assertThat(canAdjustVolume).isTrue()
+ }
+ assertThat(currentDevice!!.name).isEqualTo("local_media_device")
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
index 55e46dc1c434..e1be6b008903 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/WMShellTest.kt
@@ -24,16 +24,17 @@ import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.keyguardUpdateMonitor
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
import com.android.systemui.communal.util.fakeCommunalColors
import com.android.systemui.concurrency.fakeExecutor
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.fakeDockManager
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.wakefulnessLifecycle
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.SysUiState
@@ -63,7 +64,6 @@ import com.android.wm.shell.sysui.ShellInterface
import java.util.Optional
import java.util.concurrent.Executor
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -186,29 +186,18 @@ class WMShellTest : SysuiTestCase() {
verify(mRecentTasks).setTransitionBackgroundColor(null)
verify(mRecentTasks, never()).setTransitionBackgroundColor(black)
- setDocked(true)
- // Make communal available
- kosmos.fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
- kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
- kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
-
+ // Transition to occluded from the glanceable hub.
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ testScope
+ )
+ kosmos.setCommunalAvailable(true)
runCurrent()
verify(mRecentTasks).setTransitionBackgroundColor(black)
}
- private fun TestScope.setDocked(docked: Boolean) {
- kosmos.fakeDockManager.setIsDocked(docked)
- val event =
- if (docked) {
- DockManager.STATE_DOCKED
- } else {
- DockManager.STATE_NONE
- }
- kosmos.fakeDockManager.setDockEvent(event)
- runCurrent()
- }
-
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index c9e298934acc..d13c75082790 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -21,11 +21,11 @@ import android.graphics.drawable.Drawable;
import android.metrics.LogMaker;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
-import android.view.View;
import androidx.annotation.Nullable;
import com.android.internal.logging.InstanceId;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.plugins.annotations.DependsOn;
import com.android.systemui.plugins.annotations.ProvidesInterface;
import com.android.systemui.plugins.qs.QSTile.Callback;
@@ -58,23 +58,23 @@ public interface QSTile {
/**
* The tile was clicked.
*
- * @param view The view that was clicked.
+ * @param expandable {@link Expandable} that was clicked.
*/
- void click(@Nullable View view);
+ void click(@Nullable Expandable expandable);
/**
* The tile secondary click was triggered.
*
- * @param view The view that was clicked.
+ * @param expandable {@link Expandable} that was clicked.
*/
- void secondaryClick(@Nullable View view);
+ void secondaryClick(@Nullable Expandable expandable);
/**
* The tile was long clicked.
*
- * @param view The view that was clicked.
+ * @param expandable {@link Expandable} that was clicked.
*/
- void longClick(@Nullable View view);
+ void longClick(@Nullable Expandable expandable);
void userSwitch(int currentUser);
diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml
index a5b44e564157..0a1f2a8f5048 100644
--- a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml
+++ b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml
@@ -16,6 +16,6 @@
<shape xmlns:android = "http://schemas.android.com/apk/res/android">
<size
- android:width = "@dimen/overlay_action_chip_margin_start"
+ android:width = "@dimen/shelf_action_chip_margin_start"
android:height = "0dp"/>
</shape>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay2.xml b/packages/SystemUI/res/layout/clipboard_overlay2.xml
new file mode 100644
index 000000000000..33ad2cd3a30f
--- /dev/null
+++ b/packages/SystemUI/res/layout/clipboard_overlay2.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<com.android.systemui.clipboardoverlay.ClipboardOverlayView
+ 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/clipboard_ui"
+ android:theme="@style/FloatingOverlay"
+ android:alpha="0"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@string/clipboard_overlay_window_name">
+ <FrameLayout
+ android:id="@+id/actions_container_background"
+ android:visibility="gone"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:elevation="4dp"
+ android:background="@drawable/shelf_action_chip_container_background"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@+id/actions_container"
+ app:layout_constraintEnd_toEndOf="@+id/actions_container"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <HorizontalScrollView
+ android:id="@+id/actions_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
+ android:paddingEnd="@dimen/overlay_action_container_padding_end"
+ android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+ android:elevation="4dp"
+ android:scrollbars="none"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintWidth_percent="1.0"
+ app:layout_constraintWidth_max="wrap"
+ app:layout_constraintStart_toEndOf="@+id/preview_border"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
+ <LinearLayout
+ android:id="@+id/actions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/shelf_action_chip_margin_start"
+ android:showDividers="middle"
+ android:divider="@drawable/shelf_action_chip_divider"
+ android:animateLayoutChanges="true">
+ <include layout="@layout/shelf_action_chip"
+ android:id="@+id/share_chip"/>
+ <include layout="@layout/shelf_action_chip"
+ android:id="@+id/remote_copy_chip"/>
+ </LinearLayout>
+ </HorizontalScrollView>
+ <View
+ android:id="@+id/preview_border"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="@dimen/overlay_preview_container_margin"
+ android:layout_marginTop="@dimen/overlay_border_width_neg"
+ android:layout_marginEnd="@dimen/overlay_border_width_neg"
+ android:layout_marginBottom="@dimen/overlay_preview_container_margin"
+ android:elevation="7dp"
+ android:background="@drawable/overlay_border"
+ app:layout_constraintStart_toStartOf="@id/actions_container_background"
+ app:layout_constraintTop_toTopOf="@id/clipboard_preview"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_preview"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/>
+ <FrameLayout
+ android:id="@+id/clipboard_preview"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/overlay_border_width"
+ android:layout_marginBottom="@dimen/overlay_border_width"
+ android:layout_gravity="center"
+ android:elevation="7dp"
+ android:background="@drawable/overlay_preview_background"
+ android:clipChildren="true"
+ android:clipToOutline="true"
+ android:clipToPadding="true"
+ app:layout_constraintStart_toStartOf="@id/preview_border"
+ app:layout_constraintBottom_toBottomOf="@id/preview_border">
+ <TextView android:id="@+id/text_preview"
+ android:textFontWeight="500"
+ android:padding="8dp"
+ android:gravity="center|start"
+ android:ellipsize="end"
+ android:autoSizeTextType="uniform"
+ android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font"
+ android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font"
+ android:textColor="?attr/overlayButtonTextColor"
+ android:textColorLink="?attr/overlayButtonTextColor"
+ android:background="?androidprv:attr/colorAccentSecondary"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="@dimen/clipboard_preview_size"/>
+ <ImageView
+ android:id="@+id/image_preview"
+ android:scaleType="fitCenter"
+ android:adjustViewBounds="true"
+ android:contentDescription="@string/clipboard_image_preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <TextView
+ android:id="@+id/hidden_preview"
+ android:visibility="gone"
+ android:textFontWeight="500"
+ android:padding="8dp"
+ android:gravity="center"
+ android:textSize="14sp"
+ android:textColor="?attr/overlayButtonTextColor"
+ android:background="?androidprv:attr/colorAccentSecondary"
+ android:layout_width="@dimen/clipboard_preview_size"
+ android:layout_height="@dimen/clipboard_preview_size"/>
+ </FrameLayout>
+ <LinearLayout
+ android:id="@+id/minimized_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:elevation="7dp"
+ android:padding="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ android:background="@drawable/clipboard_minimized_background">
+ <ImageView
+ android:src="@drawable/ic_content_paste"
+ android:tint="?attr/overlayButtonTextColor"
+ android:layout_width="24dp"
+ android:layout_height="24dp"/>
+ <ImageView
+ android:src="@*android:drawable/ic_chevron_end"
+ android:tint="?attr/overlayButtonTextColor"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:paddingEnd="-8dp"
+ android:paddingStart="-4dp"/>
+ </LinearLayout>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_content_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:barrierDirection="top"
+ app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/clipboard_content_end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:barrierDirection="end"
+ app:constraint_referenced_ids="clipboard_preview,minimized_preview"/>
+ <FrameLayout
+ android:id="@+id/dismiss_button"
+ android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+ android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+ android:elevation="10dp"
+ android:visibility="gone"
+ android:alpha="0"
+ app:layout_constraintStart_toEndOf="@id/clipboard_content_end"
+ app:layout_constraintEnd_toEndOf="@id/clipboard_content_end"
+ app:layout_constraintTop_toTopOf="@id/clipboard_content_top"
+ app:layout_constraintBottom_toTopOf="@id/clipboard_content_top"
+ android:contentDescription="@string/clipboard_dismiss_description">
+ <ImageView
+ android:id="@+id/dismiss_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/overlay_dismiss_button_margin"
+ android:background="@drawable/circular_background"
+ android:backgroundTint="?androidprv:attr/materialColorPrimaryFixedDim"
+ android:tint="?androidprv:attr/materialColorOnPrimaryFixed"
+ android:padding="4dp"
+ android:src="@drawable/ic_close"/>
+ </FrameLayout>
+</com.android.systemui.clipboardoverlay.ClipboardOverlayView> \ No newline at end of file
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index 2769bea141c9..73812c965a17 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -39,7 +39,7 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Title">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">6dp</item>
<item name="android:textSize">36dp</item>
<item name="android:focusable">true</item>
@@ -47,14 +47,14 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Subtitle">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">6dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="TextAppearance.AuthNonBioCredential.Description">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">6dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml
index 0d46cbcf050e..cde1a1373bed 100644
--- a/packages/SystemUI/res/values-sw600dp-land/styles.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml
@@ -18,7 +18,7 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<style name="TextAppearance.AuthNonBioCredential.Title">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:textSize">36sp</item>
<item name="android:focusable">true</item>
@@ -26,14 +26,14 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Subtitle">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="TextAppearance.AuthNonBioCredential.Description">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml
index 3add566e439c..85e7af6ee1de 100644
--- a/packages/SystemUI/res/values-sw600dp-port/styles.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml
@@ -26,7 +26,7 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Title">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">24dp</item>
<item name="android:textSize">36sp</item>
<item name="android:focusable">true</item>
diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml
index 7cdd07bec443..e75173d152b0 100644
--- a/packages/SystemUI/res/values-sw720dp-land/styles.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml
@@ -18,7 +18,7 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<style name="TextAppearance.AuthNonBioCredential.Title">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:textSize">36sp</item>
<item name="android:focusable">true</item>
@@ -26,14 +26,14 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Subtitle">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="TextAppearance.AuthNonBioCredential.Description">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">16dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml
index 3add566e439c..85e7af6ee1de 100644
--- a/packages/SystemUI/res/values-sw720dp-port/styles.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml
@@ -26,7 +26,7 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Title">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">24dp</item>
<item name="android:textSize">36sp</item>
<item name="android:focusable">true</item>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 6bfd0887c404..2ba72e386cd1 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -635,9 +635,13 @@
58.0001 29.2229,56.9551 26.8945,55.195
</string>
- <!-- The time (in ms) needed to trigger the lock icon view's long-press affordance -->
+ <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance -->
<integer name="config_lockIconLongPress" translatable="false">200</integer>
+ <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance
+ when the device supports an under-display fingerprint sensor -->
+ <integer name="config_udfpsDeviceEntryIconLongPress" translatable="false">100</integer>
+
<!-- package name of a built-in camera app to use to restrict implicit intent resolution
when the double-press power gesture is used. Ignored if empty. -->
<string translatable="false" name="config_cameraGesturePackage"></string>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a7a6d5b2f305..a1daebd7513e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -430,6 +430,7 @@
<dimen name="overlay_button_corner_radius">16dp</dimen>
<!-- Margin between successive chips -->
<dimen name="overlay_action_chip_margin_start">8dp</dimen>
+ <dimen name="shelf_action_chip_margin_start">12dp</dimen>
<dimen name="overlay_action_chip_padding_vertical">12dp</dimen>
<dimen name="overlay_action_chip_icon_size">24sp</dimen>
<!-- Padding on each side of the icon for icon-only chips -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index dcdd4f0f82b1..45bcd829336f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1186,6 +1186,10 @@
<string name="accessibility_content_description_for_communal_hub">Widgets on lock screen</string>
<!-- Label for accessibility action to select a widget in edit mode. [CHAR LIMIT=NONE] -->
<string name="accessibility_action_label_select_widget">select widget</string>
+ <!-- Label for accessibility action to remove a widget in edit mode. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_label_remove_widget">remove widget</string>
+ <!-- Label for accessibility action to place a widget in edit mode after selecting move widget. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_action_label_place_widget">place selected widget</string>
<!-- Related to user switcher --><skip/>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 69de45ea1acf..2c4cdb9ee796 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -175,21 +175,21 @@
</style>
<style name="TextAppearance.AuthCredential.OldTitle">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:paddingTop">12dp</item>
<item name="android:paddingHorizontal">24dp</item>
<item name="android:textSize">24sp</item>
</style>
<style name="TextAppearance.AuthCredential.OldSubtitle">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingHorizontal">24dp</item>
<item name="android:textSize">16sp</item>
</style>
<style name="TextAppearance.AuthCredential.OldDescription">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingHorizontal">24dp</item>
<item name="android:textSize">14sp</item>
@@ -205,7 +205,7 @@
</style>
<style name="TextAppearance.AuthCredential.Title" parent="TextAppearance.Material3.HeadlineSmall" >
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
@@ -257,7 +257,7 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Title">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">24dp</item>
<item name="android:textSize">36dp</item>
<item name="android:focusable">true</item>
@@ -265,14 +265,14 @@
</style>
<style name="TextAppearance.AuthNonBioCredential.Subtitle">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">20dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="TextAppearance.AuthNonBioCredential.Description">
- <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">20dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index c08b0837da8e..69aa90996946 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -77,7 +77,7 @@ public class QuickStepContract {
// settings is expanded.
public static final int SYSUI_STATE_QUICK_SETTINGS_EXPANDED = 1 << 11;
// Winscope tracing is enabled
- public static final int SYSUI_STATE_TRACING_ENABLED = 1 << 12;
+ public static final int SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION = 1 << 12;
// The Assistant gesture should be constrained. It is up to the launcher implementation to
// decide how to constrain it
public static final int SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED = 1 << 13;
@@ -148,7 +148,7 @@ public class QuickStepContract {
SYSUI_STATE_OVERVIEW_DISABLED,
SYSUI_STATE_HOME_DISABLED,
SYSUI_STATE_SEARCH_DISABLED,
- SYSUI_STATE_TRACING_ENABLED,
+ SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION,
SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED,
SYSUI_STATE_BUBBLES_EXPANDED,
SYSUI_STATE_DIALOG_SHOWING,
@@ -211,8 +211,8 @@ public class QuickStepContract {
if ((flags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0) {
str.add("a11y_long_click");
}
- if ((flags & SYSUI_STATE_TRACING_ENABLED) != 0) {
- str.add("tracing");
+ if ((flags & SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION) != 0) {
+ str.add("disable_gesture_split_invocation");
}
if ((flags & SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED) != 0) {
str.add("asst_gesture_constrain");
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index c613afbda5b8..473719fa76df 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -141,6 +141,7 @@ public class TaskStackChangeListeners {
private static final int ON_TASK_DESCRIPTION_CHANGED = 21;
private static final int ON_ACTIVITY_ROTATION = 22;
private static final int ON_LOCK_TASK_MODE_CHANGED = 23;
+ private static final int ON_TASK_SNAPSHOT_INVALIDATED = 24;
/**
* List of {@link TaskStackChangeListener} registered from {@link #addListener}.
@@ -272,6 +273,12 @@ public class TaskStackChangeListeners {
}
@Override
+ public void onTaskSnapshotInvalidated(int taskId) {
+ mHandler.obtainMessage(ON_TASK_SNAPSHOT_INVALIDATED, taskId, 0 /* unused */)
+ .sendToTarget();
+ }
+
+ @Override
public void onTaskCreated(int taskId, ComponentName componentName) {
mHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget();
}
@@ -496,6 +503,15 @@ public class TaskStackChangeListeners {
}
break;
}
+ case ON_TASK_SNAPSHOT_INVALIDATED: {
+ Trace.beginSection("onTaskSnapshotInvalidated");
+ final ThumbnailData thumbnail = new ThumbnailData();
+ for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
+ mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, thumbnail);
+ }
+ Trace.endSection();
+ break;
+ }
}
}
if (msg.obj instanceof SomeArgs) {
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index 57c1fd091ae9..42896a419658 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -569,6 +569,11 @@ public class ExpandHelper implements Gefingerpoken {
return true;
}
+ /** Finish the current expand motion without accounting for velocity. */
+ public void finishExpanding() {
+ finishExpanding(false, 0);
+ }
+
/**
* Finish the current expand motion
* @param forceAbort whether the expansion should be forcefully aborted and returned to the old
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
index 35f9344ae897..004d5dba64c7 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
@@ -22,6 +22,8 @@ import com.android.systemui.accessibility.data.repository.ColorCorrectionReposit
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorInversionRepository
import com.android.systemui.accessibility.data.repository.ColorInversionRepositoryImpl
+import com.android.systemui.accessibility.data.repository.OneHandedModeRepository
+import com.android.systemui.accessibility.data.repository.OneHandedModeRepositoryImpl
import com.android.systemui.accessibility.qs.QSAccessibilityModule
import dagger.Binds
import dagger.Module
@@ -34,6 +36,8 @@ interface AccessibilityModule {
@Binds
fun colorInversionRepository(impl: ColorInversionRepositoryImpl): ColorInversionRepository
+ @Binds fun oneHandedModeRepository(impl: OneHandedModeRepositoryImpl): OneHandedModeRepository
+
@Binds
fun accessibilityQsShortcutsRepository(
impl: AccessibilityQsShortcutsRepositoryImpl
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepository.kt
new file mode 100644
index 000000000000..d921025ba151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepository.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/** Provides data related to one handed mode. */
+interface OneHandedModeRepository {
+ /** Observable for whether one handed mode is enabled */
+ fun isEnabled(userHandle: UserHandle): Flow<Boolean>
+
+ /** Sets one handed mode enabled state. */
+ suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean
+}
+
+@SysUISingleton
+class OneHandedModeRepositoryImpl
+@Inject
+constructor(
+ @Background private val bgCoroutineContext: CoroutineContext,
+ @Application private val scope: CoroutineScope,
+ private val secureSettings: SecureSettings,
+) : OneHandedModeRepository {
+
+ private val userMap = mutableMapOf<Int, Flow<Boolean>>()
+
+ override fun isEnabled(userHandle: UserHandle): Flow<Boolean> =
+ userMap.getOrPut(userHandle.identifier) {
+ secureSettings
+ .observerFlow(userHandle.identifier, SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ secureSettings.getIntForUser(SETTING_NAME, DISABLED, userHandle.identifier) ==
+ ENABLED
+ }
+ .distinctUntilChanged()
+ .flowOn(bgCoroutineContext)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_VALUE)
+ }
+
+ override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean =
+ withContext(bgCoroutineContext) {
+ secureSettings.putIntForUser(
+ SETTING_NAME,
+ if (isEnabled) ENABLED else DISABLED,
+ userHandle.identifier
+ )
+ }
+
+ companion object {
+ private const val SETTING_NAME = Settings.Secure.ONE_HANDED_MODE_ENABLED
+ private const val DISABLED = 0
+ private const val ENABLED = 1
+ private const val DEFAULT_VALUE = false
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
index 623b40f144eb..14e5f3422a27 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
@@ -18,7 +18,6 @@ package com.android.systemui.accessibility.hearingaid;
import android.bluetooth.BluetoothDevice;
import android.util.Log;
-import android.view.View;
import androidx.annotation.Nullable;
@@ -26,6 +25,7 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -58,9 +58,9 @@ public class HearingDevicesDialogManager {
/**
* Shows the dialog.
*
- * @param view The view from which the dialog is shown.
+ * @param expandable {@link Expandable} from which the dialog is shown.
*/
- public void showDialog(View view) {
+ public void showDialog(Expandable expandable) {
if (mDialog != null) {
if (DEBUG) {
Log.d(TAG, "HearingDevicesDialog already showing. Destroy it first.");
@@ -70,13 +70,17 @@ public class HearingDevicesDialogManager {
mDialog = mDialogFactory.create(!isAnyBondedHearingDevice()).createDialog();
- if (view != null) {
- mDialogTransitionAnimator.showFromView(mDialog, view,
+ if (expandable != null) {
+ DialogTransitionAnimator.Controller controller = expandable.dialogTransitionController(
new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG), /* animateBackgroundBoundsChange= */ true);
- } else {
- mDialog.show();
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(mDialog,
+ controller, /* animateBackgroundBoundsChange= */ true);
+ return;
+ }
}
+ mDialog.show();
}
private void destroyDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 99be7628b3c6..54dd6d00aa48 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -41,6 +41,10 @@ import com.android.systemui.qs.tiles.impl.inversion.domain.ColorInversionTileMap
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileDataInteractor
+import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
+import com.android.systemui.qs.tiles.impl.onehanded.ui.OneHandedModeTileMapper
import com.android.systemui.qs.tiles.impl.reducebrightness.domain.interactor.ReduceBrightColorsTileDataInteractor
import com.android.systemui.qs.tiles.impl.reducebrightness.domain.interactor.ReduceBrightColorsTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.reducebrightness.domain.model.ReduceBrightColorsTileModel
@@ -256,5 +260,24 @@ interface QSAccessibilityModule {
),
instanceId = uiEventLogger.getNewInstanceId(),
)
+
+ /** Inject One Handed Mode Tile into tileViewModelMap in QSModule. */
+ @Provides
+ @IntoMap
+ @StringKey(ONE_HANDED_TILE_SPEC)
+ fun provideOneHandedModeTileViewModel(
+ factory: QSTileViewModelFactory.Static<OneHandedModeTileModel>,
+ mapper: OneHandedModeTileMapper,
+ stateInteractor: OneHandedModeTileDataInteractor,
+ userActionInteractor: OneHandedModeTileUserActionInteractor
+ ): QSTileViewModel =
+ if (Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(ONE_HANDED_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index eb919e3ca36b..4369f3f64613 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -32,6 +32,7 @@ import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.systemui.Prefs
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
@@ -82,7 +83,7 @@ constructor(
* @param view The view from which the dialog is shown.
*/
@kotlinx.coroutines.ExperimentalCoroutinesApi
- fun showDialog(view: View?) {
+ fun showDialog(expandable: Expandable?) {
cancelJob()
job =
@@ -93,17 +94,15 @@ constructor(
val dialog = dialogDelegate.createDialog()
val context = dialog.context
- view?.let {
- dialogTransitionAnimator.showFromView(
- dialog,
- it,
- animateBackgroundBoundsChange = true,
- cuj =
- DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
- )
+ val controller =
+ expandable?.dialogTransitionController(
+ DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG
+ )
)
+ controller?.let {
+ dialogTransitionAnimator.show(dialog, it, animateBackgroundBoundsChange = true)
}
?: dialog.show()
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
index 053482431bcb..f1c3f949ffba 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepository.kt
@@ -323,6 +323,9 @@ constructor(
alternateBouncerUIAvailable
.logDiffsForTable(buffer, "", "IsAlternateBouncerUIAvailable", false)
.launchIn(applicationScope)
+ alternateBouncerVisible
+ .logDiffsForTable(buffer, "", "AlternateBouncerVisible", false)
+ .launchIn(applicationScope)
lastShownSecurityMode
.map { it.name }
.logDiffsForTable(buffer, "", "lastShownSecurityMode", null)
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
index b2699673f7ea..8efc66de24cd 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java
@@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static com.android.systemui.Flags.screenshotShelfUi2;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -25,6 +27,7 @@ import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
+import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
import android.content.res.Resources;
@@ -36,6 +39,7 @@ import android.graphics.Region;
import android.graphics.drawable.Icon;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
+import android.util.Log;
import android.util.MathUtils;
import android.util.TypedValue;
import android.view.DisplayCutout;
@@ -58,9 +62,15 @@ import com.android.systemui.res.R;
import com.android.systemui.screenshot.DraggableConstraintLayout;
import com.android.systemui.screenshot.FloatingWindowUtil;
import com.android.systemui.screenshot.OverlayActionChip;
+import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder;
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance;
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel;
import java.util.ArrayList;
+import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
+
/**
* Handles the visual elements and animations for the clipboard overlay.
*/
@@ -85,7 +95,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
private final DisplayMetrics mDisplayMetrics;
private final AccessibilityManager mAccessibilityManager;
- private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>();
+ private final ArrayList<View> mActionChips = new ArrayList<>();
private View mClipboardPreview;
private ImageView mImagePreview;
@@ -93,11 +103,12 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
private TextView mHiddenPreview;
private LinearLayout mMinimizedPreview;
private View mPreviewBorder;
- private OverlayActionChip mShareChip;
- private OverlayActionChip mRemoteCopyChip;
+ private View mShareChip;
+ private View mRemoteCopyChip;
private View mActionContainerBackground;
private View mDismissButton;
private LinearLayout mActionContainer;
+ private ClipboardOverlayCallbacks mClipboardCallbacks;
public ClipboardOverlayView(Context context) {
this(context, null);
@@ -128,17 +139,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
mRemoteCopyChip = requireViewById(R.id.remote_copy_chip);
mDismissButton = requireViewById(R.id.dismiss_button);
- mShareChip.setAlpha(1);
- mRemoteCopyChip.setAlpha(1);
- mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
-
- mRemoteCopyChip.setIcon(
- Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true);
- mShareChip.setIcon(
- Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
-
- mRemoteCopyChip.setContentDescription(
- mContext.getString(R.string.clipboard_send_nearby_description));
+ bindDefaultActionChips();
mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> {
int availableHeight = mTextPreview.getHeight()
@@ -149,15 +150,68 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
super.onFinishInflate();
}
+ private void bindDefaultActionChips() {
+ if (screenshotShelfUi2()) {
+ ActionButtonViewBinder.INSTANCE.bind(mRemoteCopyChip,
+ ActionButtonViewModel.Companion.withNextId(
+ new ActionButtonAppearance(
+ Icon.createWithResource(mContext,
+ R.drawable.ic_baseline_devices_24).loadDrawable(
+ mContext),
+ null,
+ mContext.getString(R.string.clipboard_send_nearby_description)),
+ new Function0<>() {
+ @Override
+ public Unit invoke() {
+ if (mClipboardCallbacks != null) {
+ mClipboardCallbacks.onRemoteCopyButtonTapped();
+ }
+ return null;
+ }
+ }));
+ ActionButtonViewBinder.INSTANCE.bind(mShareChip,
+ ActionButtonViewModel.Companion.withNextId(
+ new ActionButtonAppearance(
+ Icon.createWithResource(mContext,
+ R.drawable.ic_screenshot_share).loadDrawable(mContext),
+ null, mContext.getString(com.android.internal.R.string.share)),
+ new Function0<>() {
+ @Override
+ public Unit invoke() {
+ if (mClipboardCallbacks != null) {
+ mClipboardCallbacks.onShareButtonTapped();
+ }
+ return null;
+ }
+ }));
+ } else {
+ mShareChip.setAlpha(1);
+ mRemoteCopyChip.setAlpha(1);
+
+ ((ImageView) mRemoteCopyChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24));
+ ((ImageView) mShareChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon(
+ Icon.createWithResource(mContext, R.drawable.ic_screenshot_share));
+
+ mShareChip.setContentDescription(
+ mContext.getString(com.android.internal.R.string.share));
+ mRemoteCopyChip.setContentDescription(
+ mContext.getString(R.string.clipboard_send_nearby_description));
+ }
+ }
+
@Override
public void setCallbacks(SwipeDismissCallbacks callbacks) {
super.setCallbacks(callbacks);
ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks;
- mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
+ if (!screenshotShelfUi2()) {
+ mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped());
+ mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
+ }
mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped());
- mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped());
mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped());
mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped());
+ mClipboardCallbacks = clipboardCallbacks;
}
void setEditAccessibilityAction(boolean editable) {
@@ -285,7 +339,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
}
void resetActionChips() {
- for (OverlayActionChip chip : mActionChips) {
+ for (View chip : mActionChips) {
mActionContainer.removeView(chip);
}
mActionChips.clear();
@@ -437,7 +491,12 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
void setActionChip(RemoteAction action, Runnable onFinish) {
mActionContainerBackground.setVisibility(View.VISIBLE);
- OverlayActionChip chip = constructActionChip(action, onFinish);
+ View chip;
+ if (screenshotShelfUi2()) {
+ chip = constructShelfActionChip(action, onFinish);
+ } else {
+ chip = constructActionChip(action, onFinish);
+ }
mActionContainer.addView(chip);
mActionChips.add(chip);
}
@@ -450,6 +509,27 @@ public class ClipboardOverlayView extends DraggableConstraintLayout {
v.setVisibility(View.VISIBLE);
}
+ private View constructShelfActionChip(RemoteAction action, Runnable onFinish) {
+ View chip = LayoutInflater.from(mContext).inflate(
+ R.layout.shelf_action_chip, mActionContainer, false);
+ ActionButtonViewBinder.INSTANCE.bind(chip, ActionButtonViewModel.Companion.withNextId(
+ new ActionButtonAppearance(action.getIcon().loadDrawable(mContext),
+ action.getTitle(), action.getTitle()), new Function0<>() {
+ @Override
+ public Unit invoke() {
+ try {
+ action.getActionIntent().send();
+ onFinish.run();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Failed to send intent");
+ }
+ return null;
+ }
+ }));
+
+ return chip;
+ }
+
private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) {
OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate(
R.layout.overlay_action_chip, mActionContainer, false);
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
index ff9fba4c03f1..740a93eb081c 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java
@@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay.dagger;
import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
+import static com.android.systemui.Flags.screenshotShelfUi2;
+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import android.content.Context;
@@ -57,8 +59,13 @@ public interface ClipboardOverlayModule {
*/
@Provides
static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) {
- return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
- R.layout.clipboard_overlay, null);
+ if (screenshotShelfUi2()) {
+ return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+ R.layout.clipboard_overlay2, null);
+ } else {
+ return (ClipboardOverlayView) LayoutInflater.from(context).inflate(
+ R.layout.clipboard_overlay, null);
+ }
}
@Qualifier
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
index 07814512b4b8..85e2bdb43ba5 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt
@@ -37,7 +37,7 @@ import kotlinx.coroutines.DisposableHandle
class LongPressHandlingView(
context: Context,
attrs: AttributeSet?,
- private val longPressDuration: () -> Long,
+ longPressDuration: () -> Long,
) :
View(
context,
@@ -89,6 +89,12 @@ class LongPressHandlingView(
)
}
+ var longPressDuration: () -> Long
+ get() = interactionHandler.longPressDuration
+ set(longPressDuration) {
+ interactionHandler.longPressDuration = longPressDuration
+ }
+
fun setLongPressHandlingEnabled(isEnabled: Boolean) {
interactionHandler.isLongPressHandlingEnabled = isEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
index a742e8d614b1..d3fc610bc52e 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt
@@ -34,7 +34,7 @@ class LongPressHandlingViewInteractionHandler(
/** Callback reporting the a single tap gesture was detected at the given coordinates. */
private val onSingleTapDetected: () -> Unit,
/** Time for the touch to be considered a long-press in ms */
- private val longPressDuration: () -> Long,
+ var longPressDuration: () -> Long,
) {
sealed class MotionEventModel {
object Other : MotionEventModel()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 5cf68b756664..0042915d0cd9 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -44,9 +44,10 @@ import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dock.DockManager
-import com.android.systemui.dock.retrieveIsDocked
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
@@ -64,6 +65,7 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import com.android.systemui.util.kotlin.emitOnStart
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
@@ -77,9 +79,11 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
@@ -92,6 +96,7 @@ class CommunalInteractor
@Inject
constructor(
@Application val applicationScope: CoroutineScope,
+ @Background val bgDispatcher: CoroutineDispatcher,
broadcastDispatcher: BroadcastDispatcher,
private val communalRepository: CommunalRepository,
private val widgetRepository: CommunalWidgetRepository,
@@ -99,13 +104,13 @@ constructor(
mediaRepository: CommunalMediaRepository,
smartspaceRepository: SmartspaceRepository,
keyguardInteractor: KeyguardInteractor,
- private val communalSettingsInteractor: CommunalSettingsInteractor,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ communalSettingsInteractor: CommunalSettingsInteractor,
private val appWidgetHost: CommunalAppWidgetHost,
private val editWidgetsActivityStarter: EditWidgetsActivityStarter,
private val userTracker: UserTracker,
private val activityStarter: ActivityStarter,
private val userManager: UserManager,
- private val dockManager: DockManager,
sceneInteractor: SceneInteractor,
@CommunalLog logBuffer: LogBuffer,
@CommunalTableLog tableLogBuffer: TableLogBuffer,
@@ -145,8 +150,18 @@ constructor(
replay = 1,
)
- /** Whether to show communal by default */
- val showByDefault: Flow<Boolean> = and(isCommunalAvailable, dockManager.retrieveIsDocked())
+ /** Whether to show communal when exiting the occluded state. */
+ val showCommunalFromOccluded: Flow<Boolean> =
+ keyguardTransitionInteractor.startedKeyguardTransitionStep
+ .filter { step -> step.to == KeyguardState.OCCLUDED }
+ .combine(isCommunalAvailable, ::Pair)
+ .map { (step, available) -> available && step.from == KeyguardState.GLANCEABLE_HUB }
+ .flowOn(bgDispatcher)
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
/**
* Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
index 1bee83b41dbe..337d8735ea53 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
@@ -63,8 +63,8 @@ constructor(
)
.distinctUntilChanged()
- /** Whether to show communal by default */
- val showByDefault: Flow<Boolean> = communalInteractor.showByDefault
+ /** Whether to show communal when exiting the occluded state. */
+ val showCommunalFromOccluded: Flow<Boolean> = communalInteractor.showCommunalFromOccluded
val transitionFromOccludedEnded =
keyguardTransitionInteractor.transitionStepsFromState(KeyguardState.OCCLUDED).filter { step
@@ -74,8 +74,11 @@ constructor(
}
val recentsBackgroundColor: Flow<Color?> =
- combine(showByDefault, communalColors.backgroundColor) { showByDefault, backgroundColor ->
- if (showByDefault) {
+ combine(showCommunalFromOccluded, communalColors.backgroundColor) {
+ showCommunalFromOccluded,
+ backgroundColor,
+ ->
+ if (showCommunalFromOccluded) {
backgroundColor
} else {
null
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
index ba45a51ad9a3..30a56a21e322 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -46,7 +46,6 @@ import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthR
import com.android.systemui.keyguard.data.repository.FaceAuthTableLog
import com.android.systemui.keyguard.data.repository.FaceDetectTableLog
import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -64,6 +63,7 @@ import com.android.systemui.user.data.repository.UserRepository
import com.google.errorprone.annotations.CompileTimeConstant
import java.io.PrintWriter
import java.util.Arrays
+import java.util.concurrent.Executor
import java.util.stream.Collectors
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -150,12 +150,12 @@ constructor(
@Application private val applicationScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundDispatcher: CoroutineDispatcher,
+ @Background private val backgroundExecutor: Executor,
private val sessionTracker: SessionTracker,
private val uiEventsLogger: UiEventLogger,
private val faceAuthLogger: FaceAuthenticationLogger,
private val biometricSettingsRepository: BiometricSettingsRepository,
private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
- trustRepository: TrustRepository,
private val keyguardRepository: KeyguardRepository,
private val powerInteractor: PowerInteractor,
private val keyguardInteractor: KeyguardInteractor,
@@ -235,7 +235,10 @@ constructor(
}
init {
- faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
+ backgroundExecutor.execute {
+ faceManager?.addLockoutResetCallback(faceLockoutResetCallback)
+ faceAuthLogger.addLockoutResetCallbackDone()
+ }
faceAcquiredInfoIgnoreList =
Arrays.stream(
context.resources.getIntArray(
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 662974dd2c91..d079a954cb57 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -240,6 +240,15 @@ constructor(
}
/**
+ * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has
+ * chosen any secure authentication method and even if they set the lockscreen to be dismissed
+ * when the user swipes on it.
+ */
+ suspend fun isLockscreenEnabled(): Boolean {
+ return repository.isLockscreenEnabled()
+ }
+
+ /**
* Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
* dismissed once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
index c464ed1d29bb..4875f481cce6 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
@@ -30,6 +30,7 @@ import com.android.app.tracing.coroutines.launch
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.qs.tileimpl.QSTileViewImpl
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.filterNotNull
object QSLongPressEffectViewBinder {
@@ -49,64 +50,56 @@ object QSLongPressEffectViewBinder {
launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#action" }) {
var effectAnimator: ValueAnimator? = null
- qsLongPressEffect.actionType.collect { action ->
- action?.let {
- when (it) {
- QSLongPressEffect.ActionType.CLICK -> {
- tile.performClick()
- qsLongPressEffect.clearActionType()
- }
- QSLongPressEffect.ActionType.LONG_PRESS -> {
- tile.prepareForLaunch()
- tile.performLongClick()
- qsLongPressEffect.clearActionType()
- }
- QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> {
- tile.resetLongPressEffectProperties()
- tile.performLongClick()
- qsLongPressEffect.clearActionType()
- }
- QSLongPressEffect.ActionType.START_ANIMATOR -> {
- if (effectAnimator?.isRunning != true) {
- effectAnimator =
- ValueAnimator.ofFloat(0f, 1f).apply {
- this.duration =
- qsLongPressEffect.effectDuration.toLong()
- interpolator = AccelerateDecelerateInterpolator()
+ qsLongPressEffect.actionType.filterNotNull().collect { action ->
+ when (action) {
+ QSLongPressEffect.ActionType.CLICK -> {
+ tile.performClick()
+ qsLongPressEffect.clearActionType()
+ }
+ QSLongPressEffect.ActionType.LONG_PRESS -> {
+ tile.prepareForLaunch()
+ tile.performLongClick()
+ qsLongPressEffect.clearActionType()
+ }
+ QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> {
+ tile.resetLongPressEffectProperties()
+ tile.performLongClick()
+ qsLongPressEffect.clearActionType()
+ }
+ QSLongPressEffect.ActionType.START_ANIMATOR -> {
+ if (effectAnimator?.isRunning != true) {
+ effectAnimator =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ this.duration =
+ qsLongPressEffect.effectDuration.toLong()
+ interpolator = AccelerateDecelerateInterpolator()
- doOnStart {
- qsLongPressEffect.handleAnimationStart()
+ doOnStart { qsLongPressEffect.handleAnimationStart() }
+ addUpdateListener {
+ val value = animatedValue as Float
+ if (value == 0f) {
+ tile.bringToFront()
+ } else {
+ tile.updateLongPressEffectProperties(value)
}
- addUpdateListener {
- val value = animatedValue as Float
- if (value == 0f) {
- tile.bringToFront()
- } else {
- tile.updateLongPressEffectProperties(value)
- }
- }
- doOnEnd {
- qsLongPressEffect.handleAnimationComplete()
- }
- doOnCancel {
- qsLongPressEffect.handleAnimationCancel()
- }
- start()
}
- }
+ doOnEnd { qsLongPressEffect.handleAnimationComplete() }
+ doOnCancel { qsLongPressEffect.handleAnimationCancel() }
+ start()
+ }
}
- QSLongPressEffect.ActionType.REVERSE_ANIMATOR -> {
- effectAnimator?.let {
- val pausedProgress = it.animatedFraction
- qsLongPressEffect.playReverseHaptics(pausedProgress)
- it.reverse()
- }
- }
- QSLongPressEffect.ActionType.CANCEL_ANIMATOR -> {
- tile.resetLongPressEffectProperties()
- effectAnimator?.cancel()
+ }
+ QSLongPressEffect.ActionType.REVERSE_ANIMATOR -> {
+ effectAnimator?.let {
+ val pausedProgress = it.animatedFraction
+ qsLongPressEffect.playReverseHaptics(pausedProgress)
+ it.reverse()
}
}
+ QSLongPressEffect.ActionType.CANCEL_ANIMATOR -> {
+ tile.resetLongPressEffectProperties()
+ effectAnimator?.cancel()
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
index 00ec1a14bb93..44e795c3d22a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
@@ -187,18 +187,15 @@ public class KeyguardIndicationRotateTextViewController extends
return;
}
- // current indication is updated to empty
+ // Current indication is updated to empty.
+ // Update to empty even if `currMsgShownForMinTime` is false.
if (mCurrIndicationType == type
&& !hasNewIndication
&& showAsap) {
- if (currMsgShownForMinTime) {
- if (mShowNextIndicationRunnable != null) {
- mShowNextIndicationRunnable.runImmediately();
- } else {
- showIndication(INDICATION_TYPE_NONE);
- }
+ if (mShowNextIndicationRunnable != null) {
+ mShowNextIndicationRunnable.runImmediately();
} else {
- scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch);
+ showIndication(INDICATION_TYPE_NONE);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 6ef39f3ef40e..d6fd354d8544 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -177,10 +177,6 @@ import com.android.systemui.util.time.SystemClock;
import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
import com.android.wm.shell.keyguard.KeyguardTransitions;
-import dagger.Lazy;
-
-import kotlinx.coroutines.CoroutineDispatcher;
-
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -190,6 +186,9 @@ import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
+import dagger.Lazy;
+import kotlinx.coroutines.CoroutineDispatcher;
+
/**
* Mediates requests related to the keyguard. This includes queries about the
* state of the keyguard, power management events that effect whether the keyguard
@@ -1234,7 +1233,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mUnoccludeAnimator.cancel();
}
- if (isDream || mShowCommunalByDefault) {
+ if (isDream || mShowCommunalWhenUnoccluding) {
initAlphaForAnimationTargets(wallpapers);
if (isDream) {
mDreamViewModel.get().startTransitionFromDream();
@@ -1372,7 +1371,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private final Lazy<DreamViewModel> mDreamViewModel;
private final Lazy<CommunalTransitionViewModel> mCommunalTransitionViewModel;
private RemoteAnimationTarget mRemoteAnimationTarget;
- private boolean mShowCommunalByDefault = false;
+ private boolean mShowCommunalWhenUnoccluding = false;
private final Lazy<WindowManagerLockscreenVisibilityManager> mWmLockscreenVisibilityManager;
@@ -1630,8 +1629,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
getRemoteSurfaceAlphaApplier());
mJavaAdapter.alwaysCollectFlow(dreamViewModel.getTransitionEnded(),
getFinishedCallbackConsumer());
- mJavaAdapter.alwaysCollectFlow(communalViewModel.getShowByDefault(),
- (showByDefault) -> mShowCommunalByDefault = showByDefault);
+ mJavaAdapter.alwaysCollectFlow(communalViewModel.getShowCommunalFromOccluded(),
+ (showCommunalFromOccluded) -> {
+ mShowCommunalWhenUnoccluding = showCommunalFromOccluded;
+ });
mJavaAdapter.alwaysCollectFlow(communalViewModel.getTransitionFromOccludedEnded(),
getFinishedCallbackConsumer());
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 4c54bfd3a17c..e32bfcf81fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -89,6 +89,12 @@ interface KeyguardTransitionRepository {
suspend fun startTransition(info: TransitionInfo): UUID?
/**
+ * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to
+ * seed the repository with the appropriate initial state.
+ */
+ suspend fun emitInitialStepsFromOff(to: KeyguardState)
+
+ /**
* Allows manual control of a transition. When calling [startTransition], the consumer must pass
* in a null animator. In return, it will get a unique [UUID] that will be validated to allow
* further updates.
@@ -141,9 +147,17 @@ constructor(
private var updateTransitionId: UUID? = null
init {
- // Seed with transitions signaling a boot into lockscreen state. If updating this, please
- // also update FakeKeyguardTransitionRepository.
- initialTransitionSteps.forEach(::emitTransition)
+ // Start with a FINISHED transition in OFF. KeyguardBootInteractor will transition from OFF
+ // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should
+ // start in.
+ emitTransition(
+ TransitionStep(
+ KeyguardState.OFF,
+ KeyguardState.OFF,
+ 1f,
+ TransitionState.FINISHED,
+ )
+ )
}
override suspend fun startTransition(info: TransitionInfo): UUID? {
@@ -251,6 +265,28 @@ constructor(
lastStep = nextStep
}
+ override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+ emitTransition(
+ TransitionStep(
+ KeyguardState.OFF,
+ to,
+ 0f,
+ TransitionState.STARTED,
+ ownerName = "KeyguardTransitionRepository(boot)",
+ )
+ )
+
+ emitTransition(
+ TransitionStep(
+ KeyguardState.OFF,
+ to,
+ 1f,
+ TransitionState.FINISHED,
+ ownerName = "KeyguardTransitionRepository(boot)",
+ ),
+ )
+ }
+
private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
if (step.transitionState == TransitionState.RUNNING) {
return
@@ -271,31 +307,5 @@ constructor(
companion object {
private const val TAG = "KeyguardTransitionRepository"
-
- /**
- * Transition steps to seed the repository with, so that all of the transition interactor
- * flows emit reasonable initial values.
- */
- val initialTransitionSteps: List<TransitionStep> =
- listOf(
- TransitionStep(
- KeyguardState.OFF,
- KeyguardState.OFF,
- 1f,
- TransitionState.FINISHED,
- ),
- TransitionStep(
- KeyguardState.OFF,
- KeyguardState.LOCKSCREEN,
- 0f,
- TransitionState.STARTED,
- ),
- TransitionStep(
- KeyguardState.OFF,
- KeyguardState.LOCKSCREEN,
- 1f,
- TransitionState.FINISHED,
- ),
- )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 2eeb3b947a11..115fc3610ac8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -66,7 +66,7 @@ constructor(
listenForTransitionToCamera(scope, keyguardInteractor)
}
- private val canDismissLockScreen: Flow<Boolean> =
+ private val canTransitionToGoneOnWake: Flow<Boolean> =
combine(
keyguardInteractor.isKeyguardShowing,
keyguardInteractor.isKeyguardDismissible,
@@ -87,7 +87,7 @@ constructor(
keyguardInteractor.biometricUnlockState,
keyguardInteractor.isKeyguardOccluded,
communalInteractor.isIdleOnCommunal,
- canDismissLockScreen,
+ canTransitionToGoneOnWake,
keyguardInteractor.primaryBouncerShowing,
)
.collect {
@@ -96,12 +96,12 @@ constructor(
biometricUnlockState,
occluded,
isIdleOnCommunal,
- canDismissLockScreen,
+ canTransitionToGoneOnWake,
primaryBouncerShowing) ->
startTransitionTo(
if (isWakeAndUnlock(biometricUnlockState.mode)) {
KeyguardState.GONE
- } else if (canDismissLockScreen) {
+ } else if (canTransitionToGoneOnWake) {
KeyguardState.GONE
} else if (primaryBouncerShowing) {
KeyguardState.PRIMARY_BOUNCER
@@ -129,7 +129,7 @@ constructor(
.sample(
communalInteractor.isIdleOnCommunal,
keyguardInteractor.biometricUnlockState,
- canDismissLockScreen,
+ canTransitionToGoneOnWake,
keyguardInteractor.primaryBouncerShowing,
)
.collect {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index ee589f4a235c..e51ba8308a08 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -87,12 +87,15 @@ constructor(
scope.launch {
keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop
.filterRelevantKeyguardStateAnd { onTop -> !onTop }
- .sample(communalInteractor.isIdleOnCommunal, communalInteractor.showByDefault)
- .collect { (_, isIdleOnCommunal, showCommunalByDefault) ->
+ .sample(
+ communalInteractor.isIdleOnCommunal,
+ communalInteractor.showCommunalFromOccluded,
+ )
+ .collect { (_, isIdleOnCommunal, showCommunalFromOccluded) ->
// Occlusion signals come from the framework, and should interrupt any
// existing transition
val to =
- if (isIdleOnCommunal || showCommunalByDefault) {
+ if (isIdleOnCommunal || showCommunalFromOccluded) {
KeyguardState.GLANCEABLE_HUB
} else {
KeyguardState.LOCKSCREEN
@@ -106,16 +109,16 @@ constructor(
.sample(
keyguardInteractor.isKeyguardShowing,
communalInteractor.isIdleOnCommunal,
- communalInteractor.showByDefault,
+ communalInteractor.showCommunalFromOccluded,
)
.filterRelevantKeyguardStateAnd { (isOccluded, isShowing, _, _) ->
!isOccluded && isShowing
}
- .collect { (_, _, isIdleOnCommunal, showCommunalByDefault) ->
+ .collect { (_, _, isIdleOnCommunal, showCommunalFromOccluded) ->
// Occlusion signals come from the framework, and should interrupt any
// existing transition
val to =
- if (isIdleOnCommunal || showCommunalByDefault) {
+ if (isIdleOnCommunal || showCommunalFromOccluded) {
KeyguardState.GLANCEABLE_HUB
} else {
KeyguardState.LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
index 20b7b2a91ade..82255a0c0d54 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt
@@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
/**
* Distance over which the surface behind the keyguard is animated in during a Y-translation
@@ -102,8 +103,11 @@ constructor(
*/
private val isNotificationLaunchAnimationRunningOnKeyguard =
notificationLaunchInteractor.isLaunchAnimationRunning
- .sample(transitionInteractor.finishedKeyguardState)
- .map { it != KeyguardState.GONE }
+ .sample(transitionInteractor.finishedKeyguardState, ::Pair)
+ .map { (animationRunning, finishedState) ->
+ animationRunning && finishedState != KeyguardState.GONE
+ }
+ .onStart { emit(false) }
/**
* Whether we're animating the surface, or a notification launch animation is running (which
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
new file mode 100644
index 000000000000..5ad7762bb512
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Handles initialization of the KeyguardTransitionRepository on boot. */
+@SysUISingleton
+class KeyguardTransitionBootInteractor
+@Inject
+constructor(
+ @Application val scope: CoroutineScope,
+ val deviceEntryInteractor: DeviceEntryInteractor,
+ val deviceProvisioningInteractor: DeviceProvisioningInteractor,
+ val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ val repository: KeyguardTransitionRepository,
+) : CoreStartable {
+
+ /**
+ * Whether the lockscreen should be showing when the device starts up for the first time. If not
+ * then we'll seed the repository with a transition from OFF -> GONE.
+ */
+ @OptIn(ExperimentalCoroutinesApi::class)
+ private val showLockscreenOnBoot =
+ deviceProvisioningInteractor.isDeviceProvisioned.map { provisioned ->
+ (provisioned || deviceEntryInteractor.isAuthenticationRequired()) &&
+ deviceEntryInteractor.isLockscreenEnabled()
+ }
+
+ override fun start() {
+ scope.launch {
+ val state =
+ if (showLockscreenOnBoot.first()) {
+ KeyguardState.LOCKSCREEN
+ } else {
+ KeyguardState.GONE
+ }
+
+ if (
+ keyguardTransitionInteractor.currentTransitionInfoInternal.value.from !=
+ KeyguardState.OFF
+ ) {
+ Log.e(
+ "KeyguardTransitionInteractor",
+ "showLockscreenOnBoot emitted, but we've already " +
+ "transitioned to a state other than OFF. We'll respect that " +
+ "transition, but this should not happen."
+ )
+ } else {
+ repository.emitInitialStepsFromOff(state)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index 91f8420393e1..31b0bf7fe425 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -27,6 +27,7 @@ class KeyguardTransitionCoreStartable
constructor(
private val interactors: Set<TransitionInteractor>,
private val auditLogger: KeyguardTransitionAuditLogger,
+ private val bootInteractor: KeyguardTransitionBootInteractor,
) : CoreStartable {
override fun start() {
@@ -51,6 +52,7 @@ constructor(
it.start()
}
auditLogger.start()
+ bootInteractor.start()
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index 53f013275451..18022a98c69a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.binder
import android.graphics.PixelFormat
+import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@@ -95,11 +96,11 @@ constructor(
applicationScope.launch("$TAG#alternateBouncerWindowViewModel") {
alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect {
addAlternateBouncerWindowView ->
+ Log.d(TAG, "alternateBouncerWindowRequired=$addAlternateBouncerWindowView")
if (addAlternateBouncerWindowView) {
addViewToWindowManager()
- val scrim =
+ val scrim: ScrimView =
alternateBouncerView!!.requireViewById(R.id.alternate_bouncer_scrim)
- as ScrimView
scrim.viewAlpha = 0f
bind(alternateBouncerView!!, alternateBouncerDependencies.get())
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index b0d45edaf8c2..4f00495819e8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -34,6 +34,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -94,6 +95,24 @@ object DeviceEntryIconViewBinder {
longPressHandlingView.setLongPressHandlingEnabled(isEnabled)
}
}
+ launch("$TAG#viewModel.isUdfpsSupported") {
+ viewModel.isUdfpsSupported.collect { udfpsSupported ->
+ longPressHandlingView.longPressDuration =
+ if (udfpsSupported) {
+ {
+ view.resources
+ .getInteger(R.integer.config_udfpsDeviceEntryIconLongPress)
+ .toLong()
+ }
+ } else {
+ {
+ view.resources
+ .getInteger(R.integer.config_lockIconLongPress)
+ .toLong()
+ }
+ }
+ }
+ }
launch("$TAG#viewModel.accessibilityDelegateHint") {
viewModel.accessibilityDelegateHint.collect { hint ->
view.accessibilityHintType = hint
@@ -132,8 +151,12 @@ object DeviceEntryIconViewBinder {
view.getIconState(viewModel.type, viewModel.useAodVariant),
/* merge */ false
)
- fgIconView.contentDescription =
- fgIconView.resources.getString(viewModel.type.contentDescriptionResId)
+ if (viewModel.type.contentDescriptionResId != -1) {
+ fgIconView.contentDescription =
+ fgIconView.resources.getString(
+ viewModel.type.contentDescriptionResId
+ )
+ }
fgIconView.imageTintList = ColorStateList.valueOf(viewModel.tint)
fgIconView.setPadding(
viewModel.padding,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index abd79ab793d5..b9a79dccf76b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -118,6 +118,7 @@ object KeyguardQuickAffordanceViewBinder {
}
override fun destroy() {
+ view.setOnApplyWindowInsetsListener(null)
disposableHandle.dispose()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index a8e9041abfd7..0f63f65e4511 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -41,6 +41,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransition
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToPrimaryBouncerTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
@@ -196,6 +197,12 @@ abstract class DeviceEntryIconTransitionModule {
@Binds
@IntoSet
+ abstract fun offToLockscreen(
+ impl: OffToLockscreenTransitionViewModel
+ ): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
abstract fun primaryBouncerToAod(
impl: PrimaryBouncerToAodTransitionViewModel
): DeviceEntryIconTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index 2735aed3ba4d..35b259849b78 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -40,10 +40,7 @@ constructor(
attrs: AttributeSet?,
defStyleAttrs: Int = 0,
) : FrameLayout(context, attrs, defStyleAttrs) {
- val longPressHandlingView: LongPressHandlingView =
- LongPressHandlingView(context, attrs) {
- context.resources.getInteger(R.integer.config_lockIconLongPress).toLong()
- }
+ val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs)
val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg }
val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg }
val aodFpDrawable: LottieDrawable = LottieDrawable()
@@ -214,7 +211,7 @@ constructor(
R.id.unlocked,
R.id.locked_aod,
context.getDrawable(R.drawable.unlocked_to_aod_lock) as AnimatedVectorDrawable,
- /* reversible */ true,
+ /* reversible */ false,
)
}
@@ -252,6 +249,7 @@ constructor(
IconType.LOCK -> lockIconState[0] = android.R.attr.state_first
IconType.UNLOCK -> lockIconState[0] = android.R.attr.state_last
IconType.FINGERPRINT -> lockIconState[0] = android.R.attr.state_middle
+ IconType.NONE -> return StateSet.NOTHING
}
if (aod) {
lockIconState[1] = android.R.attr.state_single
@@ -265,6 +263,7 @@ constructor(
LOCK(R.string.accessibility_lock_icon),
UNLOCK(R.string.accessibility_unlock_button),
FINGERPRINT(R.string.accessibility_fingerprint_label),
+ NONE(-1),
}
enum class AccessibilityHintType {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 45b82576c6c4..9146c605ab63 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.ui.view.layout.sections
import android.content.res.Resources
+import android.view.WindowInsets
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -25,15 +26,19 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
+import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.VibratorHelper
+import dagger.Lazy
import javax.inject.Inject
class DefaultShortcutsSection
@@ -46,11 +51,29 @@ constructor(
private val falsingManager: FalsingManager,
private val indicationController: KeyguardIndicationController,
private val vibratorHelper: VibratorHelper,
+ private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
) : BaseShortcutSection() {
+
+ // Amount to increase the bottom margin by to avoid colliding with inset
+ private var safeInsetBottom = 0
+
override fun addViews(constraintLayout: ConstraintLayout) {
if (KeyguardBottomAreaRefactor.isEnabled) {
addLeftShortcut(constraintLayout)
addRightShortcut(constraintLayout)
+
+ constraintLayout
+ .requireViewById<LaunchableImageView>(R.id.start_button)
+ .setOnApplyWindowInsetsListener { _, windowInsets ->
+ val tempSafeInset = windowInsets?.displayCutout?.safeInsetBottom ?: 0
+ if (safeInsetBottom != tempSafeInset) {
+ safeInsetBottom = tempSafeInset
+ keyguardBlueprintInteractor
+ .get()
+ .refreshBlueprint(IntraBlueprintTransition.Type.DefaultTransition)
+ }
+ WindowInsets.CONSUMED
+ }
}
}
@@ -91,12 +114,24 @@ constructor(
constrainWidth(R.id.start_button, width)
constrainHeight(R.id.start_button, height)
connect(R.id.start_button, LEFT, PARENT_ID, LEFT, horizontalOffsetMargin)
- connect(R.id.start_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin)
+ connect(
+ R.id.start_button,
+ BOTTOM,
+ PARENT_ID,
+ BOTTOM,
+ verticalOffsetMargin + safeInsetBottom
+ )
constrainWidth(R.id.end_button, width)
constrainHeight(R.id.end_button, height)
connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT, horizontalOffsetMargin)
- connect(R.id.end_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin)
+ connect(
+ R.id.end_button,
+ BOTTOM,
+ PARENT_ID,
+ BOTTOM,
+ verticalOffsetMargin + safeInsetBottom
+ )
// The constraint set visibility for start and end button are default visible, set to
// ignore so the view's own initial visibility (invisible) is used
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
index e0a3af6021c0..570f37710c24 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
@@ -102,37 +102,40 @@ constructor(
to = GONE,
)
- return shadeInteractor.isAnyExpanded.flatMapLatest { isAnyExpanded ->
- transitionAnimation
- .sharedFlow(
- duration = duration,
- interpolator = EMPHASIZED_ACCELERATE,
- onStart = {
- leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
- willRunDismissFromKeyguard = willRunAnimationOnKeyguard()
- isShadeExpanded = isAnyExpanded
- },
- onStep = { 1f - it },
- )
- .map {
- if (willRunDismissFromKeyguard) {
- if (isShadeExpanded) {
+ return shadeInteractor.anyExpansion
+ .map { it > 0f }
+ .distinctUntilChanged()
+ .flatMapLatest { isAnyExpanded ->
+ transitionAnimation
+ .sharedFlow(
+ duration = duration,
+ interpolator = EMPHASIZED_ACCELERATE,
+ onStart = {
+ leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
+ willRunDismissFromKeyguard = willRunAnimationOnKeyguard()
+ isShadeExpanded = isAnyExpanded
+ },
+ onStep = { 1f - it },
+ )
+ .map {
+ if (willRunDismissFromKeyguard) {
+ if (isShadeExpanded) {
+ ScrimAlpha(
+ behindAlpha = it,
+ notificationsAlpha = it,
+ )
+ } else {
+ ScrimAlpha()
+ }
+ } else if (leaveShadeOpen) {
ScrimAlpha(
- behindAlpha = it,
- notificationsAlpha = it,
+ behindAlpha = 1f,
+ notificationsAlpha = 1f,
)
} else {
- ScrimAlpha()
+ ScrimAlpha(behindAlpha = it)
}
- } else if (leaveShadeOpen) {
- ScrimAlpha(
- behindAlpha = 1f,
- notificationsAlpha = 1f,
- )
- } else {
- ScrimAlpha(behindAlpha = it)
}
- }
- }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index e26b75fa9d5f..da2fcc48a13d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -84,19 +84,21 @@ constructor(
.map { it.deviceEntryParentViewAlpha }
.merge()
.shareIn(scope, SharingStarted.WhileSubscribed())
+ .onStart { emit(initialAlphaFromKeyguardState(transitionInteractor.getCurrentState())) }
private val alphaMultiplierFromShadeExpansion: Flow<Float> =
combine(
- showingAlternateBouncer,
- shadeExpansion,
- qsProgress,
- ) { showingAltBouncer, shadeExpansion, qsProgress ->
- val interpolatedQsProgress = (qsProgress * 2).coerceIn(0f, 1f)
- if (showingAltBouncer) {
- 1f
- } else {
- (1f - shadeExpansion) * (1f - interpolatedQsProgress)
+ showingAlternateBouncer,
+ shadeExpansion,
+ qsProgress,
+ ) { showingAltBouncer, shadeExpansion, qsProgress ->
+ val interpolatedQsProgress = (qsProgress * 2).coerceIn(0f, 1f)
+ if (showingAltBouncer) {
+ 1f
+ } else {
+ (1f - shadeExpansion) * (1f - interpolatedQsProgress)
+ }
}
- }
+ .onStart { emit(1f) }
// Burn-in offsets in AOD
private val nonAnimatedBurnInOffsets: Flow<BurnInOffsets> =
combine(
@@ -122,14 +124,34 @@ constructor(
)
}
- val deviceEntryViewAlpha: StateFlow<Float> =
+ val deviceEntryViewAlpha: Flow<Float> =
combine(
transitionAlpha,
alphaMultiplierFromShadeExpansion,
) { alpha, alphaMultiplier ->
alpha * alphaMultiplier
}
- .stateIn(scope = scope, started = SharingStarted.WhileSubscribed(), initialValue = 0f)
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = 0f,
+ )
+
+ private fun initialAlphaFromKeyguardState(keyguardState: KeyguardState): Float {
+ return when (keyguardState) {
+ KeyguardState.OFF,
+ KeyguardState.PRIMARY_BOUNCER,
+ KeyguardState.DOZING,
+ KeyguardState.DREAMING,
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.GONE,
+ KeyguardState.OCCLUDED,
+ KeyguardState.DREAMING_LOCKSCREEN_HOSTED, -> 0f
+ KeyguardState.AOD,
+ KeyguardState.ALTERNATE_BOUNCER,
+ KeyguardState.LOCKSCREEN -> 1f
+ }
+ }
val useBackgroundProtection: StateFlow<Boolean> = isUdfpsSupported
val burnInOffsets: Flow<BurnInOffsets> =
deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled
@@ -195,7 +217,14 @@ constructor(
isUnlocked,
) { isListeningForUdfps, isUnlocked ->
if (isListeningForUdfps) {
- DeviceEntryIconView.IconType.FINGERPRINT
+ if (isUnlocked) {
+ // Don't show any UI until isUnlocked=false. This covers the case
+ // when the "Power button instantly locks > 0s" or the device doesn't lock
+ // immediately after a screen time.
+ DeviceEntryIconView.IconType.NONE
+ } else {
+ DeviceEntryIconView.IconType.FINGERPRINT
+ }
} else if (isUnlocked) {
DeviceEntryIconView.IconType.UNLOCK
} else {
@@ -211,7 +240,8 @@ constructor(
when (deviceEntryStatus) {
DeviceEntryIconView.IconType.LOCK -> isUdfps
DeviceEntryIconView.IconType.UNLOCK -> true
- DeviceEntryIconView.IconType.FINGERPRINT -> false
+ DeviceEntryIconView.IconType.FINGERPRINT,
+ DeviceEntryIconView.IconType.NONE -> false
}
}
@@ -239,8 +269,8 @@ constructor(
DeviceEntryIconView.IconType.LOCK ->
DeviceEntryIconView.AccessibilityHintType.AUTHENTICATE
DeviceEntryIconView.IconType.UNLOCK -> DeviceEntryIconView.AccessibilityHintType.ENTER
- DeviceEntryIconView.IconType.FINGERPRINT ->
- DeviceEntryIconView.AccessibilityHintType.NONE
+ DeviceEntryIconView.IconType.FINGERPRINT,
+ DeviceEntryIconView.IconType.NONE -> DeviceEntryIconView.AccessibilityHintType.NONE
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
index d4c8456e0d71..d8b50133949d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt
@@ -88,12 +88,11 @@ constructor(
isCommunalAvailable: Boolean,
shadeMode: ShadeMode,
): Map<UserAction, UserActionResult> {
+ val shadeSceneKey =
+ if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+
val quickSettingsIfSingleShade =
- if (shadeMode is ShadeMode.Single) {
- Scenes.QuickSettings
- } else {
- Scenes.Shade
- }
+ if (shadeMode is ShadeMode.Single) Scenes.QuickSettings else shadeSceneKey
return mapOf(
Swipe.Left to UserActionResult(Scenes.Communal).takeIf { isCommunalAvailable },
@@ -101,11 +100,17 @@ constructor(
// Swiping down from the top edge goes to QS (or shade if in split shade mode).
swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade,
- swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade,
+ swipeDownFromTop(pointerCount = 2) to
+ // TODO(b/338577208): Remove 'Dual' once we add Dual Shade invocation zones.
+ if (shadeMode is ShadeMode.Dual) {
+ Scenes.QuickSettingsShade
+ } else {
+ quickSettingsIfSingleShade
+ },
// Swiping down, not from the edge, always navigates to the shade scene.
- swipeDown(pointerCount = 1) to Scenes.Shade,
- swipeDown(pointerCount = 2) to Scenes.Shade,
+ swipeDown(pointerCount = 1) to shadeSceneKey,
+ swipeDown(pointerCount = 2) to shadeSceneKey,
)
.filterValues { it != null }
.mapValues { checkNotNull(it.value) }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
index 74094bea140a..cf6a533ed76b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OffToLockscreenTransitionViewModel.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
@@ -28,7 +29,7 @@ class OffToLockscreenTransitionViewModel
@Inject
constructor(
animationFlow: KeyguardTransitionAnimationFlow,
-) {
+) : DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow.setup(
@@ -43,4 +44,7 @@ constructor(
onStep = { it },
onCancel = { 0f },
)
+
+ override val deviceEntryParentViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(1f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index 9e6c5520d1b7..b276f532e874 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -201,6 +201,10 @@ constructor(
)
}
+ fun addLockoutResetCallbackDone() {
+ logBuffer.log(TAG, DEBUG, {}, { "addlockoutResetCallback done" })
+ }
+
fun authRequested(uiEvent: FaceAuthUiEvent) {
logBuffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
index d995116c5629..89e4760615f0 100644
--- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt
@@ -81,8 +81,14 @@ constructor(
val EvaluatorByFlag =
mapOf<Int, (SceneContainerPluginState) -> Boolean>(
SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to { it.scene != Scenes.Gone },
- SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to { it.scene == Scenes.Shade },
- SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it.scene == Scenes.QuickSettings },
+ SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to
+ {
+ it.scene == Scenes.NotificationsShade || it.scene == Scenes.Shade
+ },
+ SYSUI_STATE_QUICK_SETTINGS_EXPANDED to
+ {
+ it.scene == Scenes.QuickSettingsShade || it.scene == Scenes.QuickSettings
+ },
SYSUI_STATE_BOUNCER_SHOWING to { it.scene == Scenes.Bouncer },
SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
{
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
new file mode 100644
index 000000000000..f677ec1b31bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.notifications.ui.viewmodel
+
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the Notifications Shade scene. */
+@SysUISingleton
+class NotificationsShadeSceneViewModel
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ overlayShadeViewModel: OverlayShadeViewModel,
+) {
+ val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+ overlayShadeViewModel.backgroundScene
+ .map(::destinationScenes)
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = destinationScenes(overlayShadeViewModel.backgroundScene.value),
+ )
+
+ private fun destinationScenes(backgroundScene: SceneKey): Map<UserAction, UserActionResult> {
+ return mapOf(
+ Swipe.Up to backgroundScene,
+ Back to backgroundScene,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index d476e6302a3b..a14479bb848e 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -67,6 +67,7 @@ import com.android.settingslib.fuelgauge.BatterySaverUtils;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.plugins.ActivityStarter;
@@ -323,7 +324,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
// remaining estimate is disabled
if (!mCurrentBatterySnapshot.isHybrid() || mBucket < -1
|| mCurrentBatterySnapshot.getTimeRemainingMillis()
- < mCurrentBatterySnapshot.getSevereThresholdMillis()) {
+ < mCurrentBatterySnapshot.getSevereThresholdMillis()) {
nb.setColor(Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorError));
}
@@ -703,17 +704,23 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
mSaverConfirmation = null;
logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_DISMISS);
});
- WeakReference<View> ref = mBatteryControllerLazy.get().getLastPowerSaverStartView();
- if (ref != null && ref.get() != null && ref.get().isAggregatedVisible()) {
- mDialogTransitionAnimator.showFromView(d, ref.get(),
+ WeakReference<Expandable> ref =
+ mBatteryControllerLazy.get().getLastPowerSaverStartExpandable();
+ if (ref != null && ref.get() != null) {
+ DialogTransitionAnimator.Controller controller = ref.get().dialogTransitionController(
new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(d, controller);
+ } else {
+ d.show();
+ }
} else {
d.show();
}
logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_DIALOG);
mSaverConfirmation = d;
- mBatteryControllerLazy.get().clearLastPowerSaverStartView();
+ mBatteryControllerLazy.get().clearLastPowerSaverStartExpandable();
}
@VisibleForTesting
@@ -873,4 +880,4 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
}
}
}
-}
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
index b53c2450f4c7..d26ae0a4dac8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -41,7 +41,6 @@ import android.service.quicksettings.TileService;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.IWindowManager;
-import android.view.View;
import android.view.WindowManagerGlobal;
import android.widget.Switch;
@@ -52,6 +51,7 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -99,7 +99,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener,
@Nullable
private CharSequence mDefaultLabel;
@Nullable
- private View mViewClicked;
+ private Expandable mExpandableClicked;
private final Context mUserContext;
@@ -347,7 +347,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener,
mService.onStartListening();
}
} else {
- mViewClicked = null;
+ mExpandableClicked = null;
mService.onStopListening();
if (mIsTokenGranted && !mIsShowingDialog) {
try {
@@ -409,11 +409,11 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener,
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
return;
}
- mViewClicked = view;
+ mExpandableClicked = expandable;
try {
if (DEBUG) Log.d(TAG, "Adding token");
mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG,
@@ -541,11 +541,9 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener,
Log.i(TAG, "The activity is starting");
ActivityTransitionAnimator.Controller controller =
- mViewClicked == null ? null :
- ActivityTransitionAnimator.Controller.fromView(
- mViewClicked,
- InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
- );
+ mExpandableClicked == null ? null :
+ mExpandableClicked.activityTransitionController(
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
mActivityStarter.startPendingIntentMaybeDismissingKeyguard(
pendingIntent,
/* intentSentUiThreadCallback= */ null,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
index 8fc66d37ef86..a6cfa75a7583 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileViewModel.kt
@@ -16,8 +16,7 @@
package com.android.systemui.qs.panels.ui.viewmodel
-import android.view.View
-import android.view.View.OnLongClickListener
+import com.android.systemui.animation.Expandable
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
@@ -26,8 +25,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onStart
-class TileViewModel(private val tile: QSTile, val spec: TileSpec) :
- OnLongClickListener, View.OnClickListener {
+class TileViewModel(private val tile: QSTile, val spec: TileSpec) {
val state: Flow<QSTile.State> =
conflatedCallbackFlow {
val callback = QSTile.Callback { trySend(it.copy()) }
@@ -42,13 +40,12 @@ class TileViewModel(private val tile: QSTile, val spec: TileSpec) :
val currentState: QSTile.State
get() = tile.state
- override fun onClick(view: View?) {
- tile.click(view)
+ fun onClick(expandable: Expandable?) {
+ tile.click(expandable)
}
- override fun onLongClick(view: View?): Boolean {
- tile.longClick(view)
- return true
+ fun onLongClick(expandable: Expandable?) {
+ tile.longClick(expandable)
}
fun startListening(token: Any) = tile.setListening(token, true)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 145674747bb6..c24113f14f00 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -42,7 +42,6 @@ import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
-import android.view.View;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
@@ -58,6 +57,7 @@ import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QSTile;
@@ -137,9 +137,9 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
*
* Calls to the controller should be made here to set the new state of the device.
*
- * @param view The view that was clicked.
+ * @param expandable {@link Expandable} that was clicked.
*/
- protected abstract void handleClick(@Nullable View view);
+ protected abstract void handleClick(@Nullable Expandable expandable);
/**
* Update state of the tile based on device state
@@ -282,7 +282,8 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
}
- public void click(@Nullable View view) {
+ @Override
+ public void click(@Nullable Expandable expandable) {
mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
.addTaggedData(FIELD_STATUS_BAR_STATE,
mStatusBarStateController.getState())));
@@ -292,11 +293,12 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
eventId);
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
- mHandler.obtainMessage(H.CLICK, eventId, 0, view).sendToTarget();
+ mHandler.obtainMessage(H.CLICK, eventId, 0, expandable).sendToTarget();
}
}
- public void secondaryClick(@Nullable View view) {
+ @Override
+ public void secondaryClick(@Nullable Expandable expandable) {
mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
.addTaggedData(FIELD_STATUS_BAR_STATE,
mStatusBarStateController.getState())));
@@ -305,11 +307,11 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
final int eventId = mClickEventId++;
mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(),
mState.state, eventId);
- mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, view).sendToTarget();
+ mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, expandable).sendToTarget();
}
@Override
- public void longClick(@Nullable View view) {
+ public void longClick(@Nullable Expandable expandable) {
mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
.addTaggedData(FIELD_STATUS_BAR_STATE,
mStatusBarStateController.getState())));
@@ -319,7 +321,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
eventId);
if (!mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) {
- mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget();
+ mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, expandable).sendToTarget();
}
}
@@ -397,22 +399,22 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
*
* Defaults to {@link QSTileImpl#handleClick}
*
- * @param view The view that was clicked.
+ * @param expandable {@link Expandable} that was clicked.
*/
- protected void handleSecondaryClick(@Nullable View view) {
+ protected void handleSecondaryClick(@Nullable Expandable expandable) {
// Default to normal click.
- handleClick(view);
+ handleClick(expandable);
}
/**
* Handles long click on the tile by launching the {@link Intent} defined in
* {@link QSTileImpl#getLongClickIntent}.
*
- * @param view The view from which the opening window will be animated.
+ * @param expandable {@link Expandable} from which the opening window will be animated.
*/
- protected void handleLongClick(@Nullable View view) {
+ protected void handleLongClick(@Nullable Expandable expandable) {
ActivityTransitionAnimator.Controller animationController =
- view != null ? ActivityTransitionAnimator.Controller.fromView(view,
+ expandable != null ? expandable.activityTransitionController(
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE) : null;
mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0,
animationController);
@@ -591,16 +593,16 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
} else {
mQSLogger.logHandleClick(mTileSpec, msg.arg1);
- handleClick((View) msg.obj);
+ handleClick((Expandable) msg.obj);
}
} else if (msg.what == SECONDARY_CLICK) {
name = "handleSecondaryClick";
mQSLogger.logHandleSecondaryClick(mTileSpec, msg.arg1);
- handleSecondaryClick((View) msg.obj);
+ handleSecondaryClick((Expandable) msg.obj);
} else if (msg.what == LONG_CLICK) {
name = "handleLongClick";
mQSLogger.logHandleLongClick(mTileSpec, msg.arg1);
- handleLongClick((View) msg.obj);
+ handleLongClick((Expandable) msg.obj);
} else if (msg.what == REFRESH_STATE) {
name = "handleRefreshState";
handleRefreshState(msg.obj);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index ca5b77108505..f3852a275209 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -53,6 +53,7 @@ import com.android.settingslib.Utils
import com.android.systemui.Flags
import com.android.systemui.Flags.quickSettingsVisualHapticsLongpress
import com.android.systemui.FontSizeUtils
+import com.android.systemui.animation.Expandable
import com.android.systemui.animation.LaunchableView
import com.android.systemui.animation.LaunchableViewDelegate
import com.android.systemui.haptics.qs.QSLongPressEffect
@@ -364,10 +365,11 @@ open class QSTileViewImpl @JvmOverloads constructor(
}
override fun init(tile: QSTile) {
+ val expandable = Expandable.fromView(this)
init(
- { v: View? -> tile.click(this) },
- { view: View? ->
- tile.longClick(this)
+ { _: View? -> tile.click(expandable) },
+ { _: View? ->
+ tile.longClick(expandable)
true
}
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
index 17251c37473f..206879905782 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -29,13 +29,13 @@ import android.provider.Settings.Global;
import android.service.quicksettings.Tile;
import android.sysprop.TelephonyProperties;
import android.telephony.TelephonyManager;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -103,7 +103,7 @@ public class AirplaneModeTile extends QSTileImpl<BooleanState> {
}
@Override
- public void handleClick(@Nullable View view) {
+ public void handleClick(@Nullable Expandable expandable) {
boolean airplaneModeEnabled = mState.value;
MetricsLogger.action(mContext, getMetricsCategory(), !airplaneModeEnabled);
if (!airplaneModeEnabled && TelephonyProperties.in_ecm_mode().orElse(false)) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
index 688f3ca38087..73d991f6efe7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt
@@ -9,12 +9,10 @@ import android.provider.AlarmClock
import android.service.quicksettings.Tile
import android.text.TextUtils
import android.text.format.DateFormat
-import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
@@ -25,12 +23,15 @@ import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.NextAlarmController
import java.util.Locale
import javax.inject.Inject
-class AlarmTile @Inject constructor(
+class AlarmTile
+@Inject
+constructor(
host: QSHost,
uiEventLogger: QsEventLogger,
@Background backgroundLooper: Looper,
@@ -56,8 +57,7 @@ class AlarmTile @Inject constructor(
private var lastAlarmInfo: AlarmManager.AlarmClockInfo? = null
private val icon = ResourceIcon.get(R.drawable.ic_alarm)
- @VisibleForTesting
- internal val defaultIntent = Intent(AlarmClock.ACTION_SHOW_ALARMS)
+ @VisibleForTesting internal val defaultIntent = Intent(AlarmClock.ACTION_SHOW_ALARMS)
private val callback = NextAlarmController.NextAlarmChangeCallback { nextAlarm ->
lastAlarmInfo = nextAlarm
refreshState()
@@ -73,11 +73,11 @@ class AlarmTile @Inject constructor(
}
}
- override fun handleClick(view: View?) {
- val animationController = view?.let {
- ActivityTransitionAnimator.Controller.fromView(
- it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
- }
+ override fun handleClick(expandable: Expandable?) {
+ val animationController =
+ expandable?.activityTransitionController(
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+ )
val pendingIntent = lastAlarmInfo?.showIntent
if (pendingIntent != null) {
mActivityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
index 426aa553f082..7c0ce4cc75a9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java
@@ -21,7 +21,6 @@ import android.os.Looper;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -29,6 +28,7 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -121,7 +121,7 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements
if (!listening) {
// If we stopped listening, it means that the tile is not visible. In that case, we
// don't need to save the view anymore
- mBatteryController.clearLastPowerSaverStartView();
+ mBatteryController.clearLastPowerSaverStartExpandable();
}
}
@@ -131,11 +131,11 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (getState().state == Tile.STATE_UNAVAILABLE) {
return;
}
- mBatteryController.setPowerSaveMode(!mPowerSave, view);
+ mBatteryController.setPowerSaveMode(!mPowerSave, expandable);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 6eae32a0ffd6..9af34f6c9918 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -31,7 +31,6 @@ import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
import android.widget.Switch;
import com.android.internal.logging.MetricsLogger;
@@ -39,6 +38,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -109,9 +109,9 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
- mDialogViewModel.showDialog(view);
+ mDialogViewModel.showDialog(expandable);
} else {
// Secondary clicks are header clicks, just toggle.
final boolean isEnabled = mState.value;
@@ -127,7 +127,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleSecondaryClick(@Nullable View view) {
+ protected void handleSecondaryClick(@Nullable Expandable expandable) {
if (!mController.canConfigBluetooth()) {
mActivityStarter.postStartActivityDismissingKeyguard(
new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index b27b974dc972..169cdc17c2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -29,7 +29,6 @@ import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.util.Log;
-import android.view.View;
import android.widget.Button;
import androidx.annotation.Nullable;
@@ -41,6 +40,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
@@ -161,12 +161,12 @@ public class CastTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleLongClick(@Nullable View view) {
- handleClick(view);
+ protected void handleLongClick(@Nullable Expandable expandable) {
+ handleClick(expandable);
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (getState().state == Tile.STATE_UNAVAILABLE) {
return;
}
@@ -174,7 +174,7 @@ public class CastTile extends QSTileImpl<BooleanState> {
List<CastDevice> activeDevices = getActiveDevices();
if (willPopDialog()) {
if (!mKeyguard.isShowing()) {
- showDialog(view);
+ showDialog(expandable);
} else {
mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
// Dismissing the keyguard will collapse the shade, so we don't animate from the
@@ -216,7 +216,7 @@ public class CastTile extends QSTileImpl<BooleanState> {
}
}
- private void showDialog(@Nullable View view) {
+ private void showDialog(@Nullable Expandable expandable) {
mUiHandler.post(() -> {
final DialogHolder holder = new DialogHolder();
final Dialog dialog = MediaRouteDialogPresenter.createDialog(
@@ -241,17 +241,21 @@ public class CastTile extends QSTileImpl<BooleanState> {
SystemUIDialog.setDialogSize(dialog);
mUiHandler.post(() -> {
- if (view != null) {
- mDialogTransitionAnimator.showFromView(dialog, view,
- new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG));
- } else {
- if (dialog.getWindow() != null) {
- DialogKt.registerAnimationOnBackInvoked(dialog,
- dialog.getWindow().getDecorView());
+ if (expandable != null) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(
+ new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(dialog, controller);
+ return;
}
- dialog.show();
}
+ if (dialog.getWindow() != null) {
+ DialogKt.registerAnimationOnBackInvoked(dialog,
+ dialog.getWindow().getDecorView());
+ }
+ dialog.show();
});
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
index c8adbfcf5487..871973dfcb7f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorCorrectionTile.java
@@ -22,12 +22,12 @@ import android.os.Looper;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -109,7 +109,7 @@ public class ColorCorrectionTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
mSetting.setValue(mState.value ? 0 : 1);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
index c34a5842c1e3..58969107ad22 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ColorInversionTile.java
@@ -22,13 +22,13 @@ import android.os.Looper;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -108,7 +108,7 @@ public class ColorInversionTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
mSetting.setValue(mState.value ? 0 : 1);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
index 58630a0b6b99..7760943476bf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java
@@ -19,7 +19,6 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -30,6 +29,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Prefs;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -47,7 +47,7 @@ import com.android.systemui.statusbar.policy.DataSaverController;
import javax.inject.Inject;
public class DataSaverTile extends QSTileImpl<BooleanState> implements
- DataSaverController.Listener{
+ DataSaverController.Listener {
public static final String TILE_SPEC = "saver";
@@ -89,8 +89,9 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements
public Intent getLongClickIntent() {
return new Intent(Settings.ACTION_DATA_SAVER_SETTINGS);
}
+
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (mState.value
|| Prefs.getBoolean(mContext, Prefs.Key.QS_DATA_SAVER_DIALOG_SHOWN, false)) {
// Do it right away.
@@ -112,10 +113,16 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements
dialog.setNeutralButton(com.android.internal.R.string.cancel, null);
dialog.setShowForAllUsers(true);
- if (view != null) {
- mDialogTransitionAnimator.showFromView(dialog, view, new DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG));
+ if (expandable != null) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(new DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(dialog, controller);
+ } else {
+ dialog.show();
+ }
} else {
dialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
index bb175e2a8534..cc8a73423174 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DeviceControlsTile.kt
@@ -1,3 +1,4 @@
+
/*
* Copyright (C) 2021 The Android Open Source Project
*
@@ -21,12 +22,11 @@ import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.service.quicksettings.Tile
-import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.MetricsLogger
import com.android.systemui.res.R
-import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.dagger.ControlsComponent
import com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE
@@ -100,26 +100,30 @@ class DeviceControlsTile @Inject constructor(
}
}
- override fun handleClick(view: View?) {
+ override fun handleClick(expandable: Expandable?) {
if (state.state == Tile.STATE_UNAVAILABLE) {
return
}
val intent = Intent().apply {
component = ComponentName(mContext, controlsComponent.getControlsUiController().get()
- .resolveActivity())
+ .resolveActivity())
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(ControlsUiController.EXTRA_ANIMATE, true)
}
- val animationController = view?.let {
- ActivityTransitionAnimator.Controller.fromView(
- it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
- }
+ val animationController =
+ expandable?.activityTransitionController(
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+ )
mUiHandler.post {
val showOverLockscreenWhenLocked = state.state == Tile.STATE_ACTIVE
mActivityStarter.startActivity(
- intent, true /* dismissShade */, animationController, showOverLockscreenWhenLocked)
+ intent,
+ true /* dismissShade */,
+ animationController,
+ showOverLockscreenWhenLocked,
+ )
}
}
@@ -130,7 +134,7 @@ class DeviceControlsTile @Inject constructor(
if (controlsComponent.isEnabled() && hasControlsApps.get()) {
if (controlsComponent.getVisibility() == AVAILABLE) {
val selection = controlsComponent
- .getControlsController().get().getPreferredSelection()
+ .getControlsController().get().getPreferredSelection()
state.state = if (selection is SelectedItem.StructureItem &&
selection.structure.controls.isEmpty()) {
Tile.STATE_INACTIVE
@@ -157,7 +161,7 @@ class DeviceControlsTile @Inject constructor(
return null
}
- override fun handleLongClick(view: View?) {}
+ override fun handleLongClick(expandable: Expandable?) {}
override fun getTileLabel(): CharSequence {
return mContext.getText(controlsComponent.getTileTitleId())
@@ -166,4 +170,4 @@ class DeviceControlsTile @Inject constructor(
companion object {
const val TILE_SPEC = "controls"
}
-}
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index f62b60bd887f..4ebebeade1f4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -35,7 +35,6 @@ import android.service.notification.ZenModeConfig;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -47,6 +46,7 @@ import com.android.settingslib.notification.EnableZenModeDialog;
import com.android.systemui.Prefs;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -147,12 +147,12 @@ public class DndTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
// Zen is currently on
if (mState.value) {
mController.setZen(ZEN_MODE_OFF, null, TAG);
} else {
- enableZenMode(view);
+ enableZenMode(expandable);
}
}
@@ -162,7 +162,7 @@ public class DndTile extends QSTileImpl<BooleanState> {
mSettingZenDuration.setUserId(newUserId);
}
- private void enableZenMode(@Nullable View view) {
+ private void enableZenMode(@Nullable Expandable expandable) {
int zenDuration = mSettingZenDuration.getValue();
boolean showOnboarding = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0
@@ -183,11 +183,17 @@ public class DndTile extends QSTileImpl<BooleanState> {
case Settings.Secure.ZEN_DURATION_PROMPT:
mUiHandler.post(() -> {
Dialog dialog = makeZenModeDialog();
- if (view != null) {
- mDialogTransitionAnimator.showFromView(dialog, view, new DialogCuj(
+ if (expandable != null) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(new DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG),
- /* animateBackgroundBoundsChange= */ false);
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(dialog,
+ controller, /* animateBackgroundBoundsChange= */ false);
+ } else {
+ dialog.show();
+ }
} else {
dialog.show();
}
@@ -217,8 +223,8 @@ public class DndTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleSecondaryClick(@Nullable View view) {
- handleLongClick(view);
+ protected void handleSecondaryClick(@Nullable Expandable expandable) {
+ handleLongClick(expandable);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
index 4f0a63b667f3..0d3d980f71f4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DreamTile.java
@@ -32,12 +32,12 @@ import android.service.dreams.IDreamManager;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -153,7 +153,7 @@ public class DreamTile extends QSTileImpl<QSTile.BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
try {
if (mDreamManager.isDreaming()) {
mDreamManager.awaken();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
index f022981bd8db..848ff3c533ba 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -22,13 +22,13 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -99,7 +99,7 @@ public class FlashlightTile extends QSTileImpl<BooleanState> implements
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (ActivityManager.isUserAMonkey()) {
return;
}
@@ -114,8 +114,8 @@ public class FlashlightTile extends QSTileImpl<BooleanState> implements
}
@Override
- protected void handleLongClick(@Nullable View view) {
- handleClick(view);
+ protected void handleLongClick(@Nullable Expandable expandable) {
+ handleClick(expandable);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
index f5018a2868c0..078698c2872d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FontScalingTile.kt
@@ -19,12 +19,12 @@ import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.provider.Settings
-import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.MetricsLogger
import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
@@ -74,18 +74,23 @@ constructor(
return QSTile.State()
}
- override fun handleClick(view: View?) {
+ override fun handleClick(expandable: Expandable?) {
// We animate from the touched view only if we are not on the keyguard
- val animateFromView: Boolean = view != null && !keyguardStateController.isShowing
+ val animateFromExpandable: Boolean =
+ expandable != null && !keyguardStateController.isShowing
val runnable = Runnable {
val dialog: SystemUIDialog = fontScalingDialogDelegateProvider.get().createDialog()
- if (animateFromView) {
- dialogTransitionAnimator.showFromView(
- dialog,
- view!!,
- DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
- )
+ if (animateFromExpandable) {
+ val controller =
+ expandable?.dialogTransitionController(
+ DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG
+ )
+ )
+ controller?.let { dialogTransitionAnimator.show(dialog, controller) }
+ ?: dialog.show()
} else {
dialog.show()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
index 81a20260792a..183c1a4a7ce7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
@@ -20,13 +20,13 @@ import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
-import android.view.View;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Flags;
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -72,8 +72,8 @@ public class HearingDevicesTile extends QSTileImpl<State> {
}
@Override
- protected void handleClick(@Nullable View view) {
- mUiHandler.post(() -> mDialogManager.showDialog(view));
+ protected void handleClick(@Nullable Expandable expandable) {
+ mUiHandler.post(() -> mDialogManager.showDialog(expandable));
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index 4d0404dd007e..ea3993ea88a9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -25,7 +25,6 @@ import android.os.UserManager;
import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.util.Log;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -33,6 +32,7 @@ import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -112,7 +112,7 @@ public class HotspotTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
final boolean isEnabled = mState.value;
if (!isEnabled && mDataSaverController.isDataSaverEnabled()) {
return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index 0f260e3dc296..6d98da4bac61 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -30,7 +30,6 @@ import android.service.quicksettings.Tile;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -41,6 +40,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.graph.SignalDrawable;
import com.android.settingslib.mobile.TelephonyIcons;
import com.android.settingslib.net.DataUsageController;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -124,10 +124,10 @@ public class InternetTile extends QSTileImpl<QSTile.BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
mHandler.post(() -> mInternetDialogManager.create(true,
mAccessPointController.canConfigMobileData(),
- mAccessPointController.canConfigWifi(), view));
+ mAccessPointController.canConfigWifi(), expandable));
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index 357743bc2bd7..932dec5af950 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -20,9 +20,9 @@ import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.provider.Settings
-import android.view.View
import android.widget.Switch
import com.android.internal.logging.MetricsLogger
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
@@ -44,18 +44,18 @@ import javax.inject.Inject
class InternetTileNewImpl
@Inject
constructor(
- host: QSHost,
- uiEventLogger: QsEventLogger,
- @Background backgroundLooper: Looper,
- @Main private val mainHandler: Handler,
- falsingManager: FalsingManager,
- metricsLogger: MetricsLogger,
- statusBarStateController: StatusBarStateController,
- activityStarter: ActivityStarter,
- qsLogger: QSLogger,
- viewModel: InternetTileViewModel,
- private val internetDialogManager: InternetDialogManager,
- private val accessPointController: AccessPointController,
+ host: QSHost,
+ uiEventLogger: QsEventLogger,
+ @Background backgroundLooper: Looper,
+ @Main private val mainHandler: Handler,
+ falsingManager: FalsingManager,
+ metricsLogger: MetricsLogger,
+ statusBarStateController: StatusBarStateController,
+ activityStarter: ActivityStarter,
+ qsLogger: QSLogger,
+ viewModel: InternetTileViewModel,
+ private val internetDialogManager: InternetDialogManager,
+ private val accessPointController: AccessPointController,
) :
QSTileImpl<QSTile.BooleanState>(
host,
@@ -84,13 +84,13 @@ constructor(
return QSTile.BooleanState().also { it.forceExpandIcon = true }
}
- override fun handleClick(view: View?) {
+ override fun handleClick(expandable: Expandable?) {
mainHandler.post {
internetDialogManager.create(
aboveStatusBar = true,
accessPointController.canConfigMobileData(),
accessPointController.canConfigWifi(),
- view,
+ expandable,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
index b3f0d8bfbba8..cad5c0d12d1d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/LocationTile.java
@@ -22,13 +22,13 @@ import android.os.Looper;
import android.os.UserManager;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -92,7 +92,7 @@ public class LocationTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (mKeyguard.isMethodSecure() && mKeyguard.isShowing()) {
mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
final boolean wasEnabled = mState.value;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
index d650f73bc92b..136eea8331df 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NfcTile.java
@@ -27,13 +27,13 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -119,7 +119,7 @@ public class NfcTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (getAdapter() == null) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
index a1ea46d9d69b..ac762de6d544 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java
@@ -28,7 +28,6 @@ import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -36,6 +35,7 @@ import androidx.annotation.StringRes;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.NightDisplayListenerModule;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -112,7 +112,7 @@ public class NightDisplayTile extends QSTileImpl<BooleanState> implements
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
// Enroll in forced auto mode if eligible.
if ("1".equals(Settings.Global.getString(mContext.getContentResolver(),
Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE))
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
index b08e6a5580a2..450c95411c3f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/OneHandedModeTile.java
@@ -21,13 +21,13 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -114,7 +114,7 @@ public class OneHandedModeTile extends QSTileImpl<BooleanState> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
mSetting.setValue(mState.value ? 0 : 1);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
index de9a08e1cda8..9766fac7965e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QRCodeScannerTile.java
@@ -21,13 +21,13 @@ import android.os.Handler;
import android.os.Looper;
import android.service.quicksettings.Tile;
import android.util.Log;
-import android.view.View;
import androidx.annotation.Nullable;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -99,7 +99,7 @@ public class QRCodeScannerTile extends QSTileImpl<QSTile.State> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
Intent intent = mQRCodeScannerController.getIntent();
if (intent == null) {
// This should never happen as the fact that we are handling clicks means that the
@@ -109,7 +109,7 @@ public class QRCodeScannerTile extends QSTileImpl<QSTile.State> {
}
ActivityTransitionAnimator.Controller animationController =
- view == null ? null : ActivityTransitionAnimator.Controller.fromView(view,
+ expandable == null ? null : expandable.activityTransitionController(
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
mActivityStarter.startActivity(intent, true /* dismissShade */,
animationController, true /* showOverLockscreenWhenLocked */);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index e1b742e1e7f0..76aa146d0531 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -35,7 +35,6 @@ import android.service.quickaccesswallet.QuickAccessWalletClient;
import android.service.quickaccesswallet.WalletCard;
import android.service.quicksettings.Tile;
import android.util.Log;
-import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -44,6 +43,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -131,9 +131,9 @@ public class QuickAccessWalletTile extends QSTileImpl<QSTile.State> {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
ActivityTransitionAnimator.Controller animationController =
- view == null ? null : ActivityTransitionAnimator.Controller.fromView(view,
+ expandable == null ? null : expandable.activityTransitionController(
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE);
mUiHandler.post(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index b418a174d84e..9937ea468fa8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -24,7 +24,6 @@ import android.os.Handler
import android.os.Looper
import android.service.quicksettings.Tile
import android.text.TextUtils
-import android.view.View
import android.widget.Switch
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN
@@ -32,6 +31,7 @@ import com.android.internal.logging.MetricsLogger
import com.android.systemui.Flags.recordIssueQsTile
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
@@ -113,11 +113,11 @@ constructor(
}
@VisibleForTesting
- public override fun handleClick(view: View?) {
+ public override fun handleClick(expandable: Expandable?) {
if (issueRecordingState.isRecording) {
stopIssueRecordingService()
} else {
- mUiHandler.post { showPrompt(view) }
+ mUiHandler.post { showPrompt(expandable) }
}
}
@@ -143,7 +143,7 @@ constructor(
)
.send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
- private fun showPrompt(view: View?) {
+ private fun showPrompt(expandable: Expandable?) {
val dialog: AlertDialog =
delegateFactory
.create {
@@ -156,12 +156,11 @@ constructor(
ActivityStarter.OnDismissAction {
// We animate from the touched view only if we are not on the keyguard, given
// that if we are we will dismiss it which will also collapse the shade.
- if (view != null && !keyguardStateController.isShowing) {
- dialogTransitionAnimator.showFromView(
- dialog,
- view,
- DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)
- )
+ if (expandable != null && !keyguardStateController.isShowing) {
+ expandable
+ .dialogTransitionController(DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC))
+ ?.let { dialogTransitionAnimator.show(dialog, it) }
+ ?: dialog.show()
} else {
dialog.show()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
index 76ada102f26a..34723523b84f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ReduceBrightColorsTile.java
@@ -23,13 +23,13 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.R;
import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -97,7 +97,7 @@ public class ReduceBrightColorsTile extends QSTileImpl<QSTile.BooleanState>
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
mReduceBrightColorsController.setReduceBrightColorsActivated(!mState.value);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
index f1d8f9f25286..35e43b6fed9e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -29,13 +29,13 @@ import android.os.Looper;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -131,7 +131,7 @@ public class RotationLockTile extends QSTileImpl<BooleanState> implements
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
final boolean newState = !mState.value;
mController.setRotationLocked(!newState, /* caller= */ "RotationLockTile#handleClick");
refreshState(newState);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 1a90d439c6d8..4715230a0958 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -23,7 +23,6 @@ import android.os.Looper;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
@@ -32,6 +31,7 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
@@ -118,13 +118,13 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (mController.isStarting()) {
cancelCountdown();
} else if (mController.isRecording()) {
stopRecording();
} else {
- mUiHandler.post(() -> showPrompt(view));
+ mUiHandler.post(() -> showPrompt(expandable));
}
refreshState();
}
@@ -174,10 +174,11 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
return mContext.getString(R.string.quick_settings_screen_record_label);
}
- private void showPrompt(@Nullable View view) {
+ private void showPrompt(@Nullable Expandable expandable) {
// We animate from the touched view only if we are not on the keyguard, given that if we
// are we will dismiss it which will also collapse the shade.
- boolean shouldAnimateFromView = view != null && !mKeyguardStateController.isShowing();
+ boolean shouldAnimateFromExpandable =
+ expandable != null && !mKeyguardStateController.isShowing();
// Create the recording dialog that will collapse the shade only if we start the recording.
Runnable onStartRecordingClicked = () -> {
@@ -192,10 +193,17 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState>
mDialogTransitionAnimator, mActivityStarter, onStartRecordingClicked);
ActivityStarter.OnDismissAction dismissAction = () -> {
- if (shouldAnimateFromView) {
- mDialogTransitionAnimator.showFromView(dialog, view, new DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG),
- /* animateBackgroundBoundsChange= */ true);
+ if (shouldAnimateFromExpandable) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(new DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(dialog,
+ controller, /* animateBackgroundBoundsChange= */ true);
+ } else {
+ dialog.show();
+ }
} else {
dialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
index 3eeb2a3303be..036ce080c543 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java
@@ -26,13 +26,13 @@ import android.provider.Settings;
import android.safetycenter.SafetyCenterManager;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -100,7 +100,7 @@ public abstract class SensorPrivacyToggleTile extends QSTileImpl<QSTile.BooleanS
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
boolean blocked = mSensorPrivacyController.isSensorBlocked(getSensorId());
if (mSensorPrivacyController.requiresAuthentication()
&& mKeyguard.isMethodSecure()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
index d92873adafd8..bec6581e54c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java
@@ -24,13 +24,13 @@ import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -107,7 +107,7 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
if (getState().state == Tile.STATE_UNAVAILABLE) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
index abc481277e61..d9546ec6ac51 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -24,7 +24,6 @@ import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.view.View;
import android.widget.Switch;
import androidx.annotation.MainThread;
@@ -32,6 +31,7 @@ import androidx.annotation.Nullable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
@@ -88,7 +88,7 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements
}
@Override
- public void handleClick(@Nullable View view) {
+ public void handleClick(@Nullable Expandable expandable) {
mProfileController.setWorkModeEnabled(!mState.value);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
index 7192f58218a4..2d3120a1dcce 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
@@ -20,9 +20,9 @@ import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserHandle
-import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
@@ -33,11 +33,11 @@ import javax.inject.Inject
*/
interface QSTileIntentUserInputHandler {
- fun handle(view: View?, intent: Intent)
+ fun handle(expandable: Expandable?, intent: Intent)
/** @param requestLaunchingDefaultActivity used in case !pendingIndent.isActivity */
fun handle(
- view: View?,
+ expandable: Expandable?,
pendingIntent: PendingIntent,
requestLaunchingDefaultActivity: Boolean = false
)
@@ -52,31 +52,25 @@ constructor(
private val userHandle: UserHandle,
) : QSTileIntentUserInputHandler {
- override fun handle(view: View?, intent: Intent) {
+ override fun handle(expandable: Expandable?, intent: Intent) {
val animationController: ActivityTransitionAnimator.Controller? =
- view?.let {
- ActivityTransitionAnimator.Controller.fromView(
- it,
- InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
- )
- }
+ expandable?.activityTransitionController(
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+ )
activityStarter.postStartActivityDismissingKeyguard(intent, 0, animationController)
}
// TODO(b/249804373): make sure to allow showing activities over the lockscreen. See b/292112939
override fun handle(
- view: View?,
+ expandable: Expandable?,
pendingIntent: PendingIntent,
requestLaunchingDefaultActivity: Boolean
) {
if (pendingIntent.isActivity) {
val animationController: ActivityTransitionAnimator.Controller? =
- view?.let {
- ActivityTransitionAnimator.Controller.fromView(
- it,
- InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
- )
- }
+ expandable?.activityTransitionController(
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+ )
activityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController)
} else if (requestLaunchingDefaultActivity) {
val intent =
@@ -97,7 +91,7 @@ constructor(
?.let { resolved ->
intent.setPackage(null)
intent.setComponent(resolved.activityInfo.componentName)
- handle(view, intent)
+ handle(expandable, intent)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
index 5aef950b6a19..246fe3883e19 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogManager.kt
@@ -16,10 +16,10 @@
package com.android.systemui.qs.tiles.dialog
import android.util.Log
-import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -47,14 +47,14 @@ constructor(
}
/**
- * Creates a [InternetDialogDelegate]. The dialog will be animated from [view] if it is not
- * null.
+ * Creates a [InternetDialogDelegate]. The dialog will be animated from [expandable] if it is
+ * not null.
*/
fun create(
aboveStatusBar: Boolean,
canConfigMobileData: Boolean,
canConfigWifi: Boolean,
- view: View?
+ expandable: Expandable?
) {
if (dialog != null) {
if (DEBUG) {
@@ -67,20 +67,18 @@ constructor(
dialogFactory
.create(aboveStatusBar, canConfigMobileData, canConfigWifi, coroutineScope)
.createDialog()
- if (view != null) {
- dialogTransitionAnimator.showFromView(
+ val controller =
+ expandable?.dialogTransitionController(
+ DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
+ )
+ controller?.let {
+ dialogTransitionAnimator.show(
dialog!!,
- view,
- animateBackgroundBoundsChange = true,
- cuj =
- DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
- )
+ controller,
+ animateBackgroundBoundsChange = true
)
- } else {
- dialog!!.show()
}
+ ?: dialog?.show()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
index 9e13a56c49a1..bf0f8f6577de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/interactor/AirplaneModeTileUserActionInteractor.kt
@@ -45,7 +45,7 @@ constructor(
}
AirplaneModeInteractor.SetResult.BLOCKED_BY_ECM -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS),
)
}
@@ -53,7 +53,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
index 0ad520bd31ee..14fc57c0f83f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
@@ -40,9 +40,12 @@ constructor(
data.alarmClockInfo.showIntent != null
) {
val pendingIndent = data.alarmClockInfo.showIntent
- inputHandler.handle(action.view, pendingIndent, true)
+ inputHandler.handle(action.expandable, pendingIndent, true)
} else {
- inputHandler.handle(action.view, Intent(AlarmClock.ACTION_SHOW_ALARMS))
+ inputHandler.handle(
+ action.expandable,
+ Intent(AlarmClock.ACTION_SHOW_ALARMS)
+ )
}
}
is QSTileUserAction.LongClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
index 1e4eb38008bd..d4b4fe06ded8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/domain/interactor/BatterySaverTileUserActionInteractor.kt
@@ -39,12 +39,12 @@ constructor(
when (action) {
is QSTileUserAction.Click -> {
if (!data.isPluggedIn) {
- batteryController.setPowerSaveMode(!data.isPowerSaving, action.view)
+ batteryController.setPowerSaveMode(!data.isPowerSaving, action.expandable)
}
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
index d1838029db4e..534bd734f5bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/interactor/ColorCorrectionUserActionInteractor.kt
@@ -45,7 +45,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_COLOR_CORRECTION_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
index a16ac360e7e4..9bdf6316b069 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
@@ -29,9 +29,9 @@ import android.os.UserHandle
import android.provider.Settings
import android.service.quicksettings.TileService
import android.view.IWindowManager
-import android.view.View
import android.view.WindowManager
import androidx.annotation.GuardedBy
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
@@ -65,20 +65,21 @@ constructor(
@GuardedBy("token") private var isTokenGranted: Boolean = false
@GuardedBy("token") private var isShowingDialog: Boolean = false
- private val lastClickedView: AtomicReference<View> = AtomicReference<View>()
+ private val lastClickedExpandable: AtomicReference<Expandable> = AtomicReference<Expandable>()
override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) =
with(input) {
when (action) {
- is QSTileUserAction.Click -> click(action.view, data.tile.activityLaunchForClick)
+ is QSTileUserAction.Click ->
+ click(action.expandable, data.tile.activityLaunchForClick)
is QSTileUserAction.LongClick ->
- longClick(user, action.view, data.componentName, data.tile.state)
+ longClick(user, action.expandable, data.componentName, data.tile.state)
}
qsTileLogger.logCustomTileUserActionDelivered(tileSpec)
}
private suspend fun click(
- view: View?,
+ expandable: Expandable?,
activityLaunchForClick: PendingIntent?,
) {
grantToken()
@@ -86,10 +87,10 @@ constructor(
// Bind active tile to deliver user action
serviceInteractor.bindOnClick()
if (activityLaunchForClick == null) {
- lastClickedView.set(view)
+ lastClickedExpandable.set(expandable)
serviceInteractor.onClick(token)
} else {
- qsTileIntentUserInputHandler.handle(view, activityLaunchForClick)
+ qsTileIntentUserInputHandler.handle(expandable, activityLaunchForClick)
}
} catch (e: RemoteException) {
qsTileLogger.logError(tileSpec, "Failed to deliver click", e)
@@ -117,10 +118,10 @@ constructor(
if (!isTokenGranted) {
return
}
- qsTileIntentUserInputHandler.handle(lastClickedView.getAndSet(null), pendingIntent)
+ qsTileIntentUserInputHandler.handle(lastClickedExpandable.getAndSet(null), pendingIntent)
}
- fun clearLastClickedView() = lastClickedView.set(null)
+ fun clearLastClickedView() = lastClickedExpandable.set(null)
private fun grantToken() {
synchronized(token) {
@@ -142,7 +143,7 @@ constructor(
private suspend fun longClick(
user: UserHandle,
- view: View?,
+ expandable: Expandable?,
componentName: ComponentName,
state: Int
) {
@@ -159,14 +160,14 @@ constructor(
}
if (resolvedIntent == null) {
qsTileIntentUserInputHandler.handle(
- view,
+ expandable,
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(
Uri.fromParts(IntentFilter.SCHEME_PACKAGE, componentName.packageName, null)
)
)
} else {
- qsTileIntentUserInputHandler.handle(view, resolvedIntent)
+ qsTileIntentUserInputHandler.handle(expandable, resolvedIntent)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
index db8b1a5a5d47..d308ec889136 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/interactor/FontScalingTileUserActionInteractor.kt
@@ -52,21 +52,22 @@ constructor(
with(input) {
when (action) {
is QSTileUserAction.Click -> {
- // We animate from the touched view only if we are not on the keyguard
- val animateFromView: Boolean =
- action.view != null && !keyguardStateController.isShowing
+ // We animate from the touched expandable only if we are not on the keyguard
+ val animateFromExpandable: Boolean =
+ action.expandable != null && !keyguardStateController.isShowing
val runnable = Runnable {
val dialog: SystemUIDialog =
fontScalingDialogDelegateProvider.get().createDialog()
- if (animateFromView) {
- dialogTransitionAnimator.showFromView(
- dialog,
- action.view!!,
- DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
+ if (animateFromExpandable) {
+ action.expandable
+ ?.dialogTransitionController(
+ DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG
+ )
)
- )
+ ?.let { dialogTransitionAnimator.show(dialog, it) }
+ ?: dialog.show()
} else {
dialog.show()
}
@@ -84,7 +85,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_TEXT_READING_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index 2620cd555d8e..c0b089d84dc8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -49,13 +49,13 @@ constructor(
aboveStatusBar = true,
accessPointController.canConfigMobileData(),
accessPointController.canConfigWifi(),
- action.view,
+ action.expandable,
)
}
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_WIFI_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
index 43b58c83f7ef..d64327333cb6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/interactor/ColorInversionUserActionInteractor.kt
@@ -45,7 +45,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_COLOR_INVERSION_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
index 66705ead3cf0..77404aa82606 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileUserActionInteractor.kt
@@ -64,7 +64,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileDataInteractor.kt
new file mode 100644
index 000000000000..8c0fd2cd672a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileDataInteractor.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.domain
+
+import android.os.UserHandle
+import com.android.systemui.accessibility.data.repository.OneHandedModeRepository
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
+import com.android.wm.shell.onehanded.OneHanded
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Observes one handed mode state changes providing the [OneHandedModeTileModel]. */
+class OneHandedModeTileDataInteractor
+@Inject
+constructor(
+ private val oneHandedModeRepository: OneHandedModeRepository,
+) : QSTileDataInteractor<OneHandedModeTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<OneHandedModeTileModel> {
+ return oneHandedModeRepository.isEnabled(user).map { OneHandedModeTileModel(it) }
+ }
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(OneHanded.sIsSupportOneHandedMode)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt
new file mode 100644
index 000000000000..5cb0e181378b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/OneHandedModeTileUserActionInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.domain
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.OneHandedModeRepository
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles one handed mode tile clicks. */
+class OneHandedModeTileUserActionInteractor
+@Inject
+constructor(
+ private val oneHandedModeRepository: OneHandedModeRepository,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<OneHandedModeTileModel> {
+
+ override suspend fun handleInput(input: QSTileInput<OneHandedModeTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ oneHandedModeRepository.setIsEnabled(
+ !data.isEnabled,
+ user,
+ )
+ }
+ is QSTileUserAction.LongClick -> {
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_ONE_HANDED_SETTINGS)
+ )
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/model/OneHandedModeTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/model/OneHandedModeTileModel.kt
new file mode 100644
index 000000000000..7cebdfe84e2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/domain/model/OneHandedModeTileModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.domain.model
+
+/**
+ * One handed mode tile model.
+ *
+ * @param isEnabled is true when one handed mode is enabled;
+ */
+@JvmInline value class OneHandedModeTileModel(val isEnabled: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt
new file mode 100644
index 000000000000..9166ed8faacf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded.ui
+
+import android.content.res.Resources
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [OneHandedModeTileModel] to [QSTileState]. */
+class OneHandedModeTileMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Resources.Theme,
+) : QSTileDataToStateMapper<OneHandedModeTileModel> {
+
+ override fun map(config: QSTileConfig, data: OneHandedModeTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ val subtitleArray = resources.getStringArray(R.array.tile_states_onehanded)
+ label = resources.getString(R.string.quick_settings_onehanded_label)
+ icon = {
+ Icon.Loaded(
+ resources.getDrawable(
+ com.android.internal.R.drawable.ic_qs_one_handed_mode,
+ theme
+ ),
+ null
+ )
+ }
+ if (data.isEnabled) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ secondaryLabel = subtitleArray[2]
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ secondaryLabel = subtitleArray[1]
+ }
+ sideViewIcon = QSTileState.SideViewIcon.None
+ contentDescription = label
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
index 762f8635c562..14dbe0e8a69a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/domain/interactor/ReduceBrightColorsTileUserActionInteractor.kt
@@ -44,7 +44,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_REDUCE_BRIGHT_COLORS_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
index 8530926e68e0..34385ea815eb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/domain/interactor/RotationLockTileUserActionInteractor.kt
@@ -42,7 +42,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_AUTO_ROTATE_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
index 861faf5a0e4f..a5dc66c901f1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt
@@ -75,34 +75,32 @@ constructor(
// must be created and shown on the main thread, so we post it to the UI
// handler
withContext(coroutineContext) {
- val dialogContext = action.view?.context ?: context
val dialogDelegate =
DataSaverDialogDelegate(
systemUIDialogFactory,
- dialogContext,
+ context,
backgroundContext,
dataSaverController,
sharedPreferences
)
- val dialog = systemUIDialogFactory.create(dialogDelegate, dialogContext)
+ val dialog = systemUIDialogFactory.create(dialogDelegate, context)
- if (action.view != null) {
- dialogTransitionAnimator.showFromView(
- dialog,
- action.view!!,
+ action.expandable
+ ?.dialogTransitionController(
DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
INTERACTION_JANK_TAG
)
)
- } else {
- dialog.show()
- }
+ ?.let { controller ->
+ dialogTransitionAnimator.show(dialog, controller)
+ }
+ ?: dialog.show()
}
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_DATA_SAVER_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index d2bd09fe0ea3..79766d6642b3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -18,10 +18,10 @@ package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor
import android.content.Context
import android.util.Log
-import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -68,14 +68,16 @@ constructor(
is ScreenRecordTileModel.Recording ->
withContext(backgroundContext) { recordingController.stopRecording() }
is ScreenRecordTileModel.DoingNothing ->
- withContext(mainContext) { showPrompt(action.view, user.identifier) }
+ withContext(mainContext) {
+ showPrompt(action.expandable, user.identifier)
+ }
}
}
is QSTileUserAction.LongClick -> {} // no-op
}
}
- private fun showPrompt(view: View?, userId: Int) {
+ private fun showPrompt(expandable: Expandable?, userId: Int) {
// Create the recording dialog that will collapse the shade only if we start the recording.
val onStartRecordingClicked = Runnable {
// We dismiss the shade. Since starting the recording will also dismiss the dialog, we
@@ -99,21 +101,29 @@ constructor(
return
}
- // We animate from the touched view only if we are not on the keyguard, given that if we
+ // We animate from the touched expandable only if we are not on the keyguard, given that if
+ // we
// are we will dismiss it which will also collapse the shade.
- val shouldAnimateFromView = view != null && !keyguardInteractor.isKeyguardShowing()
+ val shouldAnimateFromExpandable =
+ expandable != null && !keyguardInteractor.isKeyguardShowing()
val dismissAction =
ActivityStarter.OnDismissAction {
- if (shouldAnimateFromView) {
- dialogTransitionAnimator.showFromView(
- dialog,
- view!!,
- DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
- ),
- animateBackgroundBoundsChange = true
- )
+ if (shouldAnimateFromExpandable) {
+ val controller =
+ expandable?.dialogTransitionController(
+ DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG
+ )
+ )
+ controller?.let {
+ dialogTransitionAnimator.show(
+ dialog,
+ controller,
+ animateBackgroundBoundsChange = true,
+ )
+ }
+ ?: dialog.show()
} else {
dialog.show()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
index 9711cb81e2c0..f22a4269b3f5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/domain/SensorPrivacyToggleTileUserActionInteractor.kt
@@ -80,7 +80,7 @@ constructor(
}
)
}
- qsTileIntentUserActionHandler.handle(action.view, longClickIntent)
+ qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
index 00d7a629f5be..f8dd1730cc10 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileUserActionInteractor.kt
@@ -50,7 +50,7 @@ constructor(
}
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_DARK_THEME_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
index f765f8b3ac77..031e4d978739 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt
@@ -44,7 +44,7 @@ constructor(
is QSTileUserAction.LongClick -> {
if (data is WorkModeTileModel.HasActiveProfile) {
qsTileIntentUserActionHandler.handle(
- action.view,
+ action.expandable,
Intent(Settings.ACTION_MANAGED_PROFILE_SETTINGS)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
index a1450420131b..acb29362681b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileUserAction.kt
@@ -16,12 +16,12 @@
package com.android.systemui.qs.tiles.viewmodel
-import android.view.View
+import com.android.systemui.animation.Expandable
sealed interface QSTileUserAction {
- val view: View?
+ val expandable: Expandable?
- class Click(override val view: View?) : QSTileUserAction
- class LongClick(override val view: View?) : QSTileUserAction
+ class Click(override val expandable: Expandable?) : QSTileUserAction
+ class LongClick(override val expandable: Expandable?) : QSTileUserAction
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 5a389f3fe701..b88c1e566c4a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -19,10 +19,10 @@ package com.android.systemui.qs.tiles.viewmodel
import android.content.Context
import android.os.UserHandle
import android.util.Log
-import android.view.View
import androidx.annotation.GuardedBy
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
+import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.plugins.qs.QSTile
@@ -126,21 +126,21 @@ constructor(
synchronized(callbacks) { callbacks.clear() }
}
- override fun click(view: View?) {
+ override fun click(expandable: Expandable?) {
if (isActionSupported(QSTileState.UserAction.CLICK)) {
- qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view))
+ qsTileViewModel.onActionPerformed(QSTileUserAction.Click(expandable))
}
}
- override fun secondaryClick(view: View?) {
+ override fun secondaryClick(expandable: Expandable?) {
if (isActionSupported(QSTileState.UserAction.CLICK)) {
- qsTileViewModel.onActionPerformed(QSTileUserAction.Click(view))
+ qsTileViewModel.onActionPerformed(QSTileUserAction.Click(expandable))
}
}
- override fun longClick(view: View?) {
+ override fun longClick(expandable: Expandable?) {
if (isActionSupported(QSTileState.UserAction.LONG_CLICK)) {
- qsTileViewModel.onActionPerformed(QSTileUserAction.LongClick(view))
+ qsTileViewModel.onActionPerformed(QSTileUserAction.LongClick(expandable))
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index 3d86e3c084f8..63acbb0d730d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -49,21 +49,44 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
// TODO(307945185) Split View concerns into a ViewBinder
/** Adapter to use between Scene system and [QSImpl] */
interface QSSceneAdapter {
- /** Whether [QSImpl] is currently customizing */
+
+ /**
+ * Whether we are currently customizing or entering the customizer.
+ *
+ * @see CustomizerState.isCustomizing
+ */
val isCustomizing: StateFlow<Boolean>
/**
+ * Whether the customizer is showing. This includes animating into and out of it.
+ *
+ * @see CustomizerState.isShowing
+ */
+ val isCustomizerShowing: StateFlow<Boolean>
+
+ /**
+ * The duration of the current animation in/out of customizer. If not in an animating state,
+ * this duration is 0 (to match show/hide immediately).
+ *
+ * @see CustomizerState.Animating.animationDuration
+ */
+ val customizerAnimationDuration: StateFlow<Int>
+
+ /**
* A view with the QS content ([QSContainerImpl]), managed by an instance of [QSImpl] tracked by
* the interactor.
*/
@@ -181,8 +204,35 @@ constructor(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
- private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false)
- override val isCustomizing = _isCustomizing.asStateFlow()
+ private val _customizingState: MutableStateFlow<CustomizerState> =
+ MutableStateFlow(CustomizerState.Hidden)
+ val customizerState = _customizingState.asStateFlow()
+
+ override val isCustomizing: StateFlow<Boolean> =
+ customizerState
+ .map { it.isCustomizing }
+ .stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ customizerState.value.isCustomizing,
+ )
+ override val isCustomizerShowing: StateFlow<Boolean> =
+ customizerState
+ .map { it.isShowing }
+ .stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ customizerState.value.isShowing
+ )
+ override val customizerAnimationDuration: StateFlow<Int> =
+ customizerState
+ .map { (it as? CustomizerState.Animating)?.animationDuration?.toInt() ?: 0 }
+ .stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ (customizerState.value as? CustomizerState.Animating)?.animationDuration?.toInt()
+ ?: 0,
+ )
private val _qsImpl: MutableStateFlow<QSImpl?> = MutableStateFlow(null)
val qsImpl = _qsImpl.asStateFlow()
@@ -209,9 +259,9 @@ constructor(
dumpManager.registerDumpable(this)
applicationScope.launch {
launch {
- state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
+ state.sample(_customizingState, ::Pair).collect { (state, customizing) ->
qsImpl.value?.apply {
- if (state != QSSceneAdapter.State.QS && customizing) {
+ if (state != QSSceneAdapter.State.QS && customizing.isShowing) {
this@apply.closeCustomizerImmediately()
}
applyState(state)
@@ -243,14 +293,38 @@ constructor(
}
}
- override fun setCustomizerAnimating(animating: Boolean) {}
+ override fun setCustomizerAnimating(animating: Boolean) {
+ if (_customizingState.value is CustomizerState.Animating && !animating) {
+ _customizingState.update {
+ if (it is CustomizerState.AnimatingIntoCustomizer) {
+ CustomizerState.Showing
+ } else {
+ CustomizerState.Hidden
+ }
+ }
+ }
+ }
override fun setCustomizerShowing(showing: Boolean) {
- _isCustomizing.value = showing
+ setCustomizerShowing(showing, 0L)
}
override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
- setCustomizerShowing(showing)
+ _customizingState.update { _ ->
+ if (showing) {
+ if (animationDuration > 0) {
+ CustomizerState.AnimatingIntoCustomizer(animationDuration)
+ } else {
+ CustomizerState.Showing
+ }
+ } else {
+ if (animationDuration > 0) {
+ CustomizerState.AnimatingOutOfCustomizer(animationDuration)
+ } else {
+ CustomizerState.Hidden
+ }
+ }
+ }
}
override fun setDetailShowing(showing: Boolean) {}
@@ -302,9 +376,50 @@ constructor(
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
println("Last state: ${state.value}")
- println("Customizing: ${isCustomizing.value}")
+ println("CustomizerState: ${_customizingState.value}")
println("QQS height: $qqsHeight")
println("QS height: $qsHeight")
}
}
}
+
+/** Current state of the customizer */
+sealed interface CustomizerState {
+
+ /**
+ * This indicates that some part of the customizer is showing. It could be animating in or out.
+ */
+ val isShowing: Boolean
+ get() = true
+
+ /**
+ * This indicates that we are currently customizing or animating into it. In particular, when
+ * animating out, this is false.
+ *
+ * @see QSCustomizer.isCustomizing
+ */
+ val isCustomizing: Boolean
+ get() = false
+
+ sealed interface Animating : CustomizerState {
+ val animationDuration: Long
+ }
+
+ /** Customizer is completely hidden, and not animating */
+ data object Hidden : CustomizerState {
+ override val isShowing = false
+ }
+
+ /** Customizer is completely showing, and not animating */
+ data object Showing : CustomizerState {
+ override val isCustomizing = true
+ }
+
+ /** Animating from [Hidden] into [Showing]. */
+ data class AnimatingIntoCustomizer(override val animationDuration: Long) : Animating {
+ override val isCustomizing = true
+ }
+
+ /** Animating from [Showing] into [Hidden]. */
+ data class AnimatingOutOfCustomizer(override val animationDuration: Long) : Animating
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 257c4d58f569..17698f9d8647 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -79,13 +79,13 @@ constructor(
combine(
deviceEntryInteractor.isUnlocked,
deviceEntryInteractor.canSwipeToEnter,
- qsSceneAdapter.isCustomizing,
+ qsSceneAdapter.isCustomizerShowing,
backScene,
- ) { isUnlocked, canSwipeToDismiss, isCustomizing, backScene ->
+ ) { isUnlocked, canSwipeToDismiss, isCustomizerShowing, backScene ->
destinationScenes(
isUnlocked,
canSwipeToDismiss,
- isCustomizing,
+ isCustomizerShowing,
backScene,
)
}
@@ -96,7 +96,7 @@ constructor(
destinationScenes(
isUnlocked = deviceEntryInteractor.isUnlocked.value,
canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
- isCustomizing = qsSceneAdapter.isCustomizing.value,
+ isCustomizing = qsSceneAdapter.isCustomizerShowing.value,
backScene = backScene.value,
),
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
new file mode 100644
index 000000000000..d48d55dd9918
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.ui.viewmodel
+
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/** Models UI state and handles user input for the Quick Settings Shade scene. */
+@SysUISingleton
+class QuickSettingsShadeSceneViewModel
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ overlayShadeViewModel: OverlayShadeViewModel,
+) {
+ val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
+ overlayShadeViewModel.backgroundScene
+ .map(::destinationScenes)
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = destinationScenes(overlayShadeViewModel.backgroundScene.value),
+ )
+
+ private fun destinationScenes(backgroundScene: SceneKey): Map<UserAction, UserActionResult> {
+ return mapOf(
+ Swipe.Up to backgroundScene,
+ Back to backgroundScene,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index b92e8eb72353..76bd80ff6f79 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -37,7 +37,6 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_F
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_GOING_AWAY;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_TRANSITION;
@@ -74,6 +73,7 @@ import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
+import com.android.compose.animation.scene.SceneKey;
import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AssistUtils;
@@ -103,6 +103,8 @@ import com.android.systemui.scene.shared.model.Scenes;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeViewController;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
+import com.android.systemui.shade.shared.model.ShadeMode;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.system.QuickStepContract;
@@ -115,8 +117,6 @@ import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder;
import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellInterface;
-import dagger.Lazy;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -128,6 +128,8 @@ import java.util.function.Supplier;
import javax.inject.Inject;
import javax.inject.Provider;
+import dagger.Lazy;
+
/**
* Class to send information from overview to launcher with a binder.
*/
@@ -155,6 +157,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private final ScreenPinningRequest mScreenPinningRequest;
private final NotificationShadeWindowController mStatusBarWinController;
private final Provider<SceneInteractor> mSceneInteractor;
+ private final Provider<ShadeInteractor> mShadeInteractor;
private final Runnable mConnectionRunnable = () ->
internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
@@ -243,7 +246,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
// Gesture was too short to be picked up by scene container touch
// handling; programmatically start the transition to shade scene.
mSceneInteractor.get().changeScene(
- Scenes.Shade, "short launcher swipe");
+ getShadeSceneKey(),
+ "short launcher swipe"
+ );
}
}
event.recycle();
@@ -261,7 +266,9 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
"trackpad swipe");
} else if (action == ACTION_UP) {
mSceneInteractor.get().changeScene(
- Scenes.Shade, "short trackpad swipe");
+ getShadeSceneKey(),
+ "short trackpad swipe"
+ );
}
mStatusBarWinController.getWindowRootView().dispatchTouchEvent(event);
} else {
@@ -618,6 +625,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
NotificationShadeWindowController statusBarWinController,
SysUiState sysUiState,
Provider<SceneInteractor> sceneInteractor,
+ Provider<ShadeInteractor> shadeInteractor,
UserTracker userTracker,
WakefulnessLifecycle wakefulnessLifecycle,
UiEventLogger uiEventLogger,
@@ -644,6 +652,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mScreenPinningRequest = screenPinningRequest;
mStatusBarWinController = statusBarWinController;
mSceneInteractor = sceneInteractor;
+ mShadeInteractor = shadeInteractor;
mUserTracker = userTracker;
mConnectionBackoffAttempts = 0;
mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
@@ -691,8 +700,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
// Listen for tracing state changes
@Override
public void onTracingStateChanged(boolean enabled) {
- mSysUiState.setFlag(SYSUI_STATE_TRACING_ENABLED, enabled)
- .commitUpdate(mContext.getDisplayId());
+ // TODO(b/286509643) Cleanup callers of this; Unused downstream
}
@Override
@@ -909,6 +917,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
}
+ private SceneKey getShadeSceneKey() {
+ return mShadeInteractor.get().getShadeMode().getValue() == ShadeMode.dual()
+ ? Scenes.NotificationsShade
+ : Scenes.Shade;
+ }
+
private void notifyHomeRotationEnabled(boolean enabled) {
for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
mConnectionCallbacks.get(i).onHomeRotationEnabled(enabled);
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 063a52c43a1b..28569d817279 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -21,6 +21,7 @@ import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInte
import com.android.systemui.scene.domain.startable.SceneContainerStartable
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -33,6 +34,7 @@ import dagger.multibindings.IntoMap
[
EmptySceneModule::class,
GoneSceneModule::class,
+ NotificationsShadeSceneModule::class,
QuickSettingsSceneModule::class,
ShadeSceneModule::class,
],
@@ -59,18 +61,24 @@ interface KeyguardlessSceneContainerFrameworkModule {
// Note that this list is in z-order. The first one is the bottom-most and the
// last one is top-most.
sceneKeys =
- listOf(
+ listOfNotNull(
Scenes.Gone,
- Scenes.QuickSettings,
- Scenes.Shade,
+ Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
+ Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled },
+ Scenes.NotificationsShade.takeIf { DualShade.isEnabled },
+ Scenes.Shade.takeUnless { DualShade.isEnabled },
),
initialSceneKey = Scenes.Gone,
navigationDistances =
mapOf(
- Scenes.Gone to 0,
- Scenes.Shade to 1,
- Scenes.QuickSettings to 2,
- ),
+ Scenes.Gone to 0,
+ Scenes.NotificationsShade to 1.takeIf { DualShade.isEnabled },
+ Scenes.Shade to 1.takeUnless { DualShade.isEnabled },
+ Scenes.QuickSettingsShade to 2.takeIf { DualShade.isEnabled },
+ Scenes.QuickSettings to 2.takeUnless { DualShade.isEnabled },
+ )
+ .filterValues { it != null }
+ .mapValues { checkNotNull(it.value) }
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index cd1b96508cce..dbe0342a5319 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -22,6 +22,7 @@ import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInte
import com.android.systemui.scene.domain.startable.SceneContainerStartable
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -40,6 +41,8 @@ import dagger.multibindings.IntoMap
LockscreenSceneModule::class,
QuickSettingsSceneModule::class,
ShadeSceneModule::class,
+ QuickSettingsShadeSceneModule::class,
+ NotificationsShadeSceneModule::class,
],
)
interface SceneContainerFrameworkModule {
@@ -61,27 +64,33 @@ interface SceneContainerFrameworkModule {
@Provides
fun containerConfig(): SceneContainerConfig {
return SceneContainerConfig(
- // Note that this list is in z-order. The first one is the bottom-most and the
- // last one is top-most.
+ // Note that this list is in z-order. The first one is the bottom-most and the last
+ // one is top-most.
sceneKeys =
- listOf(
+ listOfNotNull(
Scenes.Gone,
Scenes.Communal,
Scenes.Lockscreen,
Scenes.Bouncer,
- Scenes.QuickSettings,
- Scenes.Shade,
+ Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
+ Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled },
+ Scenes.NotificationsShade.takeIf { DualShade.isEnabled },
+ Scenes.Shade.takeUnless { DualShade.isEnabled },
),
initialSceneKey = Scenes.Lockscreen,
navigationDistances =
mapOf(
- Scenes.Gone to 0,
- Scenes.Lockscreen to 0,
- Scenes.Communal to 1,
- Scenes.Shade to 2,
- Scenes.QuickSettings to 3,
- Scenes.Bouncer to 4,
- ),
+ Scenes.Gone to 0,
+ Scenes.Lockscreen to 0,
+ Scenes.Communal to 1,
+ Scenes.NotificationsShade to 2.takeIf { DualShade.isEnabled },
+ Scenes.Shade to 2.takeUnless { DualShade.isEnabled },
+ Scenes.QuickSettingsShade to 3.takeIf { DualShade.isEnabled },
+ Scenes.QuickSettings to 3.takeUnless { DualShade.isEnabled },
+ Scenes.Bouncer to 4,
+ )
+ .filterValues { it != null }
+ .mapValues { checkNotNull(it.value) }
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index 5748ad459ed6..eabc42b02665 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -87,6 +87,14 @@ constructor(
)
}
+ fun snapToScene(
+ toScene: SceneKey,
+ ) {
+ dataSource.snapToScene(
+ toScene = toScene,
+ )
+ }
+
/** Sets whether the container is visible. */
fun setVisible(isVisible: Boolean) {
_isVisible.value = isVisible
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index c59018b61724..6bcd92316106 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -125,7 +125,9 @@ constructor(
Scenes.Communal -> true
Scenes.Gone -> true
Scenes.Lockscreen -> true
+ Scenes.NotificationsShade -> false
Scenes.QuickSettings -> false
+ Scenes.QuickSettingsShade -> false
Scenes.Shade -> false
else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
}
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 93cef61d9dae..08efe39d7674 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
@@ -162,19 +162,45 @@ constructor(
loggingReason: String,
transitionKey: TransitionKey? = null,
) {
- if (!repository.allSceneKeys().contains(toScene)) {
+ val currentSceneKey = currentScene.value
+ if (
+ !validateSceneChange(
+ from = currentSceneKey,
+ to = toScene,
+ loggingReason = loggingReason,
+ )
+ ) {
return
}
- check(
- toScene != Scenes.Gone || deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked
- ) {
- "Cannot change to the Gone scene while the device is locked. Logging reason for scene" +
- " change was: $loggingReason"
- }
+ logger.logSceneChangeRequested(
+ from = currentSceneKey,
+ to = toScene,
+ reason = loggingReason,
+ isInstant = false,
+ )
+
+ repository.changeScene(toScene, transitionKey)
+ }
+ /**
+ * Requests a scene change to the given scene.
+ *
+ * The change is instantaneous and not animated; it will be observable in the next frame and
+ * there will be no transition animation.
+ */
+ fun snapToScene(
+ toScene: SceneKey,
+ loggingReason: String,
+ ) {
val currentSceneKey = currentScene.value
- if (currentSceneKey == toScene) {
+ if (
+ !validateSceneChange(
+ from = currentSceneKey,
+ to = toScene,
+ loggingReason = loggingReason,
+ )
+ ) {
return
}
@@ -182,9 +208,10 @@ constructor(
from = currentSceneKey,
to = toScene,
reason = loggingReason,
+ isInstant = true,
)
- repository.changeScene(toScene, transitionKey)
+ repository.snapToScene(toScene)
}
/**
@@ -249,4 +276,32 @@ constructor(
): Boolean {
return raw || isRemoteUserInteractionOngoing
}
+
+ /**
+ * Validates that the given scene change is allowed.
+ *
+ * Will throw a runtime exception for illegal states (for example, attempting to change to a
+ * scene that's not part of the current scene framework configuration).
+ *
+ * @param from The current scene being transitioned away from
+ * @param to The desired destination scene to transition to
+ * @param loggingReason The reason why the transition is requested, for logging purposes
+ * @return `true` if the scene change is valid; `false` if it shouldn't happen
+ */
+ private fun validateSceneChange(
+ from: SceneKey,
+ to: SceneKey,
+ loggingReason: String,
+ ): Boolean {
+ if (!repository.allSceneKeys().contains(to)) {
+ return false
+ }
+
+ check(to != Scenes.Gone || deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked) {
+ "Cannot change to the Gone scene while the device is locked. Logging reason for scene" +
+ " change was: $loggingReason"
+ }
+
+ return from != to
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
index e0b8b85a122b..9c2b992c0de6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
@@ -51,7 +51,7 @@ constructor(
private val windowRootViewVisibilityRepository: WindowRootViewVisibilityRepository,
private val keyguardRepository: KeyguardRepository,
private val headsUpManager: HeadsUpManager,
- private val powerInteractor: PowerInteractor,
+ powerInteractor: PowerInteractor,
private val activeNotificationsInteractor: ActiveNotificationsInteractor,
sceneInteractorProvider: Provider<SceneInteractor>,
) : CoreStartable {
@@ -77,11 +77,17 @@ constructor(
when (state) {
is ObservableTransitionState.Idle ->
state.currentScene == Scenes.Shade ||
+ state.currentScene == Scenes.NotificationsShade ||
+ state.currentScene == Scenes.QuickSettingsShade ||
state.currentScene == Scenes.Lockscreen
is ObservableTransitionState.Transition ->
state.toScene == Scenes.Shade ||
+ state.toScene == Scenes.NotificationsShade ||
+ state.toScene == Scenes.QuickSettingsShade ||
state.toScene == Scenes.Lockscreen ||
state.fromScene == Scenes.Shade ||
+ state.fromScene == Scenes.NotificationsShade ||
+ state.fromScene == Scenes.QuickSettingsShade ||
state.fromScene == Scenes.Lockscreen
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index f64e0af12d2c..9e5796449976 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -537,6 +537,7 @@ constructor(
Scenes.Lockscreen -> true
Scenes.Bouncer -> false
Scenes.Shade -> false
+ Scenes.NotificationsShade -> false
else -> null
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index 5ebdd8698656..812141928e3c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -47,6 +47,7 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
from: SceneKey,
to: SceneKey,
reason: String,
+ isInstant: Boolean,
) {
logBuffer.log(
tag = TAG,
@@ -55,8 +56,17 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
str1 = from.toString()
str2 = to.toString()
str3 = reason
+ bool1 = isInstant
+ },
+ messagePrinter = {
+ buildString {
+ append("Scene change requested: $str1 → $str2")
+ if (isInstant) {
+ append(" (instant)")
+ }
+ append(", reason: $str3")
+ }
},
- messagePrinter = { "Scene change requested: $str1 → $str2, reason: $str3" },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
index 0e078d5d8064..034da25f1a45 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
@@ -40,4 +40,11 @@ interface SceneDataSource {
toScene: SceneKey,
transitionKey: TransitionKey? = null,
)
+
+ /**
+ * Asks for an instant scene switch to [toScene], without an animated transition of any kind.
+ */
+ fun snapToScene(
+ toScene: SceneKey,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
index 2fbcba977a91..43c3635f32fc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -56,6 +56,12 @@ class SceneDataSourceDelegator(
)
}
+ override fun snapToScene(toScene: SceneKey) {
+ delegateMutable.value.snapToScene(
+ toScene = toScene,
+ )
+ }
+
/**
* Binds the current, dependency injection provided [SceneDataSource] to the given object.
*
@@ -77,5 +83,7 @@ class SceneDataSourceDelegator(
MutableStateFlow(initialSceneKey).asStateFlow()
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
+
+ override fun snapToScene(toScene: SceneKey) = Unit
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
index 73fcca8c6b7f..6d139da99345 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
@@ -42,11 +42,58 @@ object Scenes {
/** The lockscreen is the scene that shows when the device is locked. */
@JvmField val Lockscreen = SceneKey("lockscreen")
- /** The quick settings scene shows the quick setting tiles. */
+ /**
+ * The notifications shade scene primarily shows a scrollable list of notifications as an
+ * overlay UI.
+ *
+ * It's used only in the dual shade configuration, where there are two separate shades: one for
+ * notifications (this scene) and another for [QuickSettingsShade].
+ *
+ * It's not used in the single/accordion configuration (swipe down once to reveal the shade,
+ * swipe down again the to expand quick settings) or in the "split" shade configuration (on
+ * large screens or unfolded foldables, where notifications and quick settings are shown
+ * side-by-side in their own columns).
+ */
+ @JvmField val NotificationsShade = SceneKey("notifications_shade")
+
+ /**
+ * The quick settings scene shows the quick setting tiles.
+ *
+ * This scene is used for single/accordion configuration (swipe down once to reveal the shade,
+ * swipe down again the to expand quick settings).
+ *
+ * For the "split" shade configuration (on large screens or unfolded foldables, where
+ * notifications and quick settings are shown side-by-side in their own columns), the [Shade]
+ * scene is used].
+ *
+ * For the dual shade configuration, where there are two separate shades: one for notifications
+ * and one for quick settings, [NotificationsShade] and [QuickSettingsShade] scenes are used
+ * respectively.
+ */
@JvmField val QuickSettings = SceneKey("quick_settings")
/**
- * The shade is the scene whose primary purpose is to show a scrollable list of notifications.
+ * The quick settings shade scene shows the quick setting tiles as an overlay UI.
+ *
+ * It's used only in the dual shade configuration, where there are two separate shades: one for
+ * quick settings (this scene) and another for [NotificationsShade].
+ *
+ * It's not used in the single/accordion configuration (swipe down once to reveal the shade,
+ * swipe down again the to expand quick settings) or in the "split" shade configuration (on
+ * large screens or unfolded foldables, where notifications and quick settings are shown
+ * side-by-side in their own columns).
+ */
+ @JvmField val QuickSettingsShade = SceneKey("quick_settings_shade")
+
+ /**
+ * The shade is the scene that shows a scrollable list of notifications and the minimized
+ * version of quick settings (AKA "quick quick settings" or "QQS").
+ *
+ * This scene is used for single/accordion configuration (swipe down once to reveal the shade,
+ * swipe down again the to expand quick settings) and for the "split" shade configuration (on
+ * large screens or unfolded foldables, where notifications and quick settings are shown
+ * side-by-side in their own columns). For the dual shade configuration, where there are two
+ * separate shades: one for notifications and one for quick settings, other scenes are used.
*/
@JvmField val Shade = SceneKey("shade")
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index 451fd679969a..b0af7f9ce072 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -51,7 +51,7 @@ constructor(
private fun destinationScenes(shadeMode: ShadeMode): Map<UserAction, UserActionResult> {
return buildMap {
- if (shadeMode == ShadeMode.Single) {
+ if (shadeMode is ShadeMode.Single) {
this[
Swipe(
pointerCount = 2,
@@ -60,7 +60,20 @@ constructor(
)] = UserActionResult(Scenes.QuickSettings)
}
- this[Swipe(direction = SwipeDirection.Down)] = UserActionResult(Scenes.Shade)
+ // TODO(b/338577208): Remove this once we add Dual Shade invocation zones.
+ if (shadeMode is ShadeMode.Dual) {
+ this[
+ Swipe(
+ pointerCount = 2,
+ fromSource = Edge.Top,
+ direction = SwipeDirection.Down,
+ )] = UserActionResult(Scenes.QuickSettingsShade)
+ }
+
+ this[Swipe(direction = SwipeDirection.Down)] =
+ UserActionResult(
+ if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+ )
}
}
}
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 ef7829f91723..09c80b09a388 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
@@ -124,8 +124,10 @@ constructor(
when (toScene) {
Scenes.Bouncer -> Classifier.BOUNCER_UNLOCK
Scenes.Gone -> Classifier.UNLOCK
+ Scenes.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN
Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN
Scenes.QuickSettings -> Classifier.QUICK_SETTINGS
+ Scenes.QuickSettingsShade -> Classifier.QUICK_SETTINGS
else -> null
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
index 07e143a34319..ef1d87da47be 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -87,7 +87,8 @@ constructor(
AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
context.resources.getString(R.string.screenshot_edit_label),
context.resources.getString(R.string.screenshot_edit_description),
- )
+ ),
+ showDuringEntrance = true,
) {
debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
@@ -105,7 +106,8 @@ constructor(
AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
context.resources.getString(R.string.screenshot_share_label),
context.resources.getString(R.string.screenshot_share_description),
- )
+ ),
+ showDuringEntrance = true,
) {
debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" }
uiEventLogger.log(SCREENSHOT_SHARE_TAPPED, 0, request.packageNameString)
@@ -125,7 +127,8 @@ constructor(
AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_scroll),
context.resources.getString(R.string.screenshot_scroll_label),
context.resources.getString(R.string.screenshot_scroll_label),
- )
+ ),
+ showDuringEntrance = true,
) {
onClick.run()
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 9b5e71827ead..412b08905ae9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -45,6 +45,7 @@ import com.android.systemui.screenshot.scroll.ScrollCaptureController
import com.android.systemui.screenshot.ui.ScreenshotAnimationController
import com.android.systemui.screenshot.ui.ScreenshotShelfView
import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
+import com.android.systemui.screenshot.ui.viewmodel.AnimationState
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -119,12 +120,19 @@ constructor(
override fun updateOrientation(insets: WindowInsets) {}
override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
- val entrance = animationController.getEntranceAnimation(screenRect, showFlash)
- entrance.doOnStart { thumbnailObserver.onEntranceStarted() }
+ val entrance =
+ animationController.getEntranceAnimation(screenRect, showFlash) {
+ viewModel.setAnimationState(AnimationState.ENTRANCE_REVEAL)
+ }
+ entrance.doOnStart {
+ thumbnailObserver.onEntranceStarted()
+ viewModel.setAnimationState(AnimationState.ENTRANCE_STARTED)
+ }
entrance.doOnEnd {
// reset the timeout when animation finishes
callbacks?.onUserInteraction()
thumbnailObserver.onEntranceComplete()
+ viewModel.setAnimationState(AnimationState.ENTRANCE_COMPLETE)
}
return entrance
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
index da268300e8c4..06e88f46c5f1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -47,7 +47,11 @@ class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
view.requireViewById(R.id.screenshot_dismiss_button)
)
- fun getEntranceAnimation(bounds: Rect, showFlash: Boolean): Animator {
+ fun getEntranceAnimation(
+ bounds: Rect,
+ showFlash: Boolean,
+ onRevealMilestone: () -> Unit
+ ): Animator {
val entranceAnimation = AnimatorSet()
val previewAnimator = getPreviewAnimator(bounds)
@@ -70,7 +74,19 @@ class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
entranceAnimation.doOnStart { screenshotPreview.visibility = View.INVISIBLE }
}
- entranceAnimation.play(getActionsAnimator()).with(previewAnimator)
+ val actionsAnimator = getActionsAnimator()
+ entranceAnimation.play(actionsAnimator).with(previewAnimator)
+
+ // This isn't actually animating anything but is basically a timer for the first 200ms of
+ // the entrance animation. Using an animator here ensures that this is scaled if we change
+ // animator duration scales.
+ val revealMilestoneAnimator =
+ ValueAnimator.ofFloat(0f).apply {
+ duration = 0
+ startDelay = ACTION_REVEAL_DELAY_MS
+ doOnEnd { onRevealMilestone() }
+ }
+ entranceAnimation.play(revealMilestoneAnimator).with(actionsAnimator)
val fadeInAnimator = ValueAnimator.ofFloat(0f, 1f)
fadeInAnimator.addUpdateListener {
@@ -198,5 +214,6 @@ class ScreenshotAnimationController(private val view: ScreenshotShelfView) {
private const val FLASH_OUT_DURATION_MS: Long = 217
private const val PREVIEW_X_ANIMATION_DURATION_MS: Long = 234
private const val PREVIEW_Y_ANIMATION_DURATION_MS: Long = 500
+ private const val ACTION_REVEAL_DELAY_MS: Long = 200
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt
new file mode 100644
index 000000000000..0bc280c6c1e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.screenshot.ui
+
+import android.animation.ValueAnimator
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.drawable.Drawable
+import androidx.core.animation.doOnEnd
+import java.util.Objects
+
+/** */
+class TransitioningIconDrawable : Drawable() {
+ // The drawable for the current icon of this view. During icon transitions, this is the one
+ // being animated out.
+ private var drawable: Drawable? = null
+
+ // The incoming new icon. Only populated during transition animations (when drawable is also
+ // non-null).
+ private var enteringDrawable: Drawable? = null
+ private var colorFilter: ColorFilter? = null
+ private var tint: ColorStateList? = null
+ private var alpha = 255
+
+ private var transitionAnimator =
+ ValueAnimator.ofFloat(0f, 1f).also { it.doOnEnd { onTransitionComplete() } }
+
+ /**
+ * Set the drawable to be displayed, potentially animating the transition from one icon to the
+ * next.
+ */
+ fun setIcon(incomingDrawable: Drawable?) {
+ if (Objects.equals(drawable, incomingDrawable) && !transitionAnimator.isRunning) {
+ return
+ }
+
+ incomingDrawable?.colorFilter = colorFilter
+ incomingDrawable?.setTintList(tint)
+
+ if (drawable == null) {
+ // No existing icon drawn, just show the new one without a transition
+ drawable = incomingDrawable
+ invalidateSelf()
+ return
+ }
+
+ if (enteringDrawable != null) {
+ // There's already an entrance animation happening, just update the entering icon, not
+ // maintaining a queue or anything.
+ enteringDrawable = incomingDrawable
+ return
+ }
+
+ // There was already an icon, need to animate between icons.
+ enteringDrawable = incomingDrawable
+ transitionAnimator.setCurrentFraction(0f)
+ transitionAnimator.start()
+ invalidateSelf()
+ }
+
+ override fun draw(canvas: Canvas) {
+ // Scale the old one down, scale the new one up.
+ drawable?.let {
+ val scale =
+ if (transitionAnimator.isRunning) {
+ 1f - transitionAnimator.animatedFraction
+ } else {
+ 1f
+ }
+ drawScaledDrawable(it, canvas, scale)
+ }
+ enteringDrawable?.let {
+ val scale = transitionAnimator.animatedFraction
+ drawScaledDrawable(it, canvas, scale)
+ }
+
+ if (transitionAnimator.isRunning) {
+ invalidateSelf()
+ }
+ }
+
+ private fun drawScaledDrawable(drawable: Drawable, canvas: Canvas, scale: Float) {
+ drawable.bounds = getBounds()
+ canvas.save()
+ canvas.scale(
+ scale,
+ scale,
+ (drawable.intrinsicWidth / 2).toFloat(),
+ (drawable.intrinsicHeight / 2).toFloat()
+ )
+ drawable.draw(canvas)
+ canvas.restore()
+ }
+
+ private fun onTransitionComplete() {
+ drawable = enteringDrawable
+ enteringDrawable = null
+ invalidateSelf()
+ }
+
+ override fun setTintList(tint: ColorStateList?) {
+ super.setTintList(tint)
+ drawable?.setTintList(tint)
+ enteringDrawable?.setTintList(tint)
+ this.tint = tint
+ }
+
+ override fun setAlpha(alpha: Int) {
+ this.alpha = alpha
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ this.colorFilter = colorFilter
+ drawable?.colorFilter = colorFilter
+ enteringDrawable?.colorFilter = colorFilter
+ }
+
+ override fun getOpacity(): Int = alpha
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
index 3c5a0ec107f8..750bd530d9b2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.TransitioningIconDrawable
import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
object ActionButtonViewBinder {
@@ -28,7 +29,13 @@ object ActionButtonViewBinder {
fun bind(view: View, viewModel: ActionButtonViewModel) {
val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon)
val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text)
- iconView.setImageDrawable(viewModel.appearance.icon)
+ if (iconView.drawable == null) {
+ iconView.setImageDrawable(TransitioningIconDrawable())
+ }
+ val drawable = iconView.drawable as? TransitioningIconDrawable
+ // Note we never re-bind a view to a different ActionButtonViewModel, different view
+ // models would remove/create separate views.
+ drawable?.setIcon(viewModel.appearance.icon)
textView.text = viewModel.appearance.label
setMargins(iconView, textView, viewModel.appearance.label?.isNotEmpty() ?: false)
if (viewModel.onClicked != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
index bc35e6b17345..43c0107c12d5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -31,6 +31,8 @@ import com.android.systemui.res.R
import com.android.systemui.screenshot.ScreenshotEvent
import com.android.systemui.screenshot.ui.ScreenshotShelfView
import com.android.systemui.screenshot.ui.SwipeGestureListener
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+import com.android.systemui.screenshot.ui.viewmodel.AnimationState
import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
import com.android.systemui.util.children
import kotlinx.coroutines.Dispatchers
@@ -59,7 +61,6 @@ object ScreenshotShelfViewBinder {
val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
previewView.clipToOutline = true
previewViewBlur.clipToOutline = true
- val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
val dismissButton = view.requireViewById<View>(R.id.screenshot_dismiss_button)
dismissButton.visibility = if (viewModel.showDismissButton) View.VISIBLE else View.GONE
dismissButton.setOnClickListener {
@@ -90,47 +91,72 @@ object ScreenshotShelfViewBinder {
}
launch {
viewModel.actions.collect { actions ->
- val visibleActions = actions.filter { it.visible }
+ updateActions(
+ actions,
+ viewModel.animationState.value,
+ view,
+ layoutInflater
+ )
+ }
+ }
+ launch {
+ viewModel.animationState.collect { animationState ->
+ updateActions(
+ viewModel.actions.value,
+ animationState,
+ view,
+ layoutInflater
+ )
+ }
+ }
+ }
+ }
+ }
+ }
- if (visibleActions.isNotEmpty()) {
- view
- .requireViewById<View>(R.id.actions_container_background)
- .visibility = View.VISIBLE
- }
+ private fun updateActions(
+ actions: List<ActionButtonViewModel>,
+ animationState: AnimationState,
+ view: ScreenshotShelfView,
+ layoutInflater: LayoutInflater
+ ) {
+ val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
+ val visibleActions =
+ actions.filter {
+ it.visible &&
+ (animationState == AnimationState.ENTRANCE_COMPLETE ||
+ animationState == AnimationState.ENTRANCE_REVEAL ||
+ it.showDuringEntrance)
+ }
- // Remove any buttons not in the new list, then do another pass to add
- // any new actions and update any that are already there.
- // This assumes that actions can never change order and that each action
- // ID is unique.
- val newIds = visibleActions.map { it.id }
+ if (visibleActions.isNotEmpty()) {
+ view.requireViewById<View>(R.id.actions_container_background).visibility = View.VISIBLE
+ }
- for (child in actionsContainer.children.toList()) {
- if (child.tag !in newIds) {
- actionsContainer.removeView(child)
- }
- }
+ // Remove any buttons not in the new list, then do another pass to add
+ // any new actions and update any that are already there.
+ // This assumes that actions can never change order and that each action
+ // ID is unique.
+ val newIds = visibleActions.map { it.id }
- for ((index, action) in visibleActions.withIndex()) {
- val currentView: View? = actionsContainer.getChildAt(index)
- if (action.id == currentView?.tag) {
- // Same ID, update the display
- ActionButtonViewBinder.bind(currentView, action)
- } else {
- // Different ID. Removals have already happened so this must
- // mean that the new action must be inserted here.
- val actionButton =
- layoutInflater.inflate(
- R.layout.shelf_action_chip,
- actionsContainer,
- false
- )
- actionsContainer.addView(actionButton, index)
- ActionButtonViewBinder.bind(actionButton, action)
- }
- }
- }
- }
- }
+ for (child in actionsContainer.children.toList()) {
+ if (child.tag !in newIds) {
+ actionsContainer.removeView(child)
+ }
+ }
+
+ for ((index, action) in visibleActions.withIndex()) {
+ val currentView: View? = actionsContainer.getChildAt(index)
+ if (action.id == currentView?.tag) {
+ // Same ID, update the display
+ ActionButtonViewBinder.bind(currentView, action)
+ } else {
+ // Different ID. Removals have already happened so this must
+ // mean that the new action must be inserted here.
+ val actionButton =
+ layoutInflater.inflate(R.layout.shelf_action_chip, actionsContainer, false)
+ actionsContainer.addView(actionButton, index)
+ ActionButtonViewBinder.bind(actionButton, action)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
index c5fa8db953fa..364ab7624b5e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
@@ -20,6 +20,7 @@ data class ActionButtonViewModel(
val appearance: ActionButtonAppearance,
val id: Int,
val visible: Boolean,
+ val showDuringEntrance: Boolean,
val onClicked: (() -> Unit)?,
) {
companion object {
@@ -29,7 +30,14 @@ data class ActionButtonViewModel(
fun withNextId(
appearance: ActionButtonAppearance,
+ showDuringEntrance: Boolean,
onClicked: (() -> Unit)?
- ): ActionButtonViewModel = ActionButtonViewModel(appearance, getId(), true, onClicked)
+ ): ActionButtonViewModel =
+ ActionButtonViewModel(appearance, getId(), true, showDuringEntrance, onClicked)
+
+ fun withNextId(
+ appearance: ActionButtonAppearance,
+ onClicked: (() -> Unit)?
+ ): ActionButtonViewModel = withNextId(appearance, showDuringEntrance = true, onClicked)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
index f67ad402a738..5f36f73f2135 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -29,6 +29,9 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager
val previewAction: StateFlow<(() -> Unit)?> = _previewAction
private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>())
val actions: StateFlow<List<ActionButtonViewModel>> = _actions
+ private val _animationState = MutableStateFlow(AnimationState.NOT_STARTED)
+ val animationState: StateFlow<AnimationState> = _animationState
+
val showDismissButton: Boolean
get() = accessibilityManager.isEnabled
@@ -40,9 +43,14 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager
_previewAction.value = onClick
}
- fun addAction(actionAppearance: ActionButtonAppearance, onClicked: (() -> Unit)): Int {
+ fun addAction(
+ actionAppearance: ActionButtonAppearance,
+ showDuringEntrance: Boolean,
+ onClicked: (() -> Unit)
+ ): Int {
val actionList = _actions.value.toMutableList()
- val action = ActionButtonViewModel.withNextId(actionAppearance, onClicked)
+ val action =
+ ActionButtonViewModel.withNextId(actionAppearance, showDuringEntrance, onClicked)
actionList.add(action)
_actions.value = actionList
return action.id
@@ -57,6 +65,7 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager
actionList[index].appearance,
actionId,
visible,
+ actionList[index].showDuringEntrance,
actionList[index].onClicked
)
_actions.value = actionList
@@ -74,6 +83,7 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager
appearance,
actionId,
actionList[index].visible,
+ actionList[index].showDuringEntrance,
actionList[index].onClicked
)
_actions.value = actionList
@@ -92,13 +102,26 @@ class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager
}
}
+ // TODO: this should be handled entirely within the view binder.
+ fun setAnimationState(state: AnimationState) {
+ _animationState.value = state
+ }
+
fun reset() {
_preview.value = null
_previewAction.value = null
_actions.value = listOf()
+ _animationState.value = AnimationState.NOT_STARTED
}
companion object {
const val TAG = "ScreenshotViewModel"
}
}
+
+enum class AnimationState {
+ NOT_STARTED,
+ ENTRANCE_STARTED, // The first 200ms of the entrance animation
+ ENTRANCE_REVEAL, // The rest of the entrance animation
+ ENTRANCE_COMPLETE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 851bfca56359..281857fb0658 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -47,6 +47,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
@@ -69,7 +70,6 @@ constructor(
private val communalInteractor: CommunalInteractor,
private val communalViewModel: CommunalViewModel,
private val dialogFactory: SystemUIDialogFactory,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val shadeInteractor: ShadeInteractor,
private val powerManager: PowerManager,
@@ -102,12 +102,9 @@ constructor(
private var rightEdgeSwipeRegionWidth: Int = 0
/**
- * True if we are currently tracking a gesture for opening the hub that started in the edge
- * swipe region.
+ * True if we are currently tracking a touch intercepted by the hub, either because the hub is
+ * open or being opened.
*/
- private var isTrackingOpenGesture = false
-
- /** True if we are currently tracking a touch on the hub while it's open. */
private var isTrackingHubTouch = false
/**
@@ -153,7 +150,7 @@ constructor(
/**
* Creates the container view containing the glanceable hub UI.
*
- * @throws RuntimeException if [isEnabled] is false or the view is already initialized
+ * @throws RuntimeException if the view is already initialized
*/
fun initView(
context: Context,
@@ -197,6 +194,7 @@ constructor(
/** Override for testing. */
@VisibleForTesting
internal fun initView(containerView: View): View {
+ SceneContainerFlag.assertInLegacyMode()
if (communalContainerView != null) {
throw RuntimeException("Communal view has already been initialized")
}
@@ -227,7 +225,7 @@ constructor(
// BouncerSwipeTouchHandler has a larger gesture area than we want, set an exclusion area so
// the gesture area doesn't overlap with widgets.
- // TODO(b/323035776): adjust gesture areaa for portrait mode
+ // TODO(b/323035776): adjust gesture area for portrait mode
containerView.repeatWhenAttached {
// Run when the touch handling lifecycle is RESUMED, meaning the hub is visible and not
// occluded.
@@ -261,7 +259,7 @@ constructor(
)
collectFlow(
containerView,
- communalInteractor.isCommunalShowing,
+ communalInteractor.isCommunalVisible,
{
hubShowing = it
updateTouchHandlingState()
@@ -306,6 +304,7 @@ constructor(
/** Removes the container view from its parent. */
fun disposeView() {
+ SceneContainerFlag.assertInLegacyMode()
communalContainerView?.let {
(it.parent as ViewGroup).removeView(it)
lifecycleRegistry.currentState = Lifecycle.State.CREATED
@@ -323,71 +322,30 @@ constructor(
* to be fully in control of its own touch handling.
*/
fun onTouchEvent(ev: MotionEvent): Boolean {
+ SceneContainerFlag.assertInLegacyMode()
return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false
}
private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
- // If the hub is fully visible, send all touch events to it, other than top and bottom edge
- // swipes.
- return if (hubShowing) {
- handleHubOpenTouch(view, ev)
- } else {
- handleHubClosedTouch(view, ev)
- }
- }
-
- private fun handleHubOpenTouch(view: View, ev: MotionEvent): Boolean {
- val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
- val isUp = ev.actionMasked == MotionEvent.ACTION_UP
- val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
-
- val hubOccluded = anyBouncerShowing || shadeShowing
-
- if (isDown && !hubOccluded) {
- // Only intercept down events if the hub isn't occluded by the bouncer or
- // notification shade.
- isTrackingHubTouch = true
- }
-
- if (isTrackingHubTouch) {
- // Tracking a touch on the hub UI itself.
- if (isUp || isCancel) {
- isTrackingHubTouch = false
- }
- dispatchTouchEvent(view, ev)
- // Return true regardless of dispatch result as some touches at the start of a
- // gesture
- // may return false from dispatchTouchEvent.
- return true
- }
-
- return false
- }
-
- private fun handleHubClosedTouch(view: View, ev: MotionEvent): Boolean {
val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
val isUp = ev.actionMasked == MotionEvent.ACTION_UP
val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
val hubOccluded = anyBouncerShowing || shadeShowing
- if (rightEdgeSwipeRegionWidth == 0) {
- // If the edge region width has not been read yet for whatever reason, don't bother
- // intercepting touches to open the hub.
- return false
- }
-
if (isDown && !hubOccluded) {
val x = ev.rawX
val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
- if (inOpeningSwipeRegion) {
- isTrackingOpenGesture = true
+ if (inOpeningSwipeRegion || hubShowing) {
+ // Steal touch events when the hub is open, or if the touch started in the opening
+ // gesture region.
+ isTrackingHubTouch = true
}
}
- if (isTrackingOpenGesture) {
+ if (isTrackingHubTouch) {
if (isUp || isCancel) {
- isTrackingOpenGesture = false
+ isTrackingHubTouch = false
}
dispatchTouchEvent(view, ev)
// Return true regardless of dispatch result as some touches at the start of a gesture
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4a636d28aa88..3eb43895c7ae 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -412,9 +412,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
}
if (state.bouncerShowing) {
- mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+ mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
} else {
- mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+ mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 44f86da7431e..b50a3cd442d3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -52,6 +52,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.DragDownHelper;
@@ -357,7 +358,9 @@ public class NotificationShadeWindowViewController implements Dumpable {
mFalsingCollector.onTouchEvent(ev);
mPulsingWakeupGestureHandler.onTouchEvent(ev);
- if (mGlanceableHubContainerController.onTouchEvent(ev)) {
+ if (!SceneContainerFlag.isEnabled()
+ && mGlanceableHubContainerController.onTouchEvent(ev)) {
+ // GlanceableHubContainerController is only used pre-flexiglass.
return logDownDispatch(ev, "dispatched to glanceable hub container", true);
}
if (mDreamingWakeupGestureHandler != null
@@ -621,6 +624,10 @@ public class NotificationShadeWindowViewController implements Dumpable {
* The layout lives in {@link R.id.communal_ui_stub}.
*/
public void setupCommunalHubLayout() {
+ if (SceneContainerFlag.isEnabled()) {
+ // GlanceableHubContainerController is only used pre-flexiglass.
+ return;
+ }
collectFlow(
mView,
mGlanceableHubContainerController.communalAvailable(),
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
index 34629934e467..864e39af2da7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerSceneImpl.kt
@@ -33,7 +33,7 @@ constructor(
get() = shadeInteractor.isQsExpanded.value
override val isCustomizing: Boolean
- get() = qsSceneAdapter.isCustomizing.value
+ get() = qsSceneAdapter.isCustomizerShowing.value
@Deprecated("specific to legacy touch handling")
override fun shouldQuickSettingsIntercept(x: Float, y: Float, yDiff: Float): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 8c15817898a8..d2c93da671af 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -28,10 +28,10 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.dagger.ShadeTouchLog
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly
import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
import com.android.systemui.shade.ShadeController.ShadeVisibilityListener
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.VibratorHelper
@@ -99,11 +99,9 @@ constructor(
}
override fun instantCollapseShade() {
- // TODO(b/325602936) add support for instant transition
- sceneInteractor.changeScene(
+ sceneInteractor.snapToScene(
getCollapseDestinationScene(),
"hide shade",
- CollapseShadeInstantly,
)
}
@@ -194,11 +192,19 @@ constructor(
}
override fun expandToNotifications() {
- sceneInteractor.changeScene(Scenes.Shade, "ShadeController.animateExpandShade")
+ val shadeMode = shadeInteractor.shadeMode.value
+ sceneInteractor.changeScene(
+ if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
+ "ShadeController.animateExpandShade"
+ )
}
override fun expandToQs() {
- sceneInteractor.changeScene(Scenes.QuickSettings, "ShadeController.animateExpandQs")
+ val shadeMode = shadeInteractor.shadeMode.value
+ sceneInteractor.changeScene(
+ if (shadeMode is ShadeMode.Dual) Scenes.QuickSettingsShade else Scenes.QuickSettings,
+ "ShadeController.animateExpandQs"
+ )
}
override fun setVisibilityListener(listener: ShadeVisibilityListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index b934d63c17c5..7e1a3109fcbb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.data.repository
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -219,7 +220,7 @@ class ShadeRepositoryImpl @Inject constructor() : ShadeRepository {
@Deprecated("Use ShadeInteractor instead")
override val legacyQsFullscreen: StateFlow<Boolean> = _legacyQsFullscreen.asStateFlow()
- val _shadeMode = MutableStateFlow<ShadeMode>(ShadeMode.Single)
+ val _shadeMode = MutableStateFlow(if (DualShade.isEnabled) ShadeMode.Dual else ShadeMode.Single)
override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow()
override fun setShadeMode(shadeMode: ShadeMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
index 3a8ba7a0696b..c9949cdc8ab6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -19,6 +19,7 @@ package com.android.systemui.shade.domain.interactor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,7 +35,7 @@ constructor(
override fun animateCollapseQs(fullyCollapse: Boolean) {
if (shadeInteractor.isQsExpanded.value) {
val key =
- if (fullyCollapse) {
+ if (fullyCollapse || shadeInteractor.shadeMode.value is ShadeMode.Dual) {
if (deviceEntryInteractor.isDeviceEntered.value) {
Scenes.Gone
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 6a8b9eec140c..9885fe436e6c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -21,6 +21,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@@ -95,8 +96,9 @@ constructor(
}
private fun changeToShadeScene() {
+ val shadeMode = shadeInteractor.shadeMode.value
sceneInteractor.changeScene(
- Scenes.Shade,
+ if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
"ShadeLockscreenInteractorImpl.expandToNotifications",
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index f3802da9bc9b..3f4bcba288b7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -29,6 +29,7 @@ import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.transition.ScrimShadeTransitionController
import com.android.systemui.statusbar.policy.SplitShadeStateController
@@ -67,19 +68,24 @@ constructor(
private fun hydrateShadeExpansionStateManager() {
if (SceneContainerFlag.isEnabled) {
combine(
- panelExpansionInteractorProvider.get().legacyPanelExpansion,
- sceneInteractorProvider.get().isTransitionUserInputOngoing,
- ) { panelExpansion, tracking ->
- shadeExpansionStateManager.onPanelExpansionChanged(
- fraction = panelExpansion,
- expanded = panelExpansion > 0f,
- tracking = tracking,
- )
- }.launchIn(applicationScope)
+ panelExpansionInteractorProvider.get().legacyPanelExpansion,
+ sceneInteractorProvider.get().isTransitionUserInputOngoing,
+ ) { panelExpansion, tracking ->
+ shadeExpansionStateManager.onPanelExpansionChanged(
+ fraction = panelExpansion,
+ expanded = panelExpansion > 0f,
+ tracking = tracking,
+ )
+ }
+ .launchIn(applicationScope)
}
}
private fun hydrateShadeMode() {
+ if (DualShade.isEnabled) {
+ shadeRepository.setShadeMode(ShadeMode.Dual)
+ return
+ }
applicationScope.launch {
configurationRepository.onAnyConfigurationChange
// Force initial collection.
@@ -90,11 +96,7 @@ constructor(
}
.collect { isSplitShade ->
shadeRepository.setShadeMode(
- if (isSplitShade) {
- ShadeMode.Split
- } else {
- ShadeMode.Single
- }
+ if (isSplitShade) ShadeMode.Split else ShadeMode.Single
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
index 3451eaf54063..8214a24a9a38 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeMode.kt
@@ -38,4 +38,8 @@ sealed interface ShadeMode {
* a space on a small screen or folded device.
*/
data object Dual : ShadeMode
+
+ companion object {
+ @JvmStatic fun dual(): Dual = Dual
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index 5b76acb1df58..ac76becd1797 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -72,13 +72,13 @@ constructor(
deviceEntryInteractor.isUnlocked,
deviceEntryInteractor.canSwipeToEnter,
shadeInteractor.shadeMode,
- qsSceneAdapter.isCustomizing
- ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing ->
+ qsSceneAdapter.isCustomizerShowing
+ ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizerShowing ->
destinationScenes(
isUnlocked = isUnlocked,
canSwipeToDismiss = canSwipeToDismiss,
shadeMode = shadeMode,
- isCustomizing = isCustomizing
+ isCustomizing = isCustomizerShowing
)
}
.stateIn(
@@ -89,7 +89,7 @@ constructor(
isUnlocked = deviceEntryInteractor.isUnlocked.value,
canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
shadeMode = shadeInteractor.shadeMode.value,
- isCustomizing = qsSceneAdapter.isCustomizing.value,
+ isCustomizing = qsSceneAdapter.isCustomizerShowing.value,
),
)
diff --git a/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt b/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt
index 384acc493c4a..dd7942503211 100644
--- a/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt
@@ -28,6 +28,9 @@ import kotlinx.coroutines.launch
* Returns updating [Slice] for a [sliceUri]. It's null when there is no slice available for the
* provided Uri. This can change overtime because of external changes (like device being
* connected/disconnected).
+ *
+ * The flow should be [kotlinx.coroutines.flow.flowOn] the main thread because [SliceViewManager]
+ * isn't thread-safe. An exception will be thrown otherwise.
*/
fun SliceViewManager.sliceForUri(sliceUri: Uri): Flow<Slice?> =
ConflatedCallbackFlow.conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index 78e108d444d0..0d8030f02948 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -97,7 +97,7 @@ import java.util.Map;
public final class KeyboardShortcutListSearch {
private static final String TAG = KeyboardShortcutListSearch.class.getSimpleName();
private static final Object sLock = new Object();
- @VisibleForTesting static KeyboardShortcutListSearch sInstance;
+ @VisibleForTesting public static KeyboardShortcutListSearch sInstance;
private static int SHORTCUT_SYSTEM_INDEX = 0;
private static int SHORTCUT_INPUT_INDEX = 1;
@@ -136,7 +136,7 @@ public final class KeyboardShortcutListSearch {
};
private final Handler mHandler = new Handler(Looper.getMainLooper());
- @VisibleForTesting Context mContext;
+ @VisibleForTesting public Context mContext;
private final IPackageManager mPackageManager;
@VisibleForTesting BottomSheetDialog mKeyboardShortcutsBottomSheetDialog;
@@ -414,7 +414,7 @@ public final class KeyboardShortcutListSearch {
private boolean mImeShortcutsReceived;
@VisibleForTesting
- void showKeyboardShortcuts(int deviceId) {
+ public void showKeyboardShortcuts(int deviceId) {
retrieveKeyCharacterMap(deviceId);
mAppShortcutsReceived = false;
mImeShortcutsReceived = false;
@@ -502,7 +502,8 @@ public final class KeyboardShortcutListSearch {
return keyboardShortcutMultiMappingGroups;
}
- private void dismissKeyboardShortcuts() {
+ @VisibleForTesting
+ public void dismissKeyboardShortcuts() {
if (mKeyboardShortcutsBottomSheetDialog != null) {
mKeyboardShortcutsBottomSheetDialog.dismiss();
mKeyboardShortcutsBottomSheetDialog = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index 90567d8ec934..21f608e13f5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -79,7 +79,7 @@ import java.util.List;
public final class KeyboardShortcuts {
private static final String TAG = KeyboardShortcuts.class.getSimpleName();
private static final Object sLock = new Object();
- @VisibleForTesting static KeyboardShortcuts sInstance;
+ @VisibleForTesting public static KeyboardShortcuts sInstance;
private WindowManager mWindowManager;
private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
@@ -93,7 +93,7 @@ public final class KeyboardShortcuts {
};
private final Handler mHandler = new Handler(Looper.getMainLooper());
- @VisibleForTesting Context mContext;
+ @VisibleForTesting public Context mContext;
private final IPackageManager mPackageManager;
private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
@@ -373,7 +373,7 @@ public final class KeyboardShortcuts {
}
@VisibleForTesting
- void showKeyboardShortcuts(int deviceId) {
+ public void showKeyboardShortcuts(int deviceId) {
retrieveKeyCharacterMap(deviceId);
mReceivedAppShortcutGroups = null;
mReceivedImeShortcutGroups = null;
@@ -407,7 +407,8 @@ public final class KeyboardShortcuts {
showKeyboardShortcutsDialog(shortcutGroups);
}
- private void dismissKeyboardShortcuts() {
+ @VisibleForTesting
+ public void dismissKeyboardShortcuts() {
if (mKeyboardShortcutsDialog != null) {
mKeyboardShortcutsDialog.dismiss();
mKeyboardShortcutsDialog = null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
index 1cfb400280fe..815f1fcfdec6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutsReceiver.java
@@ -15,6 +15,8 @@
*/
package com.android.systemui.statusbar;
+import static com.android.systemui.Flags.keyboardShortcutHelperRewrite;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -25,21 +27,22 @@ import com.android.systemui.shared.recents.utilities.Utilities;
import javax.inject.Inject;
-/**
- * Receiver for the Keyboard Shortcuts Helper.
- */
+/** Receiver for the Keyboard Shortcuts Helper. */
public class KeyboardShortcutsReceiver extends BroadcastReceiver {
- private boolean mIsShortcutListSearchEnabled;
+ private final FeatureFlags mFeatureFlags;
@Inject
public KeyboardShortcutsReceiver(FeatureFlags featureFlags) {
- mIsShortcutListSearchEnabled = featureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
+ mFeatureFlags = featureFlags;
}
@Override
public void onReceive(Context context, Intent intent) {
- if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(context)) {
+ if (keyboardShortcutHelperRewrite()) {
+ return;
+ }
+ if (isTabletLayoutFlagEnabled() && Utilities.isLargeScreen(context)) {
if (Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
KeyboardShortcutListSearch.show(context, -1 /* deviceId unknown */);
} else if (Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS.equals(intent.getAction())) {
@@ -53,4 +56,8 @@ public class KeyboardShortcutsReceiver extends BroadcastReceiver {
}
}
}
+
+ private boolean isTabletLayoutFlagEnabled() {
+ return mFeatureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 7c1101bf7680..d7d373226d5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -382,16 +382,15 @@ public class NotificationMediaManager implements Dumpable {
private void clearCurrentMediaNotificationSession() {
mMediaMetadata = null;
- mBackgroundExecutor.execute(() -> {
- if (mMediaController != null) {
- if (DEBUG_MEDIA) {
- Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
- + mMediaController.getPackageName());
- }
- mMediaController.unregisterCallback(mMediaListener);
- mMediaController = null;
+ if (mMediaController != null) {
+ if (DEBUG_MEDIA) {
+ Log.v(TAG, "DEBUG_MEDIA: Disconnecting from old controller: "
+ + mMediaController.getPackageName());
}
- });
+ // TODO(b/336612071): move to background thread
+ mMediaController.unregisterCallback(mMediaListener);
+ }
+ mMediaController = null;
}
public interface MediaListener {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 4f8c3caa122c..70632d5aa27a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -677,7 +677,9 @@ public class StatusBarStateControllerImpl implements
Scenes.Bouncer, StatusBarState.KEYGUARD,
Scenes.Communal, StatusBarState.KEYGUARD,
Scenes.Shade, StatusBarState.SHADE_LOCKED,
+ Scenes.NotificationsShade, StatusBarState.SHADE_LOCKED,
Scenes.QuickSettings, StatusBarState.SHADE_LOCKED,
+ Scenes.QuickSettingsShade, StatusBarState.SHADE_LOCKED,
Scenes.Gone, StatusBarState.SHADE
);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 3bd873580fbb..d669369103ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1185,6 +1185,11 @@ public class NotificationStackScrollLayout
}
@Override
+ public void setCurrentGestureOverscrollConsumer(@Nullable Consumer<Boolean> consumer) {
+ mScrollViewFields.setCurrentGestureOverscrollConsumer(consumer);
+ }
+
+ @Override
public void setStackHeightConsumer(@Nullable Consumer<Float> consumer) {
mScrollViewFields.setStackHeightConsumer(consumer);
}
@@ -3403,6 +3408,8 @@ public class NotificationStackScrollLayout
boolean isUpOrCancel = action == ACTION_UP || action == ACTION_CANCEL;
if (mSendingTouchesToSceneFramework) {
mController.sendTouchToSceneFramework(ev);
+ mScrollViewFields.sendCurrentGestureOverscroll(
+ getExpandedInThisMotion() && !isUpOrCancel);
} else if (!isUpOrCancel) {
// if this is the first touch being sent to the scene framework,
// convert it into a synthetic DOWN event.
@@ -3410,6 +3417,7 @@ public class NotificationStackScrollLayout
MotionEvent downEvent = MotionEvent.obtain(ev);
downEvent.setAction(MotionEvent.ACTION_DOWN);
mController.sendTouchToSceneFramework(downEvent);
+ mScrollViewFields.sendCurrentGestureOverscroll(getExpandedInThisMotion());
downEvent.recycle();
}
@@ -3428,6 +3436,14 @@ public class NotificationStackScrollLayout
downEvent.recycle();
}
+ // Only when scene container is enabled, mark that we are being dragged so that we start
+ // dispatching the rest of the gesture to scene container.
+ void startOverscrollAfterExpanding() {
+ SceneContainerFlag.isUnexpectedlyInLegacyMode();
+ getExpandHelper().finishExpanding();
+ setIsBeingDragged(true);
+ }
+
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (!isScrollingEnabled()
@@ -5545,6 +5561,11 @@ public class NotificationStackScrollLayout
return mExpandingNotification;
}
+ @VisibleForTesting
+ void setExpandingNotification(boolean isExpanding) {
+ mExpandingNotification = isExpanding;
+ }
+
boolean getDisallowScrollingInThisMotion() {
return mDisallowScrollingInThisMotion;
}
@@ -5557,6 +5578,11 @@ public class NotificationStackScrollLayout
return mExpandedInThisMotion;
}
+ @VisibleForTesting
+ void setExpandedInThisMotion(boolean expandedInThisMotion) {
+ mExpandedInThisMotion = expandedInThisMotion;
+ }
+
boolean getDisallowDismissInThisMotion() {
return mDisallowDismissInThisMotion;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 5bb3f4273c52..3011bc284961 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -206,6 +206,7 @@ public class NotificationStackScrollLayoutController implements Dumpable {
private final SeenNotificationsInteractor mSeenNotificationsInteractor;
private final KeyguardTransitionRepository mKeyguardTransitionRepo;
private NotificationStackScrollLayout mView;
+ private TouchHandler mTouchHandler;
private NotificationSwipeHelper mSwipeHelper;
@Nullable
private Boolean mHistoryEnabled;
@@ -807,7 +808,8 @@ public class NotificationStackScrollLayoutController implements Dumpable {
mView.setStackStateLogger(mStackStateLogger);
mView.setController(this);
mView.setLogger(mLogger);
- mView.setTouchHandler(new TouchHandler());
+ mTouchHandler = new TouchHandler();
+ mView.setTouchHandler(mTouchHandler);
mView.setResetUserExpandedStatesRunnable(mNotificationsController::resetUserExpandedStates);
mView.setActivityStarter(mActivityStarter);
mView.setClearAllAnimationListener(this::onAnimationEnd);
@@ -1793,6 +1795,11 @@ public class NotificationStackScrollLayoutController implements Dumpable {
}
}
+ @VisibleForTesting
+ TouchHandler getTouchHandler() {
+ return mTouchHandler;
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("mMaxAlphaFromView=" + mMaxAlphaFromView);
@@ -2043,7 +2050,14 @@ public class NotificationStackScrollLayoutController implements Dumpable {
expandingNotification = mView.isExpandingNotification();
if (mView.getExpandedInThisMotion() && !expandingNotification && wasExpandingBefore
&& !mView.getDisallowScrollingInThisMotion()) {
- mView.dispatchDownEventToScroller(ev);
+ // We need to dispatch the overscroll differently when Scene Container is on,
+ // since NSSL no longer controls its own scroll.
+ if (SceneContainerFlag.isEnabled() && !isCancelOrUp) {
+ mView.startOverscrollAfterExpanding();
+ return true;
+ } else {
+ mView.dispatchDownEventToScroller(ev);
+ }
}
}
boolean horizontalSwipeWantsIt = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
index edac5ede1e91..a3827c140214 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt
@@ -51,6 +51,11 @@ class ScrollViewFields {
*/
var syntheticScrollConsumer: Consumer<Float>? = null
/**
+ * When a gesture is consumed internally by NSSL but needs to be handled by other elements (such
+ * as the notif scrim) as overscroll, we can notify the placeholder through here.
+ */
+ var currentGestureOverscrollConsumer: Consumer<Boolean>? = null
+ /**
* Any time the stack height is recalculated, it should be updated here to be used by the
* placeholder
*/
@@ -64,6 +69,9 @@ class ScrollViewFields {
/** send the [syntheticScroll] to the [syntheticScrollConsumer], if present. */
fun sendSyntheticScroll(syntheticScroll: Float) =
syntheticScrollConsumer?.accept(syntheticScroll)
+ /** send [isCurrentGestureOverscroll] to the [currentGestureOverscrollConsumer], if present. */
+ fun sendCurrentGestureOverscroll(isCurrentGestureOverscroll: Boolean) =
+ currentGestureOverscrollConsumer?.accept(isCurrentGestureOverscroll)
/** send the [stackHeight] to the [stackHeightConsumer], if present. */
fun sendStackHeight(stackHeight: Float) = stackHeightConsumer?.accept(stackHeight)
/** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index e980794d23dd..d0cebae40c5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -169,6 +169,14 @@ public class StackScrollAlgorithm {
}
}
+ // On the final call to {@link #resetViewState}, the alpha is set back to 1f but
+ // ambientState.isExpansionChanging() is now false. This causes a flicker on the
+ // EmptyShadeView after the shade is collapsed. Make sure the empty shade view
+ // isn't visible unless the shade is expanded.
+ if (view instanceof EmptyShadeView && ambientState.getExpansionFraction() == 0f) {
+ viewState.setAlpha(0f);
+ }
+
// For EmptyShadeView if on keyguard, we need to control the alpha to create
// a nice transition when the user is dragging down the notification panel.
if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
index 8a9da69079d4..920c9c213060 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
@@ -43,4 +43,10 @@ class NotificationViewHeightRepository @Inject constructor() {
* necessary to scroll up to keep expanding the notification.
*/
val syntheticScroll = MutableStateFlow(0f)
+
+ /**
+ * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+ * consumed part of the gesture.
+ */
+ val isCurrentGestureOverscroll = MutableStateFlow(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index b8660bad78d9..b94da388cef4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -105,6 +105,13 @@ constructor(
*/
val syntheticScroll: Flow<Float> = viewHeightRepository.syntheticScroll.asStateFlow()
+ /**
+ * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+ * consumed part of the gesture.
+ */
+ val isCurrentGestureOverscroll: Flow<Boolean> =
+ viewHeightRepository.isCurrentGestureOverscroll.asStateFlow()
+
/** Sets the alpha to apply to the NSSL for the brightness mirror */
fun setAlphaForBrightnessMirror(alpha: Float) {
placeholderRepository.alphaForBrightnessMirror.value = alpha
@@ -146,6 +153,11 @@ constructor(
viewHeightRepository.syntheticScroll.value = delta
}
+ /** Sets whether the current touch gesture is overscroll. */
+ fun setCurrentGestureOverscroll(isOverscroll: Boolean) {
+ viewHeightRepository.isCurrentGestureOverscroll.value = isOverscroll
+ }
+
fun setConstrainedAvailableSpace(height: Int) {
placeholderRepository.constrainedAvailableSpace.value = height
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index a56384dc47dd..2c8884504c32 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -51,6 +51,8 @@ interface NotificationScrollView {
/** Set a consumer for synthetic scroll events */
fun setSyntheticScrollConsumer(consumer: Consumer<Float>?)
+ /** Set a consumer for current gesture overscroll events */
+ fun setCurrentGestureOverscrollConsumer(consumer: Consumer<Boolean>?)
/** Set a consumer for stack height changed events */
fun setStackHeightConsumer(consumer: Consumer<Float>?)
/** Set a consumer for heads up height changed events */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 4476d87fbcf6..26f7ad775f1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -89,10 +89,12 @@ constructor(
launchAndDispose {
view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
+ view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
view.setStackHeightConsumer(viewModel.stackHeightConsumer)
view.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer)
DisposableHandle {
view.setSyntheticScrollConsumer(null)
+ view.setCurrentGestureOverscrollConsumer(null)
view.setStackHeightConsumer(null)
view.setHeadsUpHeightConsumer(null)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 8b1b93bfb0e5..b2184db0879d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -145,6 +145,12 @@ constructor(
/** Receives the amount (px) that the stack should scroll due to internal expansion. */
val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll
+ /**
+ * Receives whether the current touch gesture is overscroll as it has already been consumed by
+ * the stack.
+ */
+ val currentGestureOverscrollConsumer: (Boolean) -> Unit =
+ stackAppearanceInteractor::setCurrentGestureOverscroll
/** Receives the height of the contents of the notification stack. */
val stackHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setStackHeight
/** Receives the height of the heads up notification. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 486e305af05f..11eaf54efe47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -111,6 +111,13 @@ constructor(
val syntheticScroll: Flow<Float> =
interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll")
+ /**
+ * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
+ * consumed part of the gesture.
+ */
+ val isCurrentGestureOverscroll: Flow<Boolean> =
+ interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll")
+
/** Sets whether the notification stack is scrolled to the top. */
fun setScrolledToTop(scrolledToTop: Boolean) {
interactor.setScrolledToTop(scrolledToTop)
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 7630d43c3c7e..be6bef74565a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -26,10 +26,12 @@ import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_
import static androidx.lifecycle.Lifecycle.State.RESUMED;
import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
+import static com.android.systemui.Flags.keyboardShortcutHelperRewrite;
import static com.android.systemui.Flags.lightRevealMigration;
import static com.android.systemui.Flags.newAodTransition;
import static com.android.systemui.Flags.truncatedStatusBarIconsFix;
import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
+import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
@@ -289,7 +291,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
private CentralSurfacesCommandQueueCallbacks mCommandQueueCallbacks;
private float mTransitionToFullShadeProgress = 0f;
private final NotificationListContainer mNotifListContainer;
- private final boolean mIsShortcutListSearchEnabled;
private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
new KeyguardStateController.Callback() {
@@ -789,7 +790,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
mStatusBarSignalPolicy = statusBarSignalPolicy;
mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager;
mFeatureFlags = featureFlags;
- mIsShortcutListSearchEnabled = featureFlags.isEnabled(Flags.SHORTCUT_LIST_SEARCH_LAYOUT);
mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
mMainExecutor = delayableExecutor;
mMessageRouter = messageRouter;
@@ -820,10 +820,13 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
// TODO(b/190746471): Find a better home for this.
DateTimeView.setReceiverHandler(timeTickHandler);
- mMessageRouter.subscribeTo(KeyboardShortcutsMessage.class,
- data -> toggleKeyboardShortcuts(data.mDeviceId));
- mMessageRouter.subscribeTo(MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU,
- id -> dismissKeyboardShortcuts());
+ if (!keyboardShortcutHelperRewrite()) {
+ mMessageRouter.subscribeTo(
+ KeyboardShortcutsMessage.class,
+ data -> toggleKeyboardShortcuts(data.mDeviceId));
+ mMessageRouter.subscribeTo(
+ MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU, id -> dismissKeyboardShortcuts());
+ }
mMessageRouter.subscribeTo(AnimateExpandSettingsPanelMessage.class,
data -> mCommandQueueCallbacks.animateExpandSettingsPanel(data.mSubpanel));
mMessageRouter.subscribeTo(MSG_LAUNCH_TRANSITION_TIMEOUT,
@@ -1872,10 +1875,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
String action = intent.getAction();
String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
- if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(mContext)) {
- KeyboardShortcutListSearch.dismiss();
- } else {
- KeyboardShortcuts.dismiss();
+ if (!keyboardShortcutHelperRewrite()) {
+ if (shouldUseTabletKeyboardShortcuts()) {
+ KeyboardShortcutListSearch.dismiss();
+ } else {
+ KeyboardShortcuts.dismiss();
+ }
}
mRemoteInputManager.closeRemoteInputs();
if (mLockscreenUserManager.isCurrentProfile(getSendingUserId())) {
@@ -2345,6 +2350,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
} else if (mState == StatusBarState.KEYGUARD
&& !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
&& mStatusBarKeyguardViewManager.isSecure()) {
+ Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
}
}
@@ -2945,7 +2951,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
protected void toggleKeyboardShortcuts(int deviceId) {
- if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(mContext)) {
+ if (shouldUseTabletKeyboardShortcuts()) {
KeyboardShortcutListSearch.toggle(mContext, deviceId);
} else {
KeyboardShortcuts.toggle(mContext, deviceId);
@@ -2953,13 +2959,18 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
protected void dismissKeyboardShortcuts() {
- if (mIsShortcutListSearchEnabled && Utilities.isLargeScreen(mContext)) {
+ if (shouldUseTabletKeyboardShortcuts()) {
KeyboardShortcutListSearch.dismiss();
} else {
KeyboardShortcuts.dismiss();
}
}
+ private boolean shouldUseTabletKeyboardShortcuts() {
+ return mFeatureFlags.isEnabled(SHORTCUT_LIST_SEARCH_LAYOUT)
+ && Utilities.isLargeScreen(mContext);
+ }
+
private void clearNotificationEffects() {
try {
mBarService.clearNotificationEffects();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index dea94162ad0e..2e1ab383538f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -59,7 +59,10 @@ class ConfigurationControllerImpl @Inject constructor(
}
override fun notifyThemeChanged() {
- val listeners = ArrayList(listeners)
+ // Avoid concurrent modification exception
+ val listeners = synchronized(this.listeners) {
+ ArrayList(this.listeners)
+ }
listeners.filterForEach({ this.listeners.contains(it) }) {
it.onThemeChanged()
@@ -68,8 +71,9 @@ class ConfigurationControllerImpl @Inject constructor(
override fun onConfigurationChanged(newConfig: Configuration) {
// Avoid concurrent modification exception
- val listeners = ArrayList(listeners)
-
+ val listeners = synchronized(this.listeners) {
+ ArrayList(this.listeners)
+ }
listeners.filterForEach({ this.listeners.contains(it) }) {
it.onConfigChanged(newConfig)
}
@@ -148,12 +152,16 @@ class ConfigurationControllerImpl @Inject constructor(
}
override fun addCallback(listener: ConfigurationListener) {
- listeners.add(listener)
+ synchronized(listeners) {
+ listeners.add(listener)
+ }
listener.onDensityOrFontScaleChanged()
}
override fun removeCallback(listener: ConfigurationListener) {
- listeners.remove(listener)
+ synchronized(listeners) {
+ listeners.remove(listener)
+ }
}
override fun isLayoutRtl(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index 5deb08a75dff..cff46ab812bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -37,6 +37,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView;
import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
@@ -277,6 +278,15 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
addView(view, viewIndex, createLayoutParams());
}
+ /** Adds a bindable icon to the demo mode view. */
+ public void addBindableIcon(StatusBarIconHolder.BindableIconHolder holder) {
+ // This doesn't do any correct ordering, and also doesn't check if we already have an
+ // existing icon for the slot. But since we hope to remove this class soon, we won't spend
+ // the time adding that logic.
+ ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
+ addView(view, createLayoutParams());
+ }
+
public void onRemoveIcon(StatusIconDisplayable view) {
if (view.getSlot().equals("wifi")) {
if (view instanceof ModernStatusBarWifiView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
index bef0b286ebf8..08a890dbadb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
@@ -169,16 +169,19 @@ open class StatusBarIconHolder private constructor() {
* StatusBarIconController will register all available bindable icons on init (see
* [BindableIconsRepository]), and will ignore any call to setIcon for these.
*
- * [initializer] a view creator that can bind the relevant view models to the created view.
+ * @property initializer a view creator that can bind the relevant view models to the created
+ * view.
+ * @property slot the name of the slot that this holder is used for.
*/
- class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() {
+ class BindableIconHolder(val initializer: ModernStatusBarViewCreator, val slot: String) :
+ StatusBarIconHolder() {
override var type: Int = TYPE_BINDABLE
/** This is unused, as bindable icons use their own view binders to control visibility */
override var isVisible: Boolean = true
override fun toString(): String {
- return ("StatusBarIconHolder(type=BINDABLE)")
+ return ("StatusBarIconHolder(type=BINDABLE, slot=$slot)")
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 7301b87d815b..f0dab3ba1829 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -744,6 +744,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
public void showBouncer(boolean scrimmed) {
if (DeviceEntryUdfpsRefactor.isEnabled()) {
if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) {
+ Log.d(TAG, "showBouncer:alternateBouncer.forceShow()");
mAlternateBouncerInteractor.forceShow();
updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
} else {
@@ -869,6 +870,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
}
if (DeviceEntryUdfpsRefactor.isEnabled()) {
+ Log.d(TAG, "dismissWithAction:alternateBouncer.forceShow()");
mAlternateBouncerInteractor.forceShow();
updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState());
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
index 0ed94203ffa0..5ad737684ca1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
@@ -37,6 +37,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.phone.DemoStatusIcons;
import com.android.systemui.statusbar.phone.StatusBarIconHolder;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
@@ -49,7 +50,9 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWi
import com.android.systemui.util.Assert;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* Turns info from StatusBarIconController into ImageViews in a ViewGroup.
@@ -60,6 +63,11 @@ public class IconManager implements DemoModeCommandReceiver {
private final LocationBasedWifiViewModel mWifiViewModel;
private final MobileIconsViewModel mMobileIconsViewModel;
+ /**
+ * Stores the list of bindable icons that have been added, keyed on slot name. This ensures
+ * we don't accidentally add the same bindable icon twice.
+ */
+ private final Map<String, BindableIconHolder> mBindableIcons = new HashMap<>();
protected final Context mContext;
protected int mIconSize;
// Whether or not these icons show up in dumpsys
@@ -142,7 +150,7 @@ public class IconManager implements DemoModeCommandReceiver {
case TYPE_MOBILE_NEW -> addNewMobileIcon(index, slot, holder.getTag());
case TYPE_BINDABLE ->
// Safe cast, since only BindableIconHolders can set this tag on themselves
- addBindableIcon((StatusBarIconHolder.BindableIconHolder) holder, index);
+ addBindableIcon((BindableIconHolder) holder, index);
default -> null;
};
}
@@ -162,10 +170,14 @@ public class IconManager implements DemoModeCommandReceiver {
* icon view, we can simply create the icon when requested and allow the
* ViewBinder to control its visual state.
*/
- protected StatusIconDisplayable addBindableIcon(StatusBarIconHolder.BindableIconHolder holder,
+ protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder,
int index) {
+ mBindableIcons.put(holder.getSlot(), holder);
ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
mGroup.addView(view, index, onCreateLayoutParams());
+ if (mIsInDemoMode) {
+ mDemoStatusIcons.addBindableIcon(holder);
+ }
return view;
}
@@ -278,6 +290,9 @@ public class IconManager implements DemoModeCommandReceiver {
if (mDemoStatusIcons == null) {
mDemoStatusIcons = createDemoStatusIcons();
mDemoStatusIcons.addModernWifiView(mWifiViewModel);
+ for (BindableIconHolder holder : mBindableIcons.values()) {
+ mDemoStatusIcons.addBindableIcon(holder);
+ }
}
mDemoStatusIcons.onDemoModeStarted();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java
index 92d90af6f921..fabf858d0832 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerImpl.java
@@ -213,7 +213,8 @@ public class StatusBarIconControllerImpl implements Tunable,
StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0);
// Expected to be null
if (existingHolder == null) {
- BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer());
+ BindableIconHolder bindableIcon =
+ new BindableIconHolder(icon.getInitializer(), icon.getSlot());
setIcon(icon.getSlot(), bindableIcon);
} else {
Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index b80ff388955a..226a84a78116 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -41,6 +41,8 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyIm
import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl
import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepositorySwitcher
+import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel
import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModelImpl
@@ -83,8 +85,13 @@ abstract class StatusBarPipelineModule {
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds
- abstract fun deviceBasedSatelliteRepository(
+ abstract fun realDeviceBasedSatelliteRepository(
impl: DeviceBasedSatelliteRepositoryImpl
+ ): RealDeviceBasedSatelliteRepository
+
+ @Binds
+ abstract fun deviceBasedSatelliteRepository(
+ impl: DeviceBasedSatelliteRepositorySwitcher
): DeviceBasedSatelliteRepository
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
index ad8b8100f14d..d38e834ff1e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.pipeline.satellite.data
import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
/**
* Device-based satellite refers to the capability of a device to connect directly to a satellite
@@ -26,12 +26,22 @@ import kotlinx.coroutines.flow.Flow
*/
interface DeviceBasedSatelliteRepository {
/** See [SatelliteConnectionState] for available states */
- val connectionState: Flow<SatelliteConnectionState>
+ val connectionState: StateFlow<SatelliteConnectionState>
/** 0-4 level (similar to wifi and mobile) */
// @IntRange(from = 0, to = 4)
- val signalStrength: Flow<Int>
+ val signalStrength: StateFlow<Int>
/** Clients must observe this property, as device-based satellite is location-dependent */
- val isSatelliteAllowedForCurrentLocation: Flow<Boolean>
+ val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean>
}
+
+/**
+ * A no-op interface used for Dagger bindings.
+ *
+ * [DeviceBasedSatelliteRepositorySwitcher] needs to inject both the real repository and the demo
+ * mode repository, both of which implement the [DeviceBasedSatelliteRepository] interface. To help
+ * distinguish the two for the switcher, [DeviceBasedSatelliteRepositoryImpl] will implement this
+ * [RealDeviceBasedSatelliteRepository] interface.
+ */
+interface RealDeviceBasedSatelliteRepository : DeviceBasedSatelliteRepository
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
new file mode 100644
index 000000000000..6b1bc65e86db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.pipeline.satellite.data
+
+import android.os.Bundle
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A provider for the [DeviceBasedSatelliteRepository] interface that can choose between the Demo
+ * and Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo],
+ * which switches based on the latest information from [DemoModeController], and switches every flow
+ * in the interface to point to the currently-active provider. This allows us to put the demo mode
+ * interface in its own repository, completely separate from the real version, while still using all
+ * of the prod implementations for the rest of the pipeline (interactors and onward). Looks
+ * something like this:
+ * ```
+ * RealRepository
+ * │
+ * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
+ * │
+ * DemoRepository
+ * ```
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class DeviceBasedSatelliteRepositorySwitcher
+@Inject
+constructor(
+ private val realImpl: RealDeviceBasedSatelliteRepository,
+ private val demoImpl: DemoDeviceBasedSatelliteRepository,
+ private val demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) : DeviceBasedSatelliteRepository {
+ private val isDemoMode =
+ conflatedCallbackFlow {
+ val callback =
+ object : DemoMode {
+ override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+ // Don't care
+ }
+
+ override fun onDemoModeStarted() {
+ demoImpl.startProcessingCommands()
+ trySend(true)
+ }
+
+ override fun onDemoModeFinished() {
+ demoImpl.stopProcessingCommands()
+ trySend(false)
+ }
+ }
+
+ demoModeController.addCallback(callback)
+ awaitClose { demoModeController.removeCallback(callback) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+
+ @VisibleForTesting
+ val activeRepo: StateFlow<DeviceBasedSatelliteRepository> =
+ isDemoMode
+ .mapLatest { isDemoMode ->
+ if (isDemoMode) {
+ demoImpl
+ } else {
+ realImpl
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl)
+
+ override val connectionState: StateFlow<SatelliteConnectionState> =
+ activeRepo
+ .flatMapLatest { it.connectionState }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.connectionState.value)
+
+ override val signalStrength: StateFlow<Int> =
+ activeRepo
+ .flatMapLatest { it.signalStrength }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.signalStrength.value)
+
+ override val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean> =
+ activeRepo
+ .flatMapLatest { it.isSatelliteAllowedForCurrentLocation }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ realImpl.isSatelliteAllowedForCurrentLocation.value
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt
new file mode 100644
index 000000000000..7ecc29bf46c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.pipeline.satellite.data.demo
+
+import android.os.Bundle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Reads the incoming demo commands and emits the satellite-related commands to [satelliteEvents]
+ * for the demo repository to consume.
+ */
+@SysUISingleton
+class DemoDeviceBasedSatelliteDataSource
+@Inject
+constructor(
+ demoModeController: DemoModeController,
+ @Application scope: CoroutineScope,
+) {
+ private val demoCommandStream = demoModeController.demoFlowForCommand(DemoMode.COMMAND_NETWORK)
+ private val _satelliteCommands =
+ demoCommandStream.map { args -> args.toSatelliteEvent() }.filterNotNull()
+
+ /** A flow that emits the demo commands that are satellite-related. */
+ val satelliteEvents =
+ _satelliteCommands.stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_VALUE)
+
+ private fun Bundle.toSatelliteEvent(): DemoSatelliteEvent? {
+ val satellite = getString("satellite") ?: return null
+ if (satellite != "show") {
+ return null
+ }
+
+ return DemoSatelliteEvent(
+ connectionState = getString("connection").toConnectionState(),
+ signalStrength = getString("level")?.toInt() ?: 0,
+ )
+ }
+
+ data class DemoSatelliteEvent(
+ val connectionState: SatelliteConnectionState,
+ val signalStrength: Int,
+ )
+
+ private fun String?.toConnectionState(): SatelliteConnectionState {
+ if (this == null) {
+ return SatelliteConnectionState.Unknown
+ }
+ return try {
+ // Lets people use "connected" on the command line and have it be correctly converted
+ // to [SatelliteConnectionState.Connected] with a capital C.
+ SatelliteConnectionState.valueOf(this.replaceFirstChar { it.uppercase() })
+ } catch (e: IllegalArgumentException) {
+ SatelliteConnectionState.Unknown
+ }
+ }
+
+ private companion object {
+ val DEFAULT_VALUE = DemoSatelliteEvent(SatelliteConnectionState.Unknown, signalStrength = 0)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
new file mode 100644
index 000000000000..56034f08503d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.pipeline.satellite.data.demo
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/** A satellite repository that represents the latest satellite values sent via demo mode. */
+@SysUISingleton
+class DemoDeviceBasedSatelliteRepository
+@Inject
+constructor(
+ private val dataSource: DemoDeviceBasedSatelliteDataSource,
+ @Application private val scope: CoroutineScope,
+) : DeviceBasedSatelliteRepository {
+ private var demoCommandJob: Job? = null
+
+ override val connectionState = MutableStateFlow(SatelliteConnectionState.Unknown)
+ override val signalStrength = MutableStateFlow(0)
+ override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(true)
+
+ fun startProcessingCommands() {
+ demoCommandJob =
+ scope.launch { dataSource.satelliteEvents.collect { event -> processEvent(event) } }
+ }
+
+ fun stopProcessingCommands() {
+ demoCommandJob?.cancel()
+ }
+
+ private fun processEvent(event: DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent) {
+ connectionState.value = event.connectionState
+ signalStrength.value = event.signalStrength
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 3e3ea855ccf7..a7c4187afdbd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -31,7 +31,7 @@ import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.MessageInitializer
import com.android.systemui.log.core.MessagePrinter
import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
-import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -50,12 +50,14 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
@@ -134,7 +136,7 @@ constructor(
@Application private val scope: CoroutineScope,
@OemSatelliteInputLog private val logBuffer: LogBuffer,
private val systemClock: SystemClock,
-) : DeviceBasedSatelliteRepository {
+) : RealDeviceBasedSatelliteRepository {
private val satelliteManager: SatelliteManager?
@@ -200,10 +202,12 @@ constructor(
}
override val connectionState =
- satelliteSupport.whenSupported(
- supported = ::connectionStateFlow,
- orElse = flowOf(SatelliteConnectionState.Off)
- )
+ satelliteSupport
+ .whenSupported(
+ supported = ::connectionStateFlow,
+ orElse = flowOf(SatelliteConnectionState.Off)
+ )
+ .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off)
// By using the SupportedSatelliteManager here, we expect registration never to fail
private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
@@ -227,7 +231,9 @@ constructor(
.flowOn(bgDispatcher)
override val signalStrength =
- satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+ satelliteSupport
+ .whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+ .stateIn(scope, SharingStarted.Eagerly, 0)
// By using the SupportedSatelliteManager here, we expect registration never to fail
private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
@@ -312,8 +318,8 @@ constructor(
}
companion object {
- // TTL for satellite polling is one hour
- const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
+ // TTL for satellite polling is twenty minutes
+ const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 20
// Let the system boot up and stabilize before we check for system support
const val MIN_UPTIME: Long = 1000 * 60
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 37eda6490ec2..92731037dc64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -17,11 +17,11 @@
package com.android.systemui.statusbar.policy;
import android.annotation.Nullable;
-import android.view.View;
import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -51,23 +51,23 @@ public interface BatteryController extends DemoMode,
*
* Can pass the view that triggered the request.
*/
- void setPowerSaveMode(boolean powerSave, @Nullable View view);
+ void setPowerSaveMode(boolean powerSave, @Nullable Expandable expandable);
/**
* Gets a reference to the last view used when called {@link #setPowerSaveMode}.
*/
@Nullable
- default WeakReference<View> getLastPowerSaverStartView() {
+ default WeakReference<Expandable> getLastPowerSaverStartExpandable() {
return null;
}
/**
* Clears the last view used when called {@link #setPowerSaveMode}.
*
- * Immediately after calling this, a call to {@link #getLastPowerSaverStartView()} should return
- * {@code null}.
+ * Immediately after calling this, a call to {@link #getLastPowerSaverStartExpandable()} should
+ * return {@code null}.
*/
- default void clearLastPowerSaverStartView() {}
+ default void clearLastPowerSaverStartExpandable() {}
/**
* Returns {@code true} if the device is currently plugged in.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index dab27bbf6b4b..6012ecd5a7f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -37,7 +37,6 @@ import android.os.Handler;
import android.os.PowerManager;
import android.os.PowerSaveState;
import android.util.IndentingPrintWriter;
-import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -48,6 +47,7 @@ import com.android.settingslib.fuelgauge.BatterySaverUtils;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.settingslib.utils.PowerUtil;
import com.android.systemui.Dumpable;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -110,9 +110,10 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC
private boolean mFetchingEstimate = false;
// Use AtomicReference because we may request it from a different thread
- // Use WeakReference because we are keeping a reference to a View that's not as long lived
- // as this controller.
- private AtomicReference<WeakReference<View>> mPowerSaverStartView = new AtomicReference<>();
+ // Use WeakReference because we are keeping a reference to an Expandable that's not as long
+ // lived as this controller.
+ private AtomicReference<WeakReference<Expandable>> mPowerSaverStartExpandable =
+ new AtomicReference<>();
@VisibleForTesting
public BatteryControllerImpl(
@@ -196,20 +197,20 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC
}
@Override
- public void setPowerSaveMode(boolean powerSave, View view) {
- if (powerSave) mPowerSaverStartView.set(new WeakReference<>(view));
+ public void setPowerSaveMode(boolean powerSave, Expandable expandable) {
+ if (powerSave) mPowerSaverStartExpandable.set(new WeakReference<>(expandable));
BatterySaverUtils.setPowerSaveMode(mContext, powerSave, /*needFirstTimeWarning*/ true,
SAVER_ENABLED_QS);
}
@Override
- public WeakReference<View> getLastPowerSaverStartView() {
- return mPowerSaverStartView.get();
+ public WeakReference<Expandable> getLastPowerSaverStartExpandable() {
+ return mPowerSaverStartExpandable.get();
}
@Override
- public void clearLastPowerSaverStartView() {
- mPowerSaverStartView.set(null);
+ public void clearLastPowerSaverStartExpandable() {
+ mPowerSaverStartExpandable.set(null);
}
@Override
@@ -543,4 +544,4 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC
public boolean isChargingSourceDock() {
return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_DOCK;
}
-}
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
index 19d9c3f125b7..3eec3d91c809 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
@@ -32,7 +32,6 @@ import com.android.systemui.volume.domain.model.AudioOutputDevice
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import com.android.systemui.volume.panel.shared.model.filterData
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
@@ -69,14 +68,9 @@ constructor(
communicationDevice?.toAudioOutputDevice()
}
} else {
- mediaOutputInteractor.defaultActiveMediaSession
- .filterData()
- .flatMapLatest {
- localMediaRepositoryFactory
- .create(it?.packageName)
- .currentConnectedDevice
- }
- .map { mediaDevice -> mediaDevice?.toAudioOutputDevice() }
+ mediaOutputInteractor.currentConnectedDevice.map { mediaDevice ->
+ mediaDevice?.toAudioOutputDevice()
+ }
}
}
.map { it ?: AudioOutputDevice.Unknown }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt
index 8ce3b1fa1e73..3117abc44111 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt
@@ -23,6 +23,7 @@ import androidx.slice.SliceViewManager
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.slice.sliceForUri
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import dagger.assisted.Assisted
@@ -57,6 +58,7 @@ class AncSliceRepositoryImpl
constructor(
mediaRepositoryFactory: LocalMediaRepositoryFactory,
@Background private val backgroundCoroutineContext: CoroutineContext,
+ @Main private val mainCoroutineContext: CoroutineContext,
@Assisted private val sliceViewManager: SliceViewManager,
) : AncSliceRepository {
@@ -73,7 +75,7 @@ constructor(
.distinctUntilChanged()
.flatMapLatest { sliceUri ->
sliceUri ?: return@flatMapLatest flowOf(null)
- sliceViewManager.sliceForUri(sliceUri)
+ sliceViewManager.sliceForUri(sliceUri).flowOn(mainCoroutineContext)
}
.flowOn(backgroundCoroutineContext)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt
index bee79bb68141..c980eb43ec97 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/ui/viewmodel/AncViewModel.kt
@@ -16,7 +16,9 @@
package com.android.systemui.volume.panel.component.anc.ui.viewmodel
+import android.content.Intent
import androidx.slice.Slice
+import androidx.slice.SliceItem
import com.android.systemui.volume.panel.component.anc.domain.AncAvailabilityCriteria
import com.android.systemui.volume.panel.component.anc.domain.interactor.AncSliceInteractor
import com.android.systemui.volume.panel.component.anc.domain.model.AncSlices
@@ -59,6 +61,31 @@ constructor(
.map { it.buttonSlice }
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
+ fun isClickable(slice: Slice?): Boolean {
+ slice ?: return false
+ val slices = ArrayDeque<SliceItem>()
+ slices.addAll(slice.items)
+ while (slices.isNotEmpty()) {
+ val item: SliceItem = slices.removeFirst()
+ when (item.format) {
+ android.app.slice.SliceItem.FORMAT_ACTION -> {
+ val itemActionIntent: Intent? = item.action?.intent
+ if (itemActionIntent?.hasExtra(EXTRA_ANC_ENABLED) == true) {
+ return itemActionIntent.getBooleanExtra(EXTRA_ANC_ENABLED, true)
+ }
+ }
+ android.app.slice.SliceItem.FORMAT_SLICE -> {
+ item.slice?.items?.let(slices::addAll)
+ }
+ }
+ }
+ return true
+ }
+
+ private companion object {
+ const val EXTRA_ANC_ENABLED = "EXTRA_ANC_ENABLED"
+ }
+
/** Call this to update [popupSlice] width in a reaction to container size change. */
fun onPopupSliceWidthChanged(width: Int) {
interactor.onPopupSliceWidthChanged(width)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index 22c053099ac5..199bc3b78dd2 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -33,15 +33,15 @@ constructor(
private val mediaOutputDialogManager: MediaOutputDialogManager,
) {
- fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable) {
+ fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable?) {
if (sessionWithPlaybackState?.isPlaybackActive == true) {
mediaOutputDialogManager.createAndShowWithController(
sessionWithPlaybackState.session.packageName,
false,
- expandable.dialogController()
+ expandable?.dialogController()
)
} else {
- mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController())
+ mediaOutputDialogManager.createAndShowForSystemRouting(expandable?.dialogController())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index b974f90191e9..b00829e48404 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -19,10 +19,12 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto
import android.content.pm.PackageManager
import android.media.VolumeProvider
import android.media.session.MediaController
+import android.os.Handler
import android.util.Log
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepository
+import com.android.settingslib.volume.data.repository.stateChanges
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
@@ -36,14 +38,15 @@ import kotlin.coroutines.CoroutineContext
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.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -58,21 +61,31 @@ constructor(
@VolumePanelScope private val coroutineScope: CoroutineScope,
@Background private val backgroundCoroutineContext: CoroutineContext,
mediaControllerRepository: MediaControllerRepository,
+ @Background private val backgroundHandler: Handler,
) {
private val activeMediaControllers: Flow<MediaControllers> =
mediaControllerRepository.activeSessions
+ .flatMapLatest { activeSessions ->
+ activeSessions
+ .map { activeSession -> activeSession.stateChanges() }
+ .merge()
+ .map { activeSessions }
+ .onStart { emit(activeSessions) }
+ }
.map { getMediaControllers(it) }
- .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, MediaControllers(null, null))
/** [MediaDeviceSessions] that contains currently active sessions. */
val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
- activeMediaControllers.map {
- MediaDeviceSessions(
- local = it.local?.mediaDeviceSession(),
- remote = it.remote?.mediaDeviceSession()
- )
- }
+ activeMediaControllers
+ .map {
+ MediaDeviceSessions(
+ local = it.local?.mediaDeviceSession(),
+ remote = it.remote?.mediaDeviceSession()
+ )
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null))
/** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
val defaultActiveMediaSession: StateFlow<Result<MediaDeviceSession?>> =
@@ -89,13 +102,17 @@ constructor(
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading())
- private val localMediaRepository: SharedFlow<LocalMediaRepository> =
+ private val localMediaRepository: Flow<LocalMediaRepository> =
defaultActiveMediaSession
.filterData()
.map { it?.packageName }
.distinctUntilChanged()
.map { localMediaRepositoryFactory.create(it) }
- .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
+ .stateIn(
+ coroutineScope,
+ SharingStarted.Eagerly,
+ localMediaRepositoryFactory.create(null)
+ )
/** Currently connected [MediaDevice]. */
val currentConnectedDevice: Flow<MediaDevice?> =
@@ -134,21 +151,33 @@ constructor(
}
if (!remoteMediaSessions.contains(controller.packageName)) {
remoteMediaSessions.add(controller.packageName)
- if (remoteController == null) {
- remoteController = controller
- }
+ remoteController = chooseController(remoteController, controller)
}
}
MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
if (controller.packageName in remoteMediaSessions) continue
- if (localController != null) continue
- localController = controller
+ localController = chooseController(localController, controller)
}
}
}
return MediaControllers(local = localController, remote = remoteController)
}
+ private fun chooseController(
+ currentController: MediaController?,
+ newController: MediaController,
+ ): MediaController {
+ if (currentController == null) {
+ return newController
+ }
+ val isNewControllerActive = newController.playbackState?.isActive == true
+ val isCurrentControllerActive = currentController.playbackState?.isActive == true
+ if (isNewControllerActive && !isCurrentControllerActive) {
+ return newController
+ }
+ return currentController
+ }
+
private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
return MediaDeviceSession(
packageName = packageName,
@@ -160,6 +189,14 @@ constructor(
)
}
+ private fun MediaController?.stateChanges(): Flow<MediaController?> {
+ if (this == null) {
+ return flowOf(null)
+ }
+
+ return stateChanges(backgroundHandler).map { this }.onStart { emit(this@stateChanges) }
+ }
+
private data class MediaControllers(
val local: MediaController?,
val remote: MediaController?,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index 192e0ec76132..be3a529d9a75 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -143,7 +143,7 @@ constructor(
null,
)
- fun onBarClick(expandable: Expandable) {
+ fun onBarClick(expandable: Expandable?) {
uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED)
val result = sessionWithPlaybackState.value
actionsInteractor.onBarClick((result as? Result.Data)?.data, expandable)
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 263ddc175647..b86a7c92ba47 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -21,6 +21,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_B
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -273,6 +274,13 @@ public final class WMShell implements
splitScreen.setSplitscreenFocus(leftOrTop);
}
});
+ splitScreen.registerSplitAnimationListener(new SplitScreen.SplitInvocationListener() {
+ @Override
+ public void onSplitAnimationInvoked(boolean animationRunning) {
+ mSysUiState.setFlag(SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION, animationRunning)
+ .commitUpdate(mDisplayTracker.getDefaultDisplayId());
+ }
+ }, mSysUiMainExecutor);
}
@VisibleForTesting
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
index abc12ede0a67..e9c742d63d81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
@@ -24,7 +24,6 @@ import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.view.View;
import androidx.test.filters.SmallTest;
@@ -34,6 +33,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import org.junit.Before;
@@ -56,9 +56,10 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase {
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
- private final View mView = new View(mContext);
private final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();
@Mock
+ private Expandable mExpandable;
+ @Mock
private DialogTransitionAnimator mDialogTransitionAnimator;
@Mock
private HearingDevicesDialogDelegate.Factory mDialogFactory;
@@ -97,7 +98,7 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase {
public void showDialog_bluetoothDisable_showPairNewDeviceTrue() {
when(mLocalBluetoothAdapter.isEnabled()).thenReturn(false);
- mManager.showDialog(mView);
+ mManager.showDialog(mExpandable);
verify(mDialogFactory).create(eq(true));
}
@@ -109,7 +110,7 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase {
when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
mCachedDevices.add(mCachedDevice);
- mManager.showDialog(mView);
+ mManager.showDialog(mExpandable);
verify(mDialogFactory).create(eq(false));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index fbe11847f3d3..e64df905470d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -28,10 +28,13 @@ import com.android.systemui.activity.EmptyTestActivity
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.motion.MotionTestRule
import platform.test.motion.RecordedMotion
-import platform.test.motion.Sampling.Companion.evenlySampled
+import platform.test.motion.view.AnimationSampling.Companion.evenlySampled
import platform.test.motion.view.DrawableFeatureCaptures
-import platform.test.motion.view.ViewMotionTestRule
+import platform.test.motion.view.ViewRecordingSpec.Companion.captureWithoutScreenshot
+import platform.test.motion.view.ViewToolkit
+import platform.test.motion.view.record
import platform.test.screenshot.DeviceEmulationRule
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.DisplaySpec
@@ -64,7 +67,8 @@ class TransitionAnimatorTest : SysuiTestCase() {
@get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
@get:Rule(order = 1) val activityRule = ActivityScenarioRule(EmptyTestActivity::class.java)
- @get:Rule(order = 2) val motionRule = ViewMotionTestRule(pathManager, { activityRule.scenario })
+ @get:Rule(order = 2)
+ val motionRule = MotionTestRule(ViewToolkit { activityRule.scenario }, pathManager)
@Test
fun backgroundAnimation_whenLaunching() {
@@ -151,15 +155,14 @@ class TransitionAnimatorTest : SysuiTestCase() {
backgroundLayer: GradientDrawable,
animator: AnimatorSet
): RecordedMotion {
- return motionRule.checkThat(animator).record(
- backgroundLayer,
- evenlySampled(20),
- visualCapture = null
- ) {
- capture(DrawableFeatureCaptures.bounds, "bounds")
- capture(DrawableFeatureCaptures.cornerRadii, "corner_radii")
- capture(DrawableFeatureCaptures.alpha, "alpha")
- }
+ return motionRule.record(
+ animator,
+ backgroundLayer.captureWithoutScreenshot(evenlySampled(20)) {
+ feature(DrawableFeatureCaptures.bounds, "bounds")
+ feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
+ feature(DrawableFeatureCaptures.alpha, "alpha")
+ }
+ )
}
}
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 67ca9a40dc2a..023148603b50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -16,80 +16,73 @@
package com.android.systemui.biometrics
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
-import com.android.systemui.flags.Flags
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.user.domain.UserDomainLayerModule
-import dagger.BindsInstance
-import dagger.Component
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.testKosmos
+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.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.verifyZeroInteractions
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
-
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- UserDomainLayerModule::class,
- BiometricsDomainLayerModule::class,
- ]
- )
- interface TestComponent : SysUITestComponent<AuthDialogPanelInteractionDetector> {
-
- val shadeRepository: FakeShadeRepository
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- featureFlags: FakeFeatureFlagsClassicModule,
- ): TestComponent
+@RunWith(ParameterizedAndroidJunit4::class)
+class AuthDialogPanelInteractionDetectorTest(flags: FlagsParameterization?) : SysuiTestCase() {
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
}
}
- private val testComponent: TestComponent =
- DaggerAuthDialogPanelInteractionDetectorTest_TestComponent.factory()
- .create(
- test = this,
- featureFlags =
- FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) },
- )
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
- private val detector: AuthDialogPanelInteractionDetector = testComponent.underTest
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
@Mock private lateinit var action: Runnable
+ lateinit var detector: AuthDialogPanelInteractionDetector
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ detector =
+ AuthDialogPanelInteractionDetector(
+ kosmos.applicationCoroutineScope,
+ { kosmos.shadeInteractor },
+ )
}
@Test
fun enableDetector_expand_shouldRunAction() =
- testComponent.runTest {
+ testScope.runTest {
// GIVEN shade is closed and detector is enabled
- shadeRepository.setLegacyShadeExpansion(0f)
+ shadeTestUtil.setShadeExpansion(0f)
detector.enable(action)
runCurrent()
// WHEN shade expands
- shadeRepository.setLegacyShadeTracking(true)
- shadeRepository.setLegacyShadeExpansion(.5f)
+ shadeTestUtil.setTracking(true)
+ shadeTestUtil.setShadeExpansion(.5f)
runCurrent()
// THEN action was run
@@ -98,9 +91,9 @@ class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
@Test
fun enableDetector_isUserInteractingTrue_shouldNotPostRunnable() =
- testComponent.runTest {
+ testScope.runTest {
// GIVEN isInteracting starts true
- shadeRepository.setLegacyShadeTracking(true)
+ shadeTestUtil.setTracking(true)
runCurrent()
detector.enable(action)
@@ -110,33 +103,34 @@ class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
@Test
fun enableDetector_shadeExpandImmediate_shouldNotPostRunnable() =
- testComponent.runTest {
+ testScope.runTest {
// GIVEN shade is closed and detector is enabled
- shadeRepository.setLegacyShadeExpansion(0f)
+ shadeTestUtil.setShadeExpansion(0f)
detector.enable(action)
runCurrent()
// WHEN shade expands fully instantly
- shadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN action not run
verifyZeroInteractions(action)
+ detector.disable()
}
@Test
fun disableDetector_shouldNotPostRunnable() =
- testComponent.runTest {
+ testScope.runTest {
// GIVEN shade is closed and detector is enabled
- shadeRepository.setLegacyShadeExpansion(0f)
+ shadeTestUtil.setShadeExpansion(0f)
detector.enable(action)
runCurrent()
// WHEN detector is disabled and shade opens
detector.disable()
runCurrent()
- shadeRepository.setLegacyShadeTracking(true)
- shadeRepository.setLegacyShadeExpansion(.5f)
+ shadeTestUtil.setTracking(true)
+ shadeTestUtil.setShadeExpansion(.5f)
runCurrent()
// THEN action not run
@@ -145,17 +139,18 @@ class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
@Test
fun enableDetector_beginCollapse_shouldNotPostRunnable() =
- testComponent.runTest {
+ testScope.runTest {
// GIVEN shade is open and detector is enabled
- shadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
detector.enable(action)
runCurrent()
// WHEN shade begins to collapse
- shadeRepository.setLegacyShadeExpansion(.5f)
+ shadeTestUtil.programmaticCollapseShade()
runCurrent()
// THEN action not run
verifyZeroInteractions(action)
+ detector.disable()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index b05d9591d8a8..af1d31528675 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -22,7 +22,6 @@ import android.testing.TestableLooper
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
-import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -30,6 +29,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.flags.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.FakeSharedPreferences
@@ -100,6 +100,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate
@Mock private lateinit var sysuiDialog: SystemUIDialog
+ @Mock private lateinit var expandable: Expandable
+ @Mock private lateinit var controller: DialogTransitionAnimator.Controller
private val sharedPreferences = FakeSharedPreferences()
@@ -157,6 +159,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
.thenReturn(getMutableStateFlow(false))
whenever(audioSharingInteractor.audioSharingButtonStateUpdate)
.thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone))
+ whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
}
@Test
@@ -164,16 +167,16 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(null)
- verify(mDialogTransitionAnimator, never()).showFromView(any(), any(), any(), any())
+ verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
}
}
@Test
fun testShowDialog_animated() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
+ bluetoothTileDialogViewModel.showDialog(expandable)
- verify(mDialogTransitionAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
}
@@ -181,10 +184,9 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
fun testShowDialog_animated_callInBackgroundThread() {
testScope.runTest {
backgroundExecutor.execute {
- bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext))
+ bluetoothTileDialogViewModel.showDialog(expandable)
- verify(mDialogTransitionAnimator)
- .showFromView(any(), any(), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
index 39fcd417ec7a..5b836b6b06b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
@@ -346,6 +346,24 @@ public class KeyguardIndicationRotateTextViewControllerTest extends SysuiTestCas
}
@Test
+ public void testStartDozing_withMinShowTime() {
+ // GIVEN a biometric message is showing
+ mController.updateIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE,
+ new KeyguardIndication.Builder()
+ .setMessage("test_message")
+ .setMinVisibilityMillis(5000L)
+ .setTextColor(ColorStateList.valueOf(Color.WHITE))
+ .build(),
+ true);
+
+ // WHEN the device wants to hide the biometric message
+ mController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+
+ // THEN switch to INDICATION_TYPE_NONE
+ verify(mView).switchIndication(null);
+ }
+
+ @Test
public void testStoppedDozing() {
// GIVEN we're dozing & we have an indication message
mStatusBarStateListener.onDozingChanged(true);
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 325e7bf31a43..6b1d39a6b278 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -241,7 +241,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
.thenReturn(mock(Flow.class));
when(mDreamViewModel.getTransitionEnded())
.thenReturn(mock(Flow.class));
- when(mCommunalTransitionViewModel.getShowByDefault())
+ when(mCommunalTransitionViewModel.getShowCommunalFromOccluded())
.thenReturn(mock(Flow.class));
when(mCommunalTransitionViewModel.getTransitionFromOccludedEnded())
.thenReturn(mock(Flow.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
index 9ccf2121b8d2..f32e7757328f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt
@@ -274,4 +274,27 @@ class KeyguardSurfaceBehindInteractorTest : SysuiTestCase() {
runCurrent()
assertThat(isAnimatingSurface).isFalse()
}
+
+ @Test
+ fun notificationLaunchFalse_isAnimatingSurfaceFalse() =
+ testScope.runTest {
+ val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface)
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.FINISHED,
+ )
+ )
+ kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false)
+ runCurrent()
+ assertThat(isAnimatingSurface).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 691d48fe0146..1dc58d1784d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -26,8 +26,8 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.dock.DockManager
import com.android.systemui.dock.fakeDockManager
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -1229,23 +1229,22 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() {
}
@Test
- fun occludedToGlanceableHubWhenDocked() =
+ fun occludedToGlanceableHubWhenInitiallyOnHub() =
testScope.runTest {
- // GIVEN a device on lockscreen
+ // GIVEN a device on lockscreen and communal is available
keyguardRepository.setKeyguardShowing(true)
+ kosmos.setCommunalAvailable(true)
runCurrent()
- // GIVEN a prior transition has run to OCCLUDED
+ // GIVEN a prior transition has run to OCCLUDED from GLANCEABLE_HUB
runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED)
keyguardRepository.setKeyguardOccluded(true)
runCurrent()
- // GIVEN device is docked/communal is available
- dockManager.setIsDocked(true)
- dockManager.setDockEvent(DockManager.STATE_DOCKED)
+ // GIVEN on blank scene
val idleTransitionState =
MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(CommunalScenes.Communal)
+ ObservableTransitionState.Idle(CommunalScenes.Blank)
)
communalInteractor.setTransitionState(idleTransitionState)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
index 4bb0d4781376..0bca36775e9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
@@ -26,7 +26,7 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintA
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel.Companion.UNLOCKED_DELAY_MS
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
@@ -110,6 +110,46 @@ class DeviceEntryIconViewModelTest : SysuiTestCase() {
assertThat(isVisible).isTrue()
}
+ @Test
+ fun iconType_fingerprint() =
+ testScope.runTest {
+ val iconType by collectLastValue(underTest.iconType)
+ keyguardRepository.setKeyguardDismissible(false)
+ fingerprintPropertyRepository.supportsUdfps()
+ fingerprintAuthRepository.setIsRunning(true)
+ assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.FINGERPRINT)
+ }
+
+ @Test
+ fun iconType_locked() =
+ testScope.runTest {
+ val iconType by collectLastValue(underTest.iconType)
+ keyguardRepository.setKeyguardDismissible(false)
+ fingerprintAuthRepository.setIsRunning(false)
+ assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.LOCK)
+ }
+
+ @Test
+ fun iconType_unlocked() =
+ testScope.runTest {
+ val iconType by collectLastValue(underTest.iconType)
+ keyguardRepository.setKeyguardDismissible(true)
+ advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay
+ fingerprintAuthRepository.setIsRunning(false)
+ assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.UNLOCK)
+ }
+
+ @Test
+ fun iconType_none() =
+ testScope.runTest {
+ val iconType by collectLastValue(underTest.iconType)
+ keyguardRepository.setKeyguardDismissible(true)
+ advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay
+ fingerprintPropertyRepository.supportsUdfps()
+ fingerprintAuthRepository.setIsRunning(true)
+ assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.NONE)
+ }
+
private fun deviceEntryIconTransitionAlpha(alpha: Float) {
deviceEntryIconTransition.setDeviceEntryParentViewAlpha(alpha)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 9429725718db..b95d3aaef32f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -44,7 +44,6 @@ import android.os.Handler;
import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.view.View;
import androidx.test.filters.SmallTest;
@@ -53,6 +52,7 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.settings.UserTracker;
@@ -88,7 +88,9 @@ public class PowerNotificationWarningsTest extends SysuiTestCase {
@Mock
private UserTracker mUserTracker;
@Mock
- private View mView;
+ private Expandable mExpandable;
+ @Mock
+ private DialogTransitionAnimator.Controller mController;
@Mock
private SystemUIDialog.Factory mSystemUIDialogFactory;
@Mock
@@ -234,32 +236,31 @@ public class PowerNotificationWarningsTest extends SysuiTestCase {
@Test
public void testDialogStartedFromLauncher_viewVisible() {
- when(mBatteryController.getLastPowerSaverStartView())
- .thenReturn(new WeakReference<>(mView));
- when(mView.isAggregatedVisible()).thenReturn(true);
+ when(mBatteryController.getLastPowerSaverStartExpandable())
+ .thenReturn(new WeakReference<>(mExpandable));
+ when(mExpandable.dialogTransitionController(any())).thenReturn(mController);
Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION);
intent.putExtras(new Bundle());
mReceiver.onReceive(mContext, intent);
- verify(mDialogTransitionAnimator).showFromView(any(), eq(mView), any());
+ verify(mDialogTransitionAnimator).show(any(), eq(mController));
mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
}
@Test
public void testDialogStartedNotFromLauncher_viewNotVisible() {
- when(mBatteryController.getLastPowerSaverStartView())
- .thenReturn(new WeakReference<>(mView));
- when(mView.isAggregatedVisible()).thenReturn(false);
+ when(mBatteryController.getLastPowerSaverStartExpandable())
+ .thenReturn(new WeakReference<>(mExpandable));
Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION);
intent.putExtras(new Bundle());
mReceiver.onReceive(mContext, intent);
- verify(mDialogTransitionAnimator, never()).showFromView(any(), any());
+ verify(mDialogTransitionAnimator, never()).show(any(), any());
verify(mPowerNotificationWarnings.getSaverConfirmationDialog()).show();
mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss();
@@ -267,7 +268,7 @@ public class PowerNotificationWarningsTest extends SysuiTestCase {
@Test
public void testDialogShownNotFromLauncher() {
- when(mBatteryController.getLastPowerSaverStartView()).thenReturn(null);
+ when(mBatteryController.getLastPowerSaverStartExpandable()).thenReturn(null);
Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION);
intent.putExtras(new Bundle());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index ef7798e545d3..5e14b1a60ddb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -43,7 +43,6 @@ import android.os.Looper;
import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.util.SparseArray;
-import android.view.View;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
@@ -51,6 +50,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.util.CollectionUtils;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.dump.nano.SystemUIProtoDump;
import com.android.systemui.flags.FakeFeatureFlags;
@@ -734,7 +734,7 @@ public class QSTileHostTest extends SysuiTestCase {
}
@Override
- protected void handleClick(@Nullable View view) {}
+ protected void handleClick(@Nullable Expandable expandable) {}
@Override
protected void handleUpdateState(State state, Object arg) {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index df0ab341b5d4..8bf743884359 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -44,13 +44,13 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.text.TextUtils;
import android.util.ArraySet;
-import android.view.View;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.InstanceId;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSHost;
import com.android.systemui.res.R;
@@ -395,13 +395,13 @@ public class TileQueryHelperTest extends SysuiTestCase {
}
@Override
- public void click(@Nullable View view) {}
+ public void click(@Nullable Expandable expandable) {}
@Override
- public void secondaryClick(@Nullable View view) {}
+ public void secondaryClick(@Nullable Expandable expandable) {}
@Override
- public void longClick(@Nullable View view) {}
+ public void longClick(@Nullable Expandable expandable) {}
@Override
public void userSwitch(int currentUser) {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
index ef979d2d0cac..a8e9db5d527c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt
@@ -35,11 +35,10 @@ import androidx.test.filters.SmallTest
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.IWindowManager
-import android.view.View
import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityTransitionAnimator
-import com.android.systemui.animation.view.LaunchableFrameLayout
+import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile
@@ -339,7 +338,7 @@ class CustomTileTest : SysuiTestCase() {
tile.qsTile.activityLaunchForClick = pi
}
- tile.handleClick(mock(View::class.java))
+ tile.handleClick(mock(Expandable::class.java))
testableLooper.processAllMessages()
verify(activityStarter, never())
@@ -366,7 +365,7 @@ class CustomTileTest : SysuiTestCase() {
val tile = CustomTile.create(customTileFactory, TILE_SPEC, mContext)
tile.qsTile.activityLaunchForClick = pi
- tile.handleClick(mock(LaunchableFrameLayout::class.java))
+ tile.handleClick(mock(Expandable::class.java))
testableLooper.processAllMessages()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index 22b1c7b58ab3..c70624411c37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -51,7 +51,6 @@ import android.service.quicksettings.Tile;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
-import android.view.View;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
@@ -63,6 +62,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.systemui.InstanceIdSequenceFake;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
@@ -148,7 +148,7 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testClick_Metrics() {
- mTile.click(null /* view */);
+ mTile.click(null /* expandable */);
verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_CLICK)));
assertEquals(1, mUiEventLoggerFake.numLogs());
UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0);
@@ -159,7 +159,7 @@ public class QSTileImplTest extends SysuiTestCase {
public void testClick_log() {
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
- mTile.click(null /* view */);
+ mTile.click(null /* expandable */);
verify(mQsLogger).logTileClick(eq(SPEC), eq(StatusBarState.SHADE), eq(Tile.STATE_ACTIVE),
anyInt());
}
@@ -184,7 +184,7 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testClick_Metrics_Status_Bar_Status() {
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
- mTile.click(null /* view */);
+ mTile.click(null /* expandable */);
verify(mMetricsLogger).write(mLogCaptor.capture());
assertEquals(StatusBarState.SHADE, mLogCaptor.getValue()
.getTaggedData(FIELD_STATUS_BAR_STATE));
@@ -193,12 +193,12 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testClick_falsing() {
mFalsingManager.setFalseTap(true);
- mTile.click(null /* view */);
+ mTile.click(null /* expandable */);
mTestableLooper.processAllMessages();
assertThat(mTile.mClicked).isFalse();
mFalsingManager.setFalseTap(false);
- mTile.click(null /* view */);
+ mTile.click(null /* expandable */);
mTestableLooper.processAllMessages();
assertThat(mTile.mClicked).isTrue();
}
@@ -206,19 +206,19 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testLongClick_falsing() {
mFalsingManager.setFalseLongTap(true);
- mTile.longClick(null /* view */);
+ mTile.longClick(null /* expandable */);
mTestableLooper.processAllMessages();
assertThat(mTile.mLongClicked).isFalse();
mFalsingManager.setFalseLongTap(false);
- mTile.longClick(null /* view */);
+ mTile.longClick(null /* expandable */);
mTestableLooper.processAllMessages();
assertThat(mTile.mLongClicked).isTrue();
}
@Test
public void testSecondaryClick_Metrics() {
- mTile.secondaryClick(null /* view */);
+ mTile.secondaryClick(null /* expandable */);
verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_SECONDARY_CLICK)));
assertEquals(1, mUiEventLoggerFake.numLogs());
UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0);
@@ -229,7 +229,7 @@ public class QSTileImplTest extends SysuiTestCase {
public void testSecondaryClick_log() {
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
- mTile.secondaryClick(null /* view */);
+ mTile.secondaryClick(null /* expandable */);
verify(mQsLogger).logTileSecondaryClick(eq(SPEC), eq(StatusBarState.SHADE),
eq(Tile.STATE_ACTIVE), anyInt());
}
@@ -254,7 +254,7 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testSecondaryClick_Metrics_Status_Bar_Status() {
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
- mTile.secondaryClick(null /* view */);
+ mTile.secondaryClick(null /* expandable */);
verify(mMetricsLogger).write(mLogCaptor.capture());
assertEquals(StatusBarState.KEYGUARD, mLogCaptor.getValue()
.getTaggedData(FIELD_STATUS_BAR_STATE));
@@ -262,7 +262,7 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testLongClick_Metrics() {
- mTile.longClick(null /* view */);
+ mTile.longClick(null /* expandable */);
verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_LONG_PRESS)));
assertEquals(1, mUiEventLoggerFake.numLogs());
UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0);
@@ -274,7 +274,7 @@ public class QSTileImplTest extends SysuiTestCase {
public void testLongClick_log() {
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
- mTile.longClick(null /* view */);
+ mTile.longClick(null /* expandable */);
verify(mQsLogger).logTileLongClick(eq(SPEC), eq(StatusBarState.SHADE),
eq(Tile.STATE_ACTIVE), anyInt());
}
@@ -299,7 +299,7 @@ public class QSTileImplTest extends SysuiTestCase {
@Test
public void testLongClick_Metrics_Status_Bar_Status() {
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
- mTile.click(null /* view */);
+ mTile.click(null /* expandable */);
verify(mMetricsLogger).write(mLogCaptor.capture());
assertEquals(StatusBarState.SHADE_LOCKED, mLogCaptor.getValue()
.getTaggedData(FIELD_STATUS_BAR_STATE));
@@ -560,12 +560,12 @@ public class QSTileImplTest extends SysuiTestCase {
}
@Override
- protected void handleClick(@Nullable View view) {
+ protected void handleClick(@Nullable Expandable expandable) {
mClicked = true;
}
@Override
- protected void handleLongClick(@Nullable View view) {
+ protected void handleLongClick(@Nullable Expandable expandable) {
mLongClicked = true;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
index 605dc1402fc3..2c49e925edd6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt
@@ -21,11 +21,10 @@ import android.os.Handler
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
-import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile
@@ -34,6 +33,7 @@ import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
@@ -59,24 +59,15 @@ class BatterySaverTileTest : SysuiTestCase() {
private const val USER = 10
}
- @Mock
- private lateinit var userContext: Context
- @Mock
- private lateinit var qsHost: QSHost
- @Mock
- private lateinit var uiEventLogger: QsEventLogger
- @Mock
- private lateinit var metricsLogger: MetricsLogger
- @Mock
- private lateinit var statusBarStateController: StatusBarStateController
- @Mock
- private lateinit var activityStarter: ActivityStarter
- @Mock
- private lateinit var qsLogger: QSLogger
- @Mock
- private lateinit var batteryController: BatteryController
- @Mock
- private lateinit var view: View
+ @Mock private lateinit var userContext: Context
+ @Mock private lateinit var qsHost: QSHost
+ @Mock private lateinit var uiEventLogger: QsEventLogger
+ @Mock private lateinit var metricsLogger: MetricsLogger
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+ @Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var qsLogger: QSLogger
+ @Mock private lateinit var batteryController: BatteryController
+ @Mock private lateinit var expandable: Expandable
private lateinit var secureSettings: SecureSettings
private lateinit var testableLooper: TestableLooper
private lateinit var tile: BatterySaverTile
@@ -91,7 +82,8 @@ class BatterySaverTileTest : SysuiTestCase() {
secureSettings = FakeSettings()
- tile = BatterySaverTile(
+ tile =
+ BatterySaverTile(
qsHost,
uiEventLogger,
testableLooper.looper,
@@ -102,7 +94,8 @@ class BatterySaverTileTest : SysuiTestCase() {
activityStarter,
qsLogger,
batteryController,
- secureSettings)
+ secureSettings
+ )
tile.initialize()
testableLooper.processAllMessages()
@@ -131,23 +124,23 @@ class BatterySaverTileTest : SysuiTestCase() {
@Test
fun testClickingPowerSavePassesView() {
tile.onPowerSaveChanged(true)
- tile.handleClick(view)
+ tile.handleClick(expandable)
tile.onPowerSaveChanged(false)
- tile.handleClick(view)
+ tile.handleClick(expandable)
- verify(batteryController).setPowerSaveMode(true, view)
- verify(batteryController).setPowerSaveMode(false, view)
+ verify(batteryController).setPowerSaveMode(true, expandable)
+ verify(batteryController).setPowerSaveMode(false, expandable)
}
@Test
fun testStopListeningClearsViewInController() {
clearInvocations(batteryController)
tile.handleSetListening(true)
- verify(batteryController, never()).clearLastPowerSaverStartView()
+ verify(batteryController, never()).clearLastPowerSaverStartExpandable()
tile.handleSetListening(false)
- verify(batteryController).clearLastPowerSaverStartView()
+ verify(batteryController).clearLastPowerSaverStartExpandable()
}
@Test
@@ -158,7 +151,7 @@ class BatterySaverTileTest : SysuiTestCase() {
tile.handleUpdateState(state, /* arg= */ null)
assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_off))
+ .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_off))
}
@Test
@@ -169,6 +162,6 @@ class BatterySaverTileTest : SysuiTestCase() {
tile.handleUpdateState(state, /* arg= */ null)
assertThat(state.icon)
- .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_on))
+ .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_battery_saver_icon_on))
}
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
index cca1344424ac..1173fa31fbb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/DndTileTest.kt
@@ -26,12 +26,12 @@ import android.provider.Settings.Global.ZEN_MODE_OFF
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.ContextThemeWrapper
-import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile
@@ -42,7 +42,6 @@ import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
@@ -99,6 +98,12 @@ class DndTileTest : SysuiTestCase() {
@Mock
private lateinit var hostDialog: Dialog
+ @Mock
+ private lateinit var expandable: Expandable
+
+ @Mock
+ private lateinit var controller: DialogTransitionAnimator.Controller
+
private lateinit var secureSettings: SecureSettings
private lateinit var testableLooper: TestableLooper
private lateinit var tile: DndTile
@@ -119,6 +124,7 @@ class DndTileTest : SysuiTestCase() {
}
}
whenever(qsHost.context).thenReturn(wrappedContext)
+ whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
tile = DndTile(
qsHost,
@@ -187,11 +193,10 @@ class DndTileTest : SysuiTestCase() {
secureSettings.putIntForUser(KEY, Settings.Secure.ZEN_DURATION_PROMPT, DEFAULT_USER)
testableLooper.processAllMessages()
- val view = View(context)
- tile.handleClick(view)
+ tile.handleClick(expandable)
testableLooper.processAllMessages()
- verify(mDialogTransitionAnimator).showFromView(any(), eq(view), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@Test
@@ -201,8 +206,7 @@ class DndTileTest : SysuiTestCase() {
secureSettings.putIntForUser(KEY, 60, DEFAULT_USER)
testableLooper.processAllMessages()
- val view = View(context)
- tile.handleClick(view)
+ tile.handleClick(expandable)
testableLooper.processAllMessages()
verify(mDialogTransitionAnimator, never())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
index 1f5ebfec1a56..1c42dd15ce3b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/FontScalingTileTest.kt
@@ -20,12 +20,12 @@ import android.os.Handler
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
-import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.fontscaling.FontScalingDialogDelegate
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -37,7 +37,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
@@ -67,6 +66,8 @@ class FontScalingTileTest : SysuiTestCase() {
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var fontScalingDialogDelegate: FontScalingDialogDelegate
@Mock private lateinit var dialog: SystemUIDialog
+ @Mock private lateinit var expandable: Expandable
+ @Mock private lateinit var controller: DialogTransitionAnimator.Controller
private lateinit var testableLooper: TestableLooper
private lateinit var systemClock: FakeSystemClock
@@ -81,6 +82,7 @@ class FontScalingTileTest : SysuiTestCase() {
testableLooper = TestableLooper.get(this)
`when`(qsHost.getContext()).thenReturn(mContext)
`when`(fontScalingDialogDelegate.createDialog()).thenReturn(dialog)
+ `when`(expandable.dialogTransitionController(any())).thenReturn(controller)
systemClock = FakeSystemClock()
backgroundDelayableExecutor = FakeExecutor(systemClock)
@@ -119,8 +121,7 @@ class FontScalingTileTest : SysuiTestCase() {
@Test
fun clickTile_screenUnlocked_showDialogAnimationFromView() {
`when`(keyguardStateController.isShowing).thenReturn(false)
- val view = View(context)
- fontScalingTile.click(view)
+ fontScalingTile.click(expandable)
testableLooper.processAllMessages()
verify(activityStarter)
@@ -132,14 +133,13 @@ class FontScalingTileTest : SysuiTestCase() {
eq(false)
)
argumentCaptor.value.run()
- verify(mDialogTransitionAnimator).showFromView(any(), eq(view), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@Test
fun clickTile_onLockScreen_neverShowDialogAnimationFromView() {
`when`(keyguardStateController.isShowing).thenReturn(true)
- val view = View(context)
- fontScalingTile.click(view)
+ fontScalingTile.click(expandable)
testableLooper.processAllMessages()
verify(activityStarter)
@@ -151,8 +151,7 @@ class FontScalingTileTest : SysuiTestCase() {
eq(false)
)
argumentCaptor.value.run()
- verify(mDialogTransitionAnimator, never())
- .showFromView(any(), eq(view), nullable(), anyBoolean())
+ verify(mDialogTransitionAnimator, never()).show(any(), any(), anyBoolean())
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
index 73aa54cef66a..56671bf357ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
@@ -38,6 +38,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -135,10 +136,10 @@ public class HearingDevicesTileTest extends SysuiTestCase {
@Test
public void handleClick_dialogShown() {
- View view = new View(mContext);
- mTile.handleClick(view);
+ Expandable expandable = Expandable.fromView(new View(mContext));
+ mTile.handleClick(expandable);
mTestableLooper.processAllMessages();
- verify(mHearingDevicesDialogManager).showDialog(view);
+ verify(mHearingDevicesDialogManager).showDialog(expandable);
}
}
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 387f27d048ea..effae5f67f02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -241,6 +241,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
statusBarWinController,
sysUiState,
mock(),
+ mock(),
userTracker,
wakefulnessLifecycle,
uiEventLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
index 91f39126c67c..5e7d8fb5df02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionExecutorTest.kt
@@ -28,7 +28,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
-import kotlin.test.Ignore
import kotlin.test.Test
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -56,7 +55,6 @@ class ActionExecutorTest : SysuiTestCase() {
private lateinit var actionExecutor: ActionExecutor
- @Ignore // Fixed with newer mockito version (in main)
@Test
fun startSharedTransition_callsLaunchIntent() = runTest {
actionExecutor = createActionExecutor()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt
index d44e26c266fc..e32086b79918 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModelTest.kt
@@ -34,20 +34,21 @@ class ScreenshotViewModelTest {
assertThat(viewModel.actions.value).isEmpty()
- viewModel.addAction(appearance, onclick)
+ viewModel.addAction(appearance, true, onclick)
assertThat(viewModel.actions.value).hasSize(1)
val added = viewModel.actions.value[0]
assertThat(added.appearance).isEqualTo(appearance)
assertThat(added.onClicked).isEqualTo(onclick)
+ assertThat(added.showDuringEntrance).isTrue()
}
@Test
fun testRemoveAction() {
val viewModel = ScreenshotViewModel(accessibilityManager)
- val firstId = viewModel.addAction(ActionButtonAppearance(null, "", ""), {})
- val secondId = viewModel.addAction(appearance, onclick)
+ val firstId = viewModel.addAction(ActionButtonAppearance(null, "", ""), false, {})
+ val secondId = viewModel.addAction(appearance, false, onclick)
assertThat(viewModel.actions.value).hasSize(2)
assertThat(firstId).isNotEqualTo(secondId)
@@ -58,13 +59,14 @@ class ScreenshotViewModelTest {
val remaining = viewModel.actions.value[0]
assertThat(remaining.appearance).isEqualTo(appearance)
+ assertThat(remaining.showDuringEntrance).isFalse()
assertThat(remaining.onClicked).isEqualTo(onclick)
}
@Test
fun testUpdateActionAppearance() {
val viewModel = ScreenshotViewModel(accessibilityManager)
- val id = viewModel.addAction(appearance, onclick)
+ val id = viewModel.addAction(appearance, false, onclick)
val otherAppearance = ActionButtonAppearance(null, "Other", "Other")
viewModel.updateActionAppearance(id, otherAppearance)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 99204e75e5a3..537049c7b957 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -18,7 +18,7 @@ package com.android.systemui.shade
import android.graphics.Rect
import android.os.PowerManager
-import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.ViewUtils
import android.view.MotionEvent
@@ -27,6 +27,7 @@ import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
@@ -42,16 +43,11 @@ import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
@@ -59,6 +55,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -71,14 +68,12 @@ import org.mockito.Mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
@ExperimentalCoroutinesApi
-@RunWith(ParameterizedAndroidJunit4::class)
+@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
-class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class GlanceableHubContainerControllerTest : SysuiTestCase() {
private val kosmos: Kosmos =
testKosmos().apply {
// UnconfinedTestDispatcher makes testing simpler due to CommunalInteractor flows using
@@ -100,10 +95,6 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
private lateinit var communalRepository: FakeCommunalRepository
private lateinit var underTest: GlanceableHubContainerController
- init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
- }
-
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -127,7 +118,6 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
communalInteractor,
communalViewModel,
dialogFactory,
- keyguardTransitionInteractor,
keyguardInteractor,
shadeInteractor,
powerManager,
@@ -170,7 +160,6 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
communalInteractor,
communalViewModel,
dialogFactory,
- keyguardTransitionInteractor,
keyguardInteractor,
shadeInteractor,
powerManager,
@@ -216,13 +205,39 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
}
@Test
+ fun onTouchEvent_communalTransitioning_interceptsTouches() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is opening.
+ communalRepository.setTransitionState(
+ flowOf(
+ ObservableTransitionState.Transition(
+ fromScene = CommunalScenes.Blank,
+ toScene = CommunalScenes.Communal,
+ currentScene = flowOf(CommunalScenes.Blank),
+ progress = flowOf(0.5f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true)
+ )
+ )
+ )
+ testableLooper.processAllMessages()
+
+ // Touch events are intercepted.
+ assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+ // User activity sent to PowerManager.
+ verify(powerManager).userActivity(any(), any(), any())
+ }
+ }
+
+ @Test
fun onTouchEvent_communalOpen_interceptsTouches() =
with(kosmos) {
testScope.runTest {
// Communal is open.
goToScene(CommunalScenes.Communal)
- // Touch events are intercepted outside of any gesture areas.
+ // Touch events are intercepted.
assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
// User activity sent to PowerManager.
verify(powerManager).userActivity(any(), any(), any())
@@ -289,7 +304,6 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
communalInteractor,
communalViewModel,
dialogFactory,
- keyguardTransitionInteractor,
keyguardInteractor,
shadeInteractor,
powerManager,
@@ -309,7 +323,6 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
communalInteractor,
communalViewModel,
dialogFactory,
- keyguardTransitionInteractor,
keyguardInteractor,
shadeInteractor,
powerManager,
@@ -501,13 +514,6 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
}
private fun goToScene(scene: SceneKey) {
- if (SceneContainerFlag.isEnabled) {
- if (scene == CommunalScenes.Communal) {
- kosmos.sceneInteractor.changeScene(Scenes.Communal, "test")
- } else {
- kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "test")
- }
- }
communalRepository.changeScene(scene)
testableLooper.processAllMessages()
}
@@ -536,11 +542,5 @@ class GlanceableHubContainerControllerTest(flags: FlagsParameterization?) : Sysu
MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
-
- @JvmStatic
- @Parameters(name = "{0}")
- fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf().andSceneContainer()
- }
}
}
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 112829af2068..a867b0f7df00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -17,12 +17,15 @@
package com.android.systemui.shade
import android.content.Context
+import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.view.ViewTreeObserver
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardSecurityContainerController
import com.android.keyguard.LegacyLockIconViewController
@@ -72,7 +75,6 @@ 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 java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
@@ -80,12 +82,12 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.atLeast
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
@@ -93,6 +95,7 @@ import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
+import java.util.Optional
import org.mockito.Mockito.`when` as whenever
@OptIn(ExperimentalCoroutinesApi::class)
@@ -152,6 +155,7 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization?) :
private lateinit var underTest: NotificationShadeWindowViewController
private lateinit var testScope: TestScope
+ private lateinit var testableLooper: TestableLooper
private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
@@ -181,6 +185,7 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization?) :
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
testScope = TestScope()
+ testableLooper = TestableLooper.get(this)
falsingCollector = FalsingCollectorFake()
fakeClock = FakeSystemClock()
underTest =
@@ -407,6 +412,7 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization?) :
}
@Test
+ @DisableSceneContainer
fun handleDispatchTouchEvent_glanceableHubIntercepts_returnsTrue() {
whenever(mGlanceableHubContainerController.onTouchEvent(DOWN_EVENT)).thenReturn(true)
underTest.setStatusBarViewController(phoneStatusBarViewController)
@@ -559,29 +565,42 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization?) :
}
@Test
- @Ignore("b/321332798")
+ @DisableSceneContainer
fun setsUpCommunalHubLayout_whenFlagEnabled() {
whenever(mGlanceableHubContainerController.communalAvailable())
- .thenReturn(MutableStateFlow(true))
+ .thenReturn(MutableStateFlow(true))
- val mockCommunalView = mock(View::class.java)
+ val communalView = View(context)
whenever(mGlanceableHubContainerController.initView(any<Context>()))
- .thenReturn(mockCommunalView)
+ .thenReturn(communalView)
val mockCommunalPlaceholder = mock(View::class.java)
val fakeViewIndex = 20
whenever(view.findViewById<View>(R.id.communal_ui_stub)).thenReturn(mockCommunalPlaceholder)
whenever(view.indexOfChild(mockCommunalPlaceholder)).thenReturn(fakeViewIndex)
whenever(view.context).thenReturn(context)
+ whenever(view.viewTreeObserver).thenReturn(mock(ViewTreeObserver::class.java))
underTest.setupCommunalHubLayout()
- // Communal view added as a child of the container at the proper index, the stub is removed.
- verify(view).removeView(mockCommunalPlaceholder)
- verify(view).addView(eq(mockCommunalView), eq(fakeViewIndex))
+ // Simluate attaching the view so flow collection starts.
+ val onAttachStateChangeListenerArgumentCaptor = ArgumentCaptor.forClass(
+ View.OnAttachStateChangeListener::class.java
+ )
+ verify(view, atLeast(1)).addOnAttachStateChangeListener(
+ onAttachStateChangeListenerArgumentCaptor.capture()
+ )
+ for (listener in onAttachStateChangeListenerArgumentCaptor.allValues) {
+ listener.onViewAttachedToWindow(view)
+ }
+ testableLooper.processAllMessages()
+
+ // Communal view added as a child of the container at the proper index.
+ verify(view).addView(eq(communalView), eq(fakeViewIndex))
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_COMMUNAL_HUB)
fun doesNotSetupCommunalHubLayout_whenFlagDisabled() {
whenever(mGlanceableHubContainerController.communalAvailable())
.thenReturn(MutableStateFlow(false))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
index bedb2b33d07b..e0eb99cebd37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyboardShortcutsReceiverTest.java
@@ -16,120 +16,161 @@
package com.android.systemui.statusbar;
+import static com.android.systemui.Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE;
+
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.content.Intent;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.shared.recents.utilities.Utilities;
+import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
+import org.mockito.MockitoAnnotations;
import org.mockito.quality.Strictness;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class KeyboardShortcutsReceiverTest extends SysuiTestCase {
- @Rule public MockitoRule mockito = MockitoJUnit.rule();
+ private static final Intent SHOW_INTENT = new Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS);
+ private static final Intent DISMISS_INTENT =
+ new Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS);
+ private StaticMockitoSession mockitoSession;
private KeyboardShortcutsReceiver mKeyboardShortcutsReceiver;
- private Intent mIntent;
- private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock private KeyboardShortcuts mKeyboardShortcuts;
@Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
@Before
public void setUp() {
- mIntent = new Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS);
+ MockitoAnnotations.initMocks(this);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
mKeyboardShortcuts.mContext = mContext;
mKeyboardShortcutListSearch.mContext = mContext;
KeyboardShortcuts.sInstance = mKeyboardShortcuts;
KeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
+
+ mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
+ }
+
+ @Before
+ public void startStaticMocking() {
+ mockitoSession =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(Utilities.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ }
+
+ @After
+ public void endStaticMocking() {
+ mockitoSession.finishMocking();
}
@Test
public void onReceive_whenFlagOffDeviceIsTablet_showKeyboardShortcuts() {
- MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Utilities.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
- mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
when(Utilities.isLargeScreen(mContext)).thenReturn(true);
- mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+ mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
- mockitoSession.finishMocking();
}
@Test
public void onReceive_whenFlagOffDeviceIsNotTablet_showKeyboardShortcuts() {
- MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Utilities.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
- mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
when(Utilities.isLargeScreen(mContext)).thenReturn(false);
- mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+ mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
- mockitoSession.finishMocking();
}
@Test
public void onReceive_whenFlagOnDeviceIsTablet_showKeyboardShortcutListSearch() {
- MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Utilities.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
- mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
when(Utilities.isLargeScreen(mContext)).thenReturn(true);
- mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+ mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
verify(mKeyboardShortcuts, never()).showKeyboardShortcuts(anyInt());
verify(mKeyboardShortcutListSearch).showKeyboardShortcuts(anyInt());
- mockitoSession.finishMocking();
}
@Test
public void onReceive_whenFlagOnDeviceIsNotTablet_showKeyboardShortcuts() {
- MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Utilities.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
- mKeyboardShortcutsReceiver = spy(new KeyboardShortcutsReceiver(mFeatureFlags));
when(Utilities.isLargeScreen(mContext)).thenReturn(false);
- mKeyboardShortcutsReceiver.onReceive(mContext, mIntent);
+ mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
verify(mKeyboardShortcuts).showKeyboardShortcuts(anyInt());
verify(mKeyboardShortcutListSearch, never()).showKeyboardShortcuts(anyInt());
- mockitoSession.finishMocking();
+ }
+
+ @Test
+ public void onShowIntent_rewriteFlagOn_oldFlagOn_isLargeScreen_doesNotLaunchOldVersions() {
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ when(Utilities.isLargeScreen(mContext)).thenReturn(true);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
+
+ verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void onShowIntent_rewriteFlagOn_oldFlagOn_isSmallScreen_doesNotLaunchOldVersions() {
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ when(Utilities.isLargeScreen(mContext)).thenReturn(false);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, SHOW_INTENT);
+
+ verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void onDismissIntent_rewriteFlagOn_oldFlagOn_isLargeScreen_doesNotDismissOldVersions() {
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ when(Utilities.isLargeScreen(mContext)).thenReturn(true);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, DISMISS_INTENT);
+
+ verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void onDismissIntent_rewriteFlagOn_oldFlagOn_isSmallScreen_doesNotDismissOldVersions() {
+ mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ when(Utilities.isLargeScreen(mContext)).thenReturn(false);
+
+ mKeyboardShortcutsReceiver.onReceive(mContext, DISMISS_INTENT);
+
+ verifyZeroInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 158f38d48197..347620a1631a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -19,12 +19,13 @@
package com.android.systemui.statusbar.notification.footer.ui.viewmodel
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.provider.Settings
-import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -33,7 +34,7 @@ import com.android.systemui.power.data.repository.powerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
-import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository
import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -45,13 +46,16 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
-@RunWith(AndroidTestingRunner::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@SmallTest
@EnableFlags(FooterViewRefactor.FLAG_NAME)
-class FooterViewModelTest : SysuiTestCase() {
+class FooterViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -59,11 +63,29 @@ class FooterViewModelTest : SysuiTestCase() {
private val testScope = kosmos.testScope
private val activeNotificationListRepository = kosmos.activeNotificationListRepository
private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
- private val shadeRepository = kosmos.shadeRepository
private val powerRepository = kosmos.powerRepository
private val fakeSecureSettingsRepository = kosmos.fakeSecureSettingsRepository
- val underTest = kosmos.footerViewModel
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private lateinit var underTest: FooterViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ @Before
+ fun setup() {
+ underTest = kosmos.footerViewModel
+ }
@Test
fun messageVisible_whenFilteredNotifications() =
@@ -146,11 +168,9 @@ class FooterViewModelTest : SysuiTestCase() {
val visible by collectLastValue(underTest.clearAllButton.isVisible)
runCurrent()
- // WHEN shade is expanded
+ // WHEN shade is expanded AND QS not expanded
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- shadeRepository.setLegacyShadeExpansion(1f)
- // AND QS not expanded
- shadeRepository.setQsExpansion(0f)
+ shadeTestUtil.setShadeAndQsExpansion(1f, 0f)
// AND device is awake
powerRepository.updateWakefulness(
rawState = WakefulnessState.AWAKE,
@@ -182,9 +202,9 @@ class FooterViewModelTest : SysuiTestCase() {
// WHEN shade is collapsed
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- shadeRepository.setLegacyShadeExpansion(0f)
+ shadeTestUtil.setShadeExpansion(0f)
// AND QS not expanded
- shadeRepository.setQsExpansion(0f)
+ shadeTestUtil.setQsExpansion(0f)
// AND device is awake
powerRepository.updateWakefulness(
rawState = WakefulnessState.AWAKE,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index a66a1360f8f8..f262df1d875a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -24,6 +24,7 @@ import static com.android.systemui.statusbar.notification.stack.NotificationStac
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -43,6 +44,7 @@ import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -51,11 +53,14 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.nano.MetricsProto;
+import com.android.systemui.ExpandHelper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.DisableSceneContainer;
+import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
@@ -171,6 +176,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
@Mock private NotificationListViewBinder mViewBinder;
@Mock
private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
+ @Mock private ExpandHelper mExpandHelper;
@Captor
private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor;
@@ -895,6 +901,50 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any());
}
+ @Test
+ @EnableSceneContainer
+ public void onTouchEvent_stopExpandingNotification_sceneContainerEnabled() {
+ boolean touchHandled = stopExpandingNotification();
+
+ verify(mNotificationStackScrollLayout).startOverscrollAfterExpanding();
+ verify(mNotificationStackScrollLayout, never()).dispatchDownEventToScroller(any());
+ assertTrue(touchHandled);
+ }
+
+ @Test
+ @DisableSceneContainer
+ public void onTouchEvent_stopExpandingNotification_sceneContainerDisabled() {
+ stopExpandingNotification();
+
+ verify(mNotificationStackScrollLayout, never()).startOverscrollAfterExpanding();
+ verify(mNotificationStackScrollLayout).dispatchDownEventToScroller(any());
+ }
+
+ private boolean stopExpandingNotification() {
+ when(mNotificationStackScrollLayout.getExpandHelper()).thenReturn(mExpandHelper);
+ when(mNotificationStackScrollLayout.getIsExpanded()).thenReturn(true);
+ when(mNotificationStackScrollLayout.getExpandedInThisMotion()).thenReturn(true);
+ when(mNotificationStackScrollLayout.isExpandingNotification()).thenReturn(true);
+
+ when(mExpandHelper.onTouchEvent(any())).thenAnswer(i -> {
+ when(mNotificationStackScrollLayout.isExpandingNotification()).thenReturn(false);
+ return false;
+ });
+
+ initController(/* viewIsAttached= */ true);
+ NotificationStackScrollLayoutController.TouchHandler touchHandler =
+ mController.getTouchHandler();
+
+ return touchHandler.onTouchEvent(MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 0,
+ MotionEvent.ACTION_DOWN,
+ 0,
+ 0,
+ /* metaState= */ 0
+ ));
+ }
+
private LogMaker logMatcher(int category, int type) {
return argThat(new LogMatcher(category, type));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 939d0556e347..0c0a2a59d9bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -207,6 +207,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
.thenReturn(mNotificationRoundnessManager);
mStackScroller.setController(mStackScrollLayoutController);
mStackScroller.setShelf(mNotificationShelf);
+ when(mStackScroller.getExpandHelper()).thenReturn(mExpandHelper);
doNothing().when(mGroupExpansionManager).collapseGroups();
doNothing().when(mExpandHelper).cancelImmediately();
@@ -1139,6 +1140,14 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
assertFalse(mStackScroller.mHeadsUpAnimatingAway);
}
+ @Test
+ @EnableSceneContainer
+ public void finishExpanding_sceneContainerEnabled() {
+ mStackScroller.startOverscrollAfterExpanding();
+ verify(mStackScroller.getExpandHelper()).finishExpanding();
+ assertTrue(mStackScroller.getIsBeingDragged());
+ }
+
private MotionEvent captureTouchSentToSceneFramework() {
ArgumentCaptor<MotionEvent> captor = ArgumentCaptor.forClass(MotionEvent.class);
verify(mStackScrollLayoutController).sendTouchToSceneFramework(captor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 82725d656bb9..a6fb7187ff5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -394,8 +394,20 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
}
@Test
+ fun resetViewStates_shadeCollapsed_emptyShadeViewBecomesTransparent() {
+ ambientState.expansionFraction = 0f
+ stackScrollAlgorithm.initView(context)
+ hostView.removeAllViews()
+ hostView.addView(emptyShadeView)
+
+ stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
+
+ assertThat(emptyShadeView.viewState.alpha).isEqualTo(0f)
+ }
+
+ @Test
fun resetViewStates_isOnKeyguard_emptyShadeViewBecomesOpaque() {
- ambientState.setStatusBarState(StatusBarState.SHADE)
+ ambientState.setStatusBarState(StatusBarState.KEYGUARD)
ambientState.fractionToShade = 0.25f
stackScrollAlgorithm.initView(context)
hostView.removeAllViews()
@@ -403,7 +415,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
- assertThat(emptyShadeView.viewState.alpha).isEqualTo(1f)
+ val expected = getContentAlpha(ambientState.fractionToShade)
+ assertThat(emptyShadeView.viewState.alpha).isEqualTo(expected)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index f666d8e3ffa7..b9312d3ce2be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -21,9 +21,12 @@ import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
import static android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED;
import static android.provider.Settings.Global.HEADS_UP_ON;
+import static com.android.systemui.Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE;
import static com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION;
+import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.phone.CentralSurfaces.MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU;
import static com.google.common.truth.Truth.assertThat;
@@ -42,6 +45,7 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static java.util.Collections.emptySet;
@@ -52,6 +56,8 @@ import android.app.WallpaperManager;
import android.app.trust.TrustManager;
import android.content.BroadcastReceiver;
import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.graphics.Rect;
import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.AmbientDisplayConfiguration;
@@ -73,6 +79,8 @@ import android.util.DisplayMetrics;
import android.util.SparseArray;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
import androidx.test.filters.SmallTest;
@@ -137,6 +145,8 @@ import com.android.systemui.shade.ShadeControllerImpl;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.shade.ShadeLogger;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.KeyboardShortcutListSearch;
+import com.android.systemui.statusbar.KeyboardShortcuts;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.LightRevealScrim;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -202,6 +212,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.ByteArrayOutputStream;
@@ -326,18 +337,21 @@ public class CentralSurfacesImplTest extends SysuiTestCase {
@Mock IPowerManager mPowerManagerService;
@Mock ActivityStarter mActivityStarter;
@Mock private WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor;
+ @Mock private KeyboardShortcuts mKeyboardShortcuts;
+ @Mock private KeyboardShortcutListSearch mKeyboardShortcutListSearch;
private ShadeController mShadeController;
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings();
private final SystemSettings mSystemSettings = new FakeSettings();
private final FakeEventLog mFakeEventLog = new FakeEventLog();
- private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+ private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private final InitController mInitController = new InitController();
private final DumpManager mDumpManager = new DumpManager();
private final ScreenLifecycle mScreenLifecycle = new ScreenLifecycle(mDumpManager);
+ private MessageRouterImpl mMessageRouter = new MessageRouterImpl(mMainExecutor);
private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor =
mKosmos.getBrightnessMirrorShowingInteractor();
@@ -347,7 +361,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase {
MockitoAnnotations.initMocks(this);
// Set default value to avoid IllegalStateException.
- mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
mSetFlagsRule.enableFlags(FLAG_LIGHT_REVEAL_MIGRATION);
// Turn AOD on and toggle feature flag for jank fixes
mFeatureFlags.set(Flags.ZJ_285570694_LOCKSCREEN_TRANSITION_FROM_AOD, true);
@@ -462,6 +476,16 @@ public class CentralSurfacesImplTest extends SysuiTestCase {
}
private void createCentralSurfaces() {
+ mMainExecutor = new FakeExecutor(mFakeSystemClock);
+ mMessageRouter = new MessageRouterImpl(mMainExecutor);
+ mKeyboardShortcuts = mock(KeyboardShortcuts.class);
+ mKeyboardShortcutListSearch = mock(KeyboardShortcutListSearch.class);
+ // Test setup for legacy version
+ mKeyboardShortcuts.mContext = mContext;
+ mKeyboardShortcutListSearch.mContext = mContext;
+ KeyboardShortcuts.sInstance = mKeyboardShortcuts;
+ KeyboardShortcutListSearch.sInstance = mKeyboardShortcutListSearch;
+
ConfigurationController configurationController = new ConfigurationControllerImpl(mContext);
mCentralSurfaces = new CentralSurfacesImpl(
mContext,
@@ -554,7 +578,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase {
mFeatureFlags,
mKeyguardUnlockAnimationController,
mMainExecutor,
- new MessageRouterImpl(mMainExecutor),
+ mMessageRouter,
mWallpaperManager,
Optional.of(mStartingSurface),
mActivityTransitionAnimator,
@@ -1126,6 +1150,194 @@ public class CentralSurfacesImplTest extends SysuiTestCase {
verify(mScrimController, never()).transitionTo(eq(ScrimState.BRIGHTNESS_MIRROR), any());
}
+ @Test
+ public void dismissKeyboardShortcuts_largeScreen_bothFlagsEnabled_doesNotDismissAny() {
+ switchToLargeScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ dismissKeyboardShortcuts();
+
+ verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void dismissKeyboardShortcuts_largeScreen_newFlagsDisabled_dismissesTabletVersion() {
+ switchToLargeScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ dismissKeyboardShortcuts();
+
+ verify(mKeyboardShortcutListSearch).dismissKeyboardShortcuts();
+ }
+
+ @Test
+ public void dismissKeyboardShortcuts_largeScreen_bothFlagsDisabled_dismissesPhoneVersion() {
+ switchToLargeScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ dismissKeyboardShortcuts();
+
+ verify(mKeyboardShortcuts).dismissKeyboardShortcuts();
+ verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void dismissKeyboardShortcuts_smallScreen_bothFlagsEnabled_doesNotDismissAny() {
+ switchToSmallScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ dismissKeyboardShortcuts();
+
+ verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void dismissKeyboardShortcuts_smallScreen_newFlagsDisabled_dismissesPhoneVersion() {
+ switchToSmallScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ dismissKeyboardShortcuts();
+
+ verify(mKeyboardShortcuts).dismissKeyboardShortcuts();
+ verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void dismissKeyboardShortcuts_smallScreen_bothFlagsDisabled_dismissesPhoneVersion() {
+ switchToSmallScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ dismissKeyboardShortcuts();
+
+ verify(mKeyboardShortcuts).dismissKeyboardShortcuts();
+ verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void toggleKeyboardShortcuts_largeScreen_bothFlagsEnabled_doesNotTogglesAny() {
+ switchToLargeScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ int deviceId = 321;
+ toggleKeyboardShortcuts(/* deviceId= */ deviceId);
+
+ verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void toggleKeyboardShortcuts_largeScreen_newFlagsDisabled_togglesTabletVersion() {
+ switchToLargeScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ int deviceId = 654;
+ toggleKeyboardShortcuts(deviceId);
+
+ verify(mKeyboardShortcutListSearch).showKeyboardShortcuts(deviceId);
+ verifyNoMoreInteractions(mKeyboardShortcuts);
+ }
+
+ @Test
+ public void toggleKeyboardShortcuts_largeScreen_bothFlagsDisabled_togglesPhoneVersion() {
+ switchToLargeScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ int deviceId = 987;
+ toggleKeyboardShortcuts(deviceId);
+
+ verify(mKeyboardShortcuts).showKeyboardShortcuts(deviceId);
+ verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void toggleKeyboardShortcuts_smallScreen_bothFlagsEnabled_doesNotToggleAny() {
+ switchToSmallScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.enableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ int deviceId = 789;
+ toggleKeyboardShortcuts(/* deviceId= */ deviceId);
+
+ verifyNoMoreInteractions(mKeyboardShortcuts, mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void toggleKeyboardShortcuts_smallScreen_newFlagsDisabled_togglesPhoneVersion() {
+ switchToSmallScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, true);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ int deviceId = 456;
+ toggleKeyboardShortcuts(deviceId);
+
+ verify(mKeyboardShortcuts).showKeyboardShortcuts(deviceId);
+ verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+ }
+
+ @Test
+ public void toggleKeyboardShortcuts_smallScreen_bothFlagsDisabled_togglesPhoneVersion() {
+ switchToSmallScreen();
+ mFeatureFlags.set(SHORTCUT_LIST_SEARCH_LAYOUT, false);
+ mSetFlagsRule.disableFlags(FLAG_KEYBOARD_SHORTCUT_HELPER_REWRITE);
+ createCentralSurfaces();
+
+ int deviceId = 123;
+ toggleKeyboardShortcuts(deviceId);
+
+ verify(mKeyboardShortcuts).showKeyboardShortcuts(deviceId);
+ verifyNoMoreInteractions(mKeyboardShortcutListSearch);
+ }
+
+ private void dismissKeyboardShortcuts() {
+ mMessageRouter.sendMessage(MSG_DISMISS_KEYBOARD_SHORTCUTS_MENU);
+ mMainExecutor.runAllReady();
+ }
+
+ private void toggleKeyboardShortcuts(int deviceId) {
+ mMessageRouter.sendMessage(new CentralSurfaces.KeyboardShortcutsMessage(deviceId));
+ mMainExecutor.runAllReady();
+ }
+
+ private void switchToLargeScreen() {
+ switchToScreenSize(1280, 800);
+ }
+
+ private void switchToSmallScreen() {
+ switchToScreenSize(504, 1122);
+ }
+
+ private void switchToScreenSize(int widthDp, int heightDp) {
+ WindowMetrics windowMetrics = Mockito.mock(WindowMetrics.class);
+ WindowManager windowManager = Mockito.mock(WindowManager.class);
+
+ Configuration configuration = new Configuration();
+ configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT;
+ mContext.getOrCreateTestableResources().overrideConfiguration(configuration);
+
+ when(windowMetrics.getBounds()).thenReturn(new Rect(0, 0, widthDp, heightDp));
+ when(windowManager.getCurrentWindowMetrics()).thenReturn(windowMetrics);
+ mContext.addMockSystemService(WindowManager.class, windowManager);
+ }
+
/**
* Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
* to reconfigure the keyguard to reflect the requested showing/occluded states.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
new file mode 100644
index 000000000000..7ca3b1c425d3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.pipeline.satellite.data
+
+import android.telephony.satellite.SatelliteManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.demomode.DemoMode
+import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteDataSource
+import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+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.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@SmallTest
+class DeviceBasedSatelliteRepositorySwitcherTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val demoModeController =
+ mock<DemoModeController>().apply { whenever(this.isInDemoMode).thenReturn(false) }
+ private val satelliteManager = mock<SatelliteManager>()
+ private val systemClock = FakeSystemClock()
+
+ private val realImpl =
+ DeviceBasedSatelliteRepositoryImpl(
+ Optional.of(satelliteManager),
+ testDispatcher,
+ testScope.backgroundScope,
+ FakeLogBuffer.Factory.create(),
+ systemClock,
+ )
+ private val demoDataSource =
+ mock<DemoDeviceBasedSatelliteDataSource>().also {
+ whenever(it.satelliteEvents)
+ .thenReturn(
+ MutableStateFlow(
+ DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+ connectionState = SatelliteConnectionState.Unknown,
+ signalStrength = 0,
+ )
+ )
+ )
+ }
+ private val demoImpl =
+ DemoDeviceBasedSatelliteRepository(demoDataSource, testScope.backgroundScope)
+
+ private val underTest =
+ DeviceBasedSatelliteRepositorySwitcher(
+ realImpl,
+ demoImpl,
+ demoModeController,
+ testScope.backgroundScope,
+ )
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun switcherActiveRepo_updatesWhenDemoModeChanges() =
+ testScope.runTest {
+ assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl)
+
+ val latest by collectLastValue(underTest.activeRepo)
+ runCurrent()
+
+ startDemoMode()
+
+ assertThat(latest).isSameInstanceAs(demoImpl)
+
+ finishDemoMode()
+
+ assertThat(latest).isSameInstanceAs(realImpl)
+ }
+
+ private fun startDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(true)
+ getDemoModeCallback().onDemoModeStarted()
+ }
+
+ private fun finishDemoMode() {
+ whenever(demoModeController.isInDemoMode).thenReturn(false)
+ getDemoModeCallback().onDemoModeFinished()
+ }
+
+ private fun getDemoModeCallback(): DemoMode {
+ val captor = kotlinArgumentCaptor<DemoMode>()
+ verify(demoModeController).addCallback(captor.capture())
+ return captor.value
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
new file mode 100644
index 000000000000..f77fd1999007
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.pipeline.satellite.data.demo
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+
+@SmallTest
+class DemoDeviceBasedSatelliteRepositoryTest : SysuiTestCase() {
+
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val fakeSatelliteEvents =
+ MutableStateFlow(
+ DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+ connectionState = SatelliteConnectionState.Unknown,
+ signalStrength = 0,
+ )
+ )
+
+ private lateinit var dataSource: DemoDeviceBasedSatelliteDataSource
+
+ private lateinit var underTest: DemoDeviceBasedSatelliteRepository
+
+ @Before
+ fun setUp() {
+ dataSource =
+ mock<DemoDeviceBasedSatelliteDataSource>().also {
+ whenever(it.satelliteEvents).thenReturn(fakeSatelliteEvents)
+ }
+
+ underTest = DemoDeviceBasedSatelliteRepository(dataSource, testScope.backgroundScope)
+ }
+
+ @Test
+ fun startProcessing_getsNewUpdates() =
+ testScope.runTest {
+ val latestConnection by collectLastValue(underTest.connectionState)
+ val latestSignalStrength by collectLastValue(underTest.signalStrength)
+
+ underTest.startProcessingCommands()
+
+ fakeSatelliteEvents.value =
+ DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+ connectionState = SatelliteConnectionState.On,
+ signalStrength = 3,
+ )
+
+ assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On)
+ assertThat(latestSignalStrength).isEqualTo(3)
+
+ fakeSatelliteEvents.value =
+ DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+ connectionState = SatelliteConnectionState.Connected,
+ signalStrength = 4,
+ )
+
+ assertThat(latestConnection).isEqualTo(SatelliteConnectionState.Connected)
+ assertThat(latestSignalStrength).isEqualTo(4)
+ }
+
+ @Test
+ fun stopProcessing_stopsGettingUpdates() =
+ testScope.runTest {
+ val latestConnection by collectLastValue(underTest.connectionState)
+ val latestSignalStrength by collectLastValue(underTest.signalStrength)
+
+ underTest.startProcessingCommands()
+
+ fakeSatelliteEvents.value =
+ DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+ connectionState = SatelliteConnectionState.On,
+ signalStrength = 3,
+ )
+ assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On)
+ assertThat(latestSignalStrength).isEqualTo(3)
+
+ underTest.stopProcessingCommands()
+
+ // WHEN new values are emitted
+ fakeSatelliteEvents.value =
+ DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent(
+ connectionState = SatelliteConnectionState.Connected,
+ signalStrength = 4,
+ )
+
+ // THEN they're not collected because we stopped processing commands, so the old values
+ // are still present
+ assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On)
+ assertThat(latestSignalStrength).isEqualTo(3)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 77e48bff04de..6b0ad4bdb770 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -156,7 +156,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture())
}
- assertThat(latest).isNull()
+ assertThat(latest).isEqualTo(0)
callback.onNtnSignalStrengthChanged(NtnSignalStrength(1))
assertThat(latest).isEqualTo(1)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index 9d4f1fc04594..ed8843b1d9f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -38,13 +38,13 @@ import android.os.PowerManager;
import android.os.PowerSaveState;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
-import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.dx.mockito.inline.extended.StaticInOrder;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager;
@@ -72,7 +72,7 @@ public class BatteryControllerTest extends SysuiTestCase {
@Mock private PowerManager mPowerManager;
@Mock private BroadcastDispatcher mBroadcastDispatcher;
@Mock private DemoModeController mDemoModeController;
- @Mock private View mView;
+ @Mock private Expandable mExpandable;
@Mock private UsbPort mUsbPort;
@Mock private UsbManager mUsbManager;
@Mock private UsbPortStatus mUsbPortStatus;
@@ -175,8 +175,8 @@ public class BatteryControllerTest extends SysuiTestCase {
@Test
public void testBatteryUtilsCalledOnSetPowerSaveMode() {
- mBatteryController.setPowerSaveMode(true, mView);
- mBatteryController.setPowerSaveMode(false, mView);
+ mBatteryController.setPowerSaveMode(true, mExpandable);
+ mBatteryController.setPowerSaveMode(false, mExpandable);
StaticInOrder inOrder = inOrder(staticMockMarker(BatterySaverUtils.class));
inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), true, true,
@@ -187,21 +187,21 @@ public class BatteryControllerTest extends SysuiTestCase {
@Test
public void testSaveViewReferenceWhenSettingPowerSaveMode() {
- mBatteryController.setPowerSaveMode(false, mView);
+ mBatteryController.setPowerSaveMode(false, mExpandable);
- Assert.assertNull(mBatteryController.getLastPowerSaverStartView());
+ Assert.assertNull(mBatteryController.getLastPowerSaverStartExpandable());
- mBatteryController.setPowerSaveMode(true, mView);
+ mBatteryController.setPowerSaveMode(true, mExpandable);
- Assert.assertSame(mView, mBatteryController.getLastPowerSaverStartView().get());
+ Assert.assertSame(mExpandable, mBatteryController.getLastPowerSaverStartExpandable().get());
}
@Test
public void testClearViewReference() {
- mBatteryController.setPowerSaveMode(true, mView);
- mBatteryController.clearLastPowerSaverStartView();
+ mBatteryController.setPowerSaveMode(true, mExpandable);
+ mBatteryController.clearLastPowerSaverStartExpandable();
- Assert.assertNull(mBatteryController.getLastPowerSaverStartView());
+ Assert.assertNull(mBatteryController.getLastPowerSaverStartExpandable());
}
@Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeOneHandedModeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeOneHandedModeRepository.kt
new file mode 100644
index 000000000000..ac135afaebff
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeOneHandedModeRepository.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.os.UserHandle
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeOneHandedModeRepository : OneHandedModeRepository {
+ private val userMap = mutableMapOf<Int, MutableStateFlow<Boolean>>()
+
+ override fun isEnabled(userHandle: UserHandle): StateFlow<Boolean> {
+ return getFlow(userHandle.identifier)
+ }
+
+ override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean {
+ getFlow(userHandle.identifier).value = isEnabled
+ return true
+ }
+
+ /** initializes the flow if already not */
+ private fun getFlow(userId: Int): MutableStateFlow<Boolean> {
+ return userMap.getOrPut(userId) { MutableStateFlow(false) }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryKosmos.kt
new file mode 100644
index 000000000000..9ee200ab5532
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeOneHandedModeRepository by Kosmos.Fixture { FakeOneHandedModeRepository() }
+val Kosmos.oneHandedModeRepository by Kosmos.Fixture { fakeOneHandedModeRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
index 2e2cf9a61a8c..0975687b4744 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
@@ -81,7 +81,8 @@ class FakePromptRepository : PromptRepository {
com.android.systemui.Flags.constraintBp() &&
!Utils.isBiometricAllowed(promptInfo) &&
Utils.isDeviceCredentialAllowed(promptInfo) &&
- promptInfo.contentView != null
+ promptInfo.contentView != null &&
+ !promptInfo.isContentViewMoreOptionsButtonUsed
_showBpWithoutIconForCredential.value = showBpForCredential && !hasCredentialViewShown
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
index 9f5c6b8faa38..d958bae7ae2a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
@@ -23,6 +23,7 @@ class FakeCommunalRepository(
) : CommunalRepository {
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
this.currentScene.value = toScene
+ this._transitionState.value = flowOf(ObservableTransitionState.Idle(toScene))
}
private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index 3dd382f6db3e..3fe6973d36df 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -23,14 +23,15 @@ import com.android.systemui.communal.data.repository.communalPrefsRepository
import com.android.systemui.communal.data.repository.communalRepository
import com.android.systemui.communal.data.repository.communalWidgetRepository
import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
-import com.android.systemui.dock.fakeDockManager
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -42,6 +43,7 @@ import com.android.systemui.util.mockito.mock
val Kosmos.communalInteractor by Fixture {
CommunalInteractor(
applicationScope = applicationCoroutineScope,
+ bgDispatcher = testDispatcher,
broadcastDispatcher = broadcastDispatcher,
communalRepository = communalRepository,
widgetRepository = communalWidgetRepository,
@@ -49,13 +51,13 @@ val Kosmos.communalInteractor by Fixture {
mediaRepository = communalMediaRepository,
smartspaceRepository = smartspaceRepository,
keyguardInteractor = keyguardInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
communalSettingsInteractor = communalSettingsInteractor,
appWidgetHost = mock(),
editWidgetsActivityStarter = editWidgetsActivityStarter,
userTracker = userTracker,
activityStarter = activityStarter,
userManager = userManager,
- dockManager = fakeDockManager,
sceneInteractor = sceneInteractor,
logBuffer = logcatLogBuffer("CommunalInteractor"),
tableLogBuffer = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index a242368d1ee1..2fe7438bcc77 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -40,12 +40,21 @@ import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
-/** Fake implementation of [KeyguardTransitionRepository] */
+/**
+ * Fake implementation of [KeyguardTransitionRepository].
+ *
+ * By default, will be seeded with a transition from OFF -> LOCKSCREEN, which is the most common
+ * case. If the lockscreen is disabled, or we're in setup wizard, the repository will initialize
+ * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior.
+ */
@SysUISingleton
-class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitionRepository {
+class FakeKeyguardTransitionRepository(
+ private val initInLockscreen: Boolean = true,
+) : KeyguardTransitionRepository {
private val _transitions =
MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val transitions: SharedFlow<TransitionStep> = _transitions
+ @Inject constructor() : this(initInLockscreen = true)
private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
MutableStateFlow(
@@ -59,8 +68,21 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio
override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
init {
- // Seed the fake repository with the same initial steps the actual repository uses.
- KeyguardTransitionRepositoryImpl.initialTransitionSteps.forEach { _transitions.tryEmit(it) }
+ // Seed with a FINISHED transition in OFF, same as the real repository.
+ _transitions.tryEmit(
+ TransitionStep(
+ KeyguardState.OFF,
+ KeyguardState.OFF,
+ 1f,
+ TransitionState.FINISHED,
+ )
+ )
+
+ if (initInLockscreen) {
+ tryEmitInitialStepsFromOff(KeyguardState.LOCKSCREEN)
+ } else {
+ tryEmitInitialStepsFromOff(KeyguardState.OFF)
+ }
}
/**
@@ -223,6 +245,32 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio
return if (info.animator == null) UUID.randomUUID() else null
}
+ override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+ tryEmitInitialStepsFromOff(to)
+ }
+
+ private fun tryEmitInitialStepsFromOff(to: KeyguardState) {
+ _transitions.tryEmit(
+ TransitionStep(
+ KeyguardState.OFF,
+ to,
+ 0f,
+ TransitionState.STARTED,
+ ownerName = "KeyguardTransitionRepository(boot)",
+ )
+ )
+
+ _transitions.tryEmit(
+ TransitionStep(
+ KeyguardState.OFF,
+ to,
+ 1f,
+ TransitionState.FINISHED,
+ ownerName = "KeyguardTransitionRepository(boot)",
+ ),
+ )
+ }
+
override fun updateTransition(
transitionId: UUID,
@FloatRange(from = 0.0, to = 1.0) value: Float,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractorKosmos.kt
new file mode 100644
index 000000000000..7d8d33f7dd11
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionBootInteractorKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
+
+val Kosmos.keyguardTransitionBootInteractor: KeyguardTransitionBootInteractor by
+ Kosmos.Fixture {
+ KeyguardTransitionBootInteractor(
+ scope = applicationCoroutineScope,
+ deviceEntryInteractor = deviceEntryInteractor,
+ deviceProvisioningInteractor = deviceProvisioningInteractor,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ repository = keyguardTransitionRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
index 0307c414351f..c4bf8ff12817 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/actions/FakeQSTileIntentUserInputHandler.kt
@@ -18,7 +18,7 @@ package com.android.systemui.qs.tiles.base.actions
import android.app.PendingIntent
import android.content.Intent
-import android.view.View
+import com.android.systemui.animation.Expandable
/**
* Fake implementation of [QSTileIntentUserInputHandler] interface. Consider using this alongside
@@ -31,22 +31,24 @@ class FakeQSTileIntentUserInputHandler : QSTileIntentUserInputHandler {
private val mutableInputs = mutableListOf<Input>()
- override fun handle(view: View?, intent: Intent) {
- mutableInputs.add(Input.Intent(view, intent))
+ override fun handle(expandable: Expandable?, intent: Intent) {
+ mutableInputs.add(Input.Intent(expandable, intent))
}
override fun handle(
- view: View?,
+ expandable: Expandable?,
pendingIntent: PendingIntent,
requestLaunchingDefaultActivity: Boolean
) {
- mutableInputs.add(Input.PendingIntent(view, pendingIntent, requestLaunchingDefaultActivity))
+ mutableInputs.add(
+ Input.PendingIntent(expandable, pendingIntent, requestLaunchingDefaultActivity)
+ )
}
sealed interface Input {
- data class Intent(val view: View?, val intent: android.content.Intent) : Input
+ data class Intent(val expandable: Expandable?, val intent: android.content.Intent) : Input
data class PendingIntent(
- val view: View?,
+ val expandable: Expandable?,
val pendingIntent: android.app.PendingIntent,
val requestLaunchingDefaultActivity: Boolean
) : Input
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
index 832b07a0763b..9cb76bb412f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/QSTileInputTestKtx.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.tiles.base.interactor
import android.os.UserHandle
-import android.view.View
+import com.android.systemui.animation.Expandable
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
object QSTileInputTestKtx {
@@ -25,12 +25,12 @@ object QSTileInputTestKtx {
fun <T> click(
data: T,
user: UserHandle = UserHandle.CURRENT,
- view: View? = null,
- ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.Click(view), data)
+ expandable: Expandable? = null,
+ ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.Click(expandable), data)
fun <T> longClick(
data: T,
user: UserHandle = UserHandle.CURRENT,
- view: View? = null,
- ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.LongClick(view), data)
+ expandable: Expandable? = null,
+ ): QSTileInput<T> = QSTileInput(user, QSTileUserAction.LongClick(expandable), data)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/onehanded/OneHandedModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/onehanded/OneHandedModeTileKosmos.kt
new file mode 100644
index 000000000000..d9c0361c8dc9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/onehanded/OneHandedModeTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.qs.tiles.impl.onehanded
+
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsOneHandedModeTileConfig by
+ Kosmos.Fixture { QSAccessibilityModule.provideOneHandedTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
index a654d6fc239a..e0f60e9b3a01 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -21,7 +21,6 @@ import android.view.View
import com.android.systemui.settings.brightness.MirrorController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
@@ -30,8 +29,16 @@ class FakeQSSceneAdapter(
override val qqsHeight: Int = 0,
override val qsHeight: Int = 0,
) : QSSceneAdapter {
+ private val _customizerState = MutableStateFlow<CustomizerState>(CustomizerState.Hidden)
+
+ private val _customizerShowing = MutableStateFlow(false)
+ override val isCustomizerShowing = _customizerShowing.asStateFlow()
+
private val _customizing = MutableStateFlow(false)
- override val isCustomizing: StateFlow<Boolean> = _customizing.asStateFlow()
+ override val isCustomizing = _customizing.asStateFlow()
+
+ private val _animationDuration = MutableStateFlow(0)
+ override val customizerAnimationDuration = _animationDuration.asStateFlow()
private val _view = MutableStateFlow<View?>(null)
override val qsView: Flow<View> = _view.filterNotNull()
@@ -58,7 +65,7 @@ class FakeQSSceneAdapter(
}
fun setCustomizing(value: Boolean) {
- _customizing.value = value
+ updateCustomizerFlows(if (value) CustomizerState.Showing else CustomizerState.Hidden)
}
override suspend fun applyBottomNavBarPadding(padding: Int) {
@@ -66,10 +73,18 @@ class FakeQSSceneAdapter(
}
override fun requestCloseCustomizer() {
- _customizing.value = false
+ updateCustomizerFlows(CustomizerState.Hidden)
}
override fun setBrightnessMirrorController(mirrorController: MirrorController?) {
brightnessMirrorController = mirrorController
}
+
+ private fun updateCustomizerFlows(customizerState: CustomizerState) {
+ _customizerState.value = customizerState
+ _customizing.value = customizerState.isCustomizing
+ _customizerShowing.value = customizerState.isShowing
+ _animationDuration.value =
+ (customizerState as? CustomizerState.Animating)?.animationDuration?.toInt() ?: 0
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
index 59a01cbedc5c..957a60f83134 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
@@ -42,6 +42,10 @@ class FakeSceneDataSource(
}
}
+ override fun snapToScene(toScene: SceneKey) {
+ changeScene(toScene)
+ }
+
/**
* Pauses scene changes.
*
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index 59a5bb5360e3..38ede44efef6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -59,11 +59,23 @@ class ShadeTestUtil constructor(val delegate: ShadeTestUtilDelegate) {
delegate.setLockscreenShadeExpansion(lockscreenShadeExpansion)
}
- /** Sets whether the user is moving the shade with touch input. */
+ /** Sets whether the user is moving the shade with touch input on Lockscreen. */
fun setLockscreenShadeTracking(lockscreenShadeTracking: Boolean) {
delegate.assertFlagValid()
delegate.setLockscreenShadeTracking(lockscreenShadeTracking)
}
+
+ /** Sets whether the user is moving the shade with touch input. */
+ fun setTracking(tracking: Boolean) {
+ delegate.assertFlagValid()
+ delegate.setTracking(tracking)
+ }
+
+ /** Sets the shade to half collapsed with no touch input. */
+ fun programmaticCollapseShade() {
+ delegate.assertFlagValid()
+ delegate.programmaticCollapseShade()
+ }
}
/** Sets up shade state for tests for a specific value of the scene container flag. */
@@ -80,11 +92,17 @@ interface ShadeTestUtilDelegate {
/** Sets whether the user is moving the shade with touch input. */
fun setLockscreenShadeTracking(lockscreenShadeTracking: Boolean)
+ /** Sets whether the user is moving the shade with touch input. */
+ fun setTracking(tracking: Boolean)
+
/** Sets shade expansion to a value between 0-1. */
fun setShadeExpansion(shadeExpansion: Float)
/** Sets QS expansion to a value between 0-1. */
fun setQsExpansion(qsExpansion: Float)
+
+ /** Sets the shade to half collapsed with no touch input. */
+ fun programmaticCollapseShade()
}
/** Sets up shade state for tests when the scene container flag is disabled. */
@@ -104,6 +122,10 @@ class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: Fak
shadeRepository.setLegacyLockscreenShadeTracking(lockscreenShadeTracking)
}
+ override fun setTracking(tracking: Boolean) {
+ shadeRepository.setLegacyShadeTracking(tracking)
+ }
+
override fun assertFlagValid() {
Assert.assertFalse(SceneContainerFlag.isEnabled)
}
@@ -119,6 +141,11 @@ class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: Fak
shadeRepository.setQsExpansion(qsExpansion)
testScope.runCurrent()
}
+
+ override fun programmaticCollapseShade() {
+ shadeRepository.setLegacyShadeExpansion(.5f)
+ testScope.runCurrent()
+ }
}
/** Sets up shade state for tests when the scene container flag is enabled. */
@@ -127,14 +154,16 @@ class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: Scen
val isUserInputOngoing = MutableStateFlow(true)
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
- if (shadeExpansion == 0f) {
- setTransitionProgress(Scenes.Lockscreen, Scenes.QuickSettings, qsExpansion)
- } else if (qsExpansion == 0f) {
- setTransitionProgress(Scenes.Lockscreen, Scenes.Shade, shadeExpansion)
- } else if (shadeExpansion == 1f) {
+ if (shadeExpansion == 1f) {
setIdleScene(Scenes.Shade)
} else if (qsExpansion == 1f) {
setIdleScene(Scenes.QuickSettings)
+ } else if (shadeExpansion == 0f && qsExpansion == 0f) {
+ setIdleScene(Scenes.Lockscreen)
+ } else if (shadeExpansion == 0f) {
+ setTransitionProgress(Scenes.Lockscreen, Scenes.QuickSettings, qsExpansion)
+ } else if (qsExpansion == 0f) {
+ setTransitionProgress(Scenes.Lockscreen, Scenes.Shade, shadeExpansion)
} else {
setTransitionProgress(Scenes.Shade, Scenes.QuickSettings, qsExpansion)
}
@@ -150,6 +179,10 @@ class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: Scen
setShadeAndQsExpansion(0f, qsExpansion)
}
+ override fun programmaticCollapseShade() {
+ setTransitionProgress(Scenes.Shade, Scenes.Lockscreen, .5f, false)
+ }
+
override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
if (lockscreenShadeExpansion == 0f) {
setIdleScene(Scenes.Lockscreen)
@@ -161,7 +194,11 @@ class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: Scen
}
override fun setLockscreenShadeTracking(lockscreenShadeTracking: Boolean) {
- isUserInputOngoing.value = lockscreenShadeTracking
+ setTracking(lockscreenShadeTracking)
+ }
+
+ override fun setTracking(tracking: Boolean) {
+ isUserInputOngoing.value = tracking
}
private fun setIdleScene(scene: SceneKey) {
@@ -172,7 +209,12 @@ class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: Scen
testScope.runCurrent()
}
- private fun setTransitionProgress(from: SceneKey, to: SceneKey, progress: Float) {
+ private fun setTransitionProgress(
+ from: SceneKey,
+ to: SceneKey,
+ progress: Float,
+ isInitiatedByUserInput: Boolean = true
+ ) {
sceneInteractor.changeScene(from, "test")
val transitionState =
MutableStateFlow<ObservableTransitionState>(
@@ -181,7 +223,7 @@ class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: Scen
toScene = to,
currentScene = flowOf(to),
progress = MutableStateFlow(progress),
- isInitiatedByUserInput = true,
+ isInitiatedByUserInput = isInitiatedByUserInput,
isUserInputOngoing = isUserInputOngoing,
)
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
new file mode 100644
index 000000000000..872eba06961e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.shade.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+
+val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by
+ Kosmos.Fixture {
+ NotificationsShadeSceneViewModel(
+ applicationScope = applicationCoroutineScope,
+ overlayShadeViewModel = overlayShadeViewModel,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
new file mode 100644
index 000000000000..8c5ff1d5d216
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.shade.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel
+
+val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by
+ Kosmos.Fixture {
+ QuickSettingsShadeSceneViewModel(
+ applicationScope = applicationCoroutineScope,
+ overlayShadeViewModel = overlayShadeViewModel,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
index d798b3b13341..0364e0595999 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
@@ -16,8 +16,8 @@ package com.android.systemui.utils.leaks;
import android.os.Bundle;
import android.testing.LeakCheck;
-import android.view.View;
+import com.android.systemui.animation.Expandable;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -61,7 +61,7 @@ public class FakeBatteryController extends BaseLeakChecker<BatteryStateChangeCal
* Note: this method ignores the View argument
*/
@Override
- public void setPowerSaveMode(boolean powerSave, View view) {
+ public void setPowerSaveMode(boolean powerSave, Expandable expandable) {
setPowerSaveMode(powerSave);
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
index 5db17243c4e3..546a797482a5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
@@ -19,8 +19,10 @@ package com.android.systemui.volume
import android.content.packageManager
import android.content.pm.ApplicationInfo
import android.media.AudioAttributes
+import android.media.VolumeProvider
import android.media.session.MediaController
import android.media.session.MediaSession
+import android.media.session.PlaybackState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -28,6 +30,18 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
private const val LOCAL_PACKAGE = "local.test.pkg"
+var Kosmos.localPlaybackInfo by
+ Kosmos.Fixture {
+ MediaController.PlaybackInfo(
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+ VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
+ 10,
+ 3,
+ AudioAttributes.Builder().build(),
+ "",
+ )
+ }
+var Kosmos.localPlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() }
var Kosmos.localMediaController: MediaController by
Kosmos.Fixture {
val appInfo: ApplicationInfo = mock {
@@ -39,22 +53,25 @@ var Kosmos.localMediaController: MediaController by
val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
mock {
whenever(packageName).thenReturn(LOCAL_PACKAGE)
- whenever(playbackInfo)
- .thenReturn(
- MediaController.PlaybackInfo(
- MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
- 0,
- 0,
- 0,
- AudioAttributes.Builder().build(),
- "",
- )
- )
+ whenever(playbackInfo).thenReturn(localPlaybackInfo)
+ whenever(playbackState).thenReturn(localPlaybackStateBuilder.build())
whenever(sessionToken).thenReturn(localSessionToken)
}
}
private const val REMOTE_PACKAGE = "remote.test.pkg"
+var Kosmos.remotePlaybackInfo by
+ Kosmos.Fixture {
+ MediaController.PlaybackInfo(
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+ VolumeProvider.VOLUME_CONTROL_ABSOLUTE,
+ 10,
+ 7,
+ AudioAttributes.Builder().build(),
+ "",
+ )
+ }
+var Kosmos.remotePlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() }
var Kosmos.remoteMediaController: MediaController by
Kosmos.Fixture {
val appInfo: ApplicationInfo = mock {
@@ -66,17 +83,8 @@ var Kosmos.remoteMediaController: MediaController by
val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {})
mock {
whenever(packageName).thenReturn(REMOTE_PACKAGE)
- whenever(playbackInfo)
- .thenReturn(
- MediaController.PlaybackInfo(
- MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
- 0,
- 0,
- 0,
- AudioAttributes.Builder().build(),
- "",
- )
- )
+ whenever(playbackInfo).thenReturn(remotePlaybackInfo)
+ whenever(playbackState).thenReturn(remotePlaybackStateBuilder.build())
whenever(sessionToken).thenReturn(remoteSessionToken)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
index fa3a19bae655..d74355894581 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt
@@ -30,13 +30,12 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.volume.data.repository.FakeLocalMediaRepository
import com.android.systemui.volume.data.repository.FakeMediaControllerRepository
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory
-import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() }
-val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by
+val Kosmos.localMediaRepositoryFactory by
Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
val Kosmos.mediaOutputActionsInteractor by
@@ -55,6 +54,7 @@ val Kosmos.mediaOutputInteractor by
testScope.backgroundScope,
testScope.testScheduler,
mediaControllerRepository,
+ Handler(TestableLooper.get(testCase).looper),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
index 1b3480c423e4..9c902cf57fde 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
@@ -18,9 +18,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.data.repository
import com.android.settingslib.volume.data.repository.LocalMediaRepository
-class FakeLocalMediaRepositoryFactory(
- val provider: (packageName: String?) -> LocalMediaRepository
-) : LocalMediaRepositoryFactory {
+class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMediaRepository) :
+ LocalMediaRepositoryFactory {
- override fun create(packageName: String?): LocalMediaRepository = provider(packageName)
+ private val repositories = mutableMapOf<String, LocalMediaRepository>()
+
+ fun setLocalMediaRepository(packageName: String, localMediaRepository: LocalMediaRepository) {
+ repositories[packageName] = localMediaRepository
+ }
+
+ override fun create(packageName: String?): LocalMediaRepository =
+ repositories[packageName] ?: defaultProvider()
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index e06f40070a0a..bc608c5ac0c0 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -16,6 +16,38 @@ filegroup {
visibility: ["//visibility:public"],
}
+filegroup {
+ name: "ravenwood-services-policies",
+ srcs: [
+ "texts/ravenwood-services-policies.txt",
+ ],
+ visibility: ["//visibility:public"],
+}
+
+filegroup {
+ name: "ravenwood-framework-policies",
+ srcs: [
+ "texts/ravenwood-framework-policies.txt",
+ ],
+ visibility: ["//visibility:public"],
+}
+
+filegroup {
+ name: "ravenwood-standard-options",
+ srcs: [
+ "texts/ravenwood-standard-options.txt",
+ ],
+ visibility: ["//visibility:public"],
+}
+
+filegroup {
+ name: "ravenwood-annotation-allowed-classes",
+ srcs: [
+ "texts/ravenwood-annotation-allowed-classes.txt",
+ ],
+ visibility: ["//visibility:public"],
+}
+
java_library {
name: "ravenwood-annotations-lib",
srcs: [":ravenwood-annotations"],
diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh
index beacde282d49..cf58bd2f3717 100755
--- a/ravenwood/scripts/ravenwood-stats-collector.sh
+++ b/ravenwood/scripts/ravenwood-stats-collector.sh
@@ -24,6 +24,8 @@ apis=/tmp/ravenwood-apis-all.csv
# Where the input files are.
path=$ANDROID_BUILD_TOP/out/host/linux-x86/testcases/ravenwood-stats-checker/x86_64/
+timestamp="$(date --iso-8601=seconds)"
+
m() {
${ANDROID_BUILD_TOP}/build/soong/soong_ui.bash --make-mode "$@"
}
@@ -39,15 +41,15 @@ dump() {
local jar=$1
local file=$2
- # Use sed to remove the header + prepend the jar filename.
- sed -e '1d' -e "s/^/$jar,/" $file
+ # Remove the header row, and prepend the columns.
+ sed -e '1d' -e "s/^/$jar,$timestamp,/" $file
}
collect_stats() {
local out="$1"
{
# Copy the header, with the first column appended.
- echo -n "Jar,"
+ echo -n "Jar,Generated Date,"
head -n 1 hoststubgen_framework-minus-apex_stats.csv
dump "framework-minus-apex" hoststubgen_framework-minus-apex_stats.csv
@@ -61,7 +63,7 @@ collect_apis() {
local out="$1"
{
# Copy the header, with the first column appended.
- echo -n "Jar,"
+ echo -n "Jar,Generated Date,"
head -n 1 hoststubgen_framework-minus-apex_apis.csv
dump "framework-minus-apex" hoststubgen_framework-minus-apex_apis.csv
diff --git a/ravenwood/texts/framework-minus-apex-ravenwood-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index 371c3acab144..371c3acab144 100644
--- a/ravenwood/texts/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
diff --git a/ravenwood/texts/services.core-ravenwood-policies.txt b/ravenwood/texts/ravenwood-services-policies.txt
index d8d563e05435..d8d563e05435 100644
--- a/ravenwood/texts/services.core-ravenwood-policies.txt
+++ b/ravenwood/texts/ravenwood-services-policies.txt
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index e66fe1b9452c..1ba47e40b080 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -128,16 +128,6 @@ flag {
}
flag {
- name: "manager_avoid_receiver_timeout"
- namespace: "accessibility"
- description: "Avoid broadcast receiver timeout by offloading potentially slow operations to the background thread."
- bug: "333890389"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "pinch_zoom_zero_min_span"
namespace: "accessibility"
description: "Whether to set min span of ScaleGestureDetector to zero."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 2bece6cac95a..726a01c59a2e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -994,10 +994,43 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
"context=" + context + ";intent=" + intent);
}
- if (com.android.server.accessibility.Flags.managerAvoidReceiverTimeout()) {
- BackgroundThread.getHandler().post(() -> processBroadcast(intent));
- } else {
- processBroadcast(intent);
+ String action = intent.getAction();
+ if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
+ unlockUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
+ removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
+ } else if (Intent.ACTION_SETTING_RESTORED.equals(action)) {
+ final String which = intent.getStringExtra(Intent.EXTRA_SETTING_NAME);
+ if (Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES.equals(which)) {
+ synchronized (mLock) {
+ restoreEnabledAccessibilityServicesLocked(
+ intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
+ intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE),
+ intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT,
+ 0));
+ }
+ } else if (ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED.equals(which)) {
+ synchronized (mLock) {
+ restoreLegacyDisplayMagnificationNavBarIfNeededLocked(
+ intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE),
+ intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT,
+ 0));
+ }
+ } else if (Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS.equals(which)) {
+ synchronized (mLock) {
+ restoreAccessibilityButtonTargetsLocked(
+ intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
+ intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
+ }
+ } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
+ if (!android.view.accessibility.Flags.a11yQsShortcut()) {
+ return;
+ }
+ restoreAccessibilityQsTargets(
+ intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
+ }
}
}
}, UserHandle.ALL, intentFilter, null, null);
@@ -2000,19 +2033,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mA11yWindowManager.onTouchInteractionEnd();
}
- private void processBroadcast(Intent intent) {
- String action = intent.getAction();
- if (Intent.ACTION_USER_SWITCHED.equals(action)) {
- switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
- } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
- unlockUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
- } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
- removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
- } else if (Intent.ACTION_SETTING_RESTORED.equals(action)) {
- restoreSetting(intent);
- }
- }
-
@VisibleForTesting
void switchUser(int userId) {
mMagnificationController.updateUserIdIfNeeded(userId);
@@ -2105,38 +2125,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
getMagnificationController().onUserRemoved(userId);
}
- private void restoreSetting(Intent intent) {
- final String which = intent.getStringExtra(Intent.EXTRA_SETTING_NAME);
- if (Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES.equals(which)) {
- synchronized (mLock) {
- restoreEnabledAccessibilityServicesLocked(
- intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
- intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE),
- intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT,
- 0));
- }
- } else if (ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED.equals(which)) {
- synchronized (mLock) {
- restoreLegacyDisplayMagnificationNavBarIfNeededLocked(
- intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE),
- intent.getIntExtra(Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT,
- 0));
- }
- } else if (Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS.equals(which)) {
- synchronized (mLock) {
- restoreAccessibilityButtonTargetsLocked(
- intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
- intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
- }
- } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
- if (!android.view.accessibility.Flags.a11yQsShortcut()) {
- return;
- }
- restoreAccessibilityQsTargets(
- intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
- }
- }
-
// Called only during settings restore; currently supports only the owner user
// TODO: http://b/22388012
void restoreEnabledAccessibilityServicesLocked(String oldSetting, String newSetting,
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 970129231f03..763879e5379a 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -1625,13 +1625,13 @@ public final class AutofillManagerService
final class AutoFillManagerServiceStub extends IAutoFillManager.Stub {
@Override
public void addClient(IAutoFillManagerClient client, ComponentName componentName,
- int userId, IResultReceiver receiver) {
+ int userId, IResultReceiver receiver, boolean credmanRequested) {
int flags = 0;
try {
synchronized (mLock) {
final int enabledFlags =
getServiceForUserWithLocalBinderIdentityLocked(userId)
- .addClientLocked(client, componentName);
+ .addClientLocked(client, componentName, credmanRequested);
if (enabledFlags != 0) {
flags |= enabledFlags;
}
@@ -1644,7 +1644,7 @@ public final class AutofillManagerService
}
} catch (Exception ex) {
// Don't do anything, send back default flags
- Log.wtf(TAG, "addClient(): failed " + ex.toString());
+ Log.wtf(TAG, "addClient(): failed " + ex.toString(), ex);
} finally {
send(receiver, flags);
}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 68222298b94c..92acce24e64e 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -33,6 +33,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -96,6 +97,7 @@ import com.android.server.wm.ActivityTaskManagerInternal;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Random;
/**
* Bridge between the {@code system_server}'s {@link AutofillManagerService} and the
@@ -293,19 +295,31 @@ final class AutofillManagerServiceImpl
* @return {@code 0} if disabled, {@code FLAG_ADD_CLIENT_ENABLED} if enabled (it might be
* OR'ed with {@code FLAG_AUGMENTED_AUTOFILL_REQUEST}).
*/
- @GuardedBy("mLock")
- int addClientLocked(IAutoFillManagerClient client, ComponentName componentName) {
- if (mClients == null) {
- mClients = new RemoteCallbackList<>();
- }
- mClients.register(client);
+ int addClientLocked(IAutoFillManagerClient client, ComponentName componentName,
+ boolean credmanRequested) {
+ synchronized (mLock) {
+ ComponentName credComponentName = getCredentialAutofillService(getContext());
+
+ if (!credmanRequested
+ && Objects.equals(credComponentName,
+ mInfo == null ? null : mInfo.getServiceInfo().getComponentName())) {
+ // If the service component name corresponds to cred component name, then it means
+ // no autofill provider is selected by the user. Cred Autofill Service should only
+ // be active if there is a credman request.
+ return 0;
+ }
+ if (mClients == null) {
+ mClients = new RemoteCallbackList<>();
+ }
+ mClients.register(client);
- if (isEnabledLocked()) return FLAG_ADD_CLIENT_ENABLED;
+ if (isEnabledLocked()) return FLAG_ADD_CLIENT_ENABLED;
- // Check if it's enabled for augmented autofill
- if (componentName != null && isAugmentedAutofillServiceAvailableLocked()
- && isWhitelistedForAugmentedAutofillLocked(componentName)) {
- return FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY;
+ // Check if it's enabled for augmented autofill
+ if (componentName != null && isAugmentedAutofillServiceAvailableLocked()
+ && isWhitelistedForAugmentedAutofillLocked(componentName)) {
+ return FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY;
+ }
}
// No flags / disabled
@@ -1486,6 +1500,22 @@ final class AutofillManagerServiceImpl
return true;
}
+ @Nullable
+ private ComponentName getCredentialAutofillService(Context context) {
+ ComponentName componentName = null;
+ String credentialManagerAutofillCompName = context.getResources().getString(
+ R.string.config_defaultCredentialManagerAutofillService);
+ if (credentialManagerAutofillCompName != null
+ && !credentialManagerAutofillCompName.isEmpty()) {
+ componentName = ComponentName.unflattenFromString(
+ credentialManagerAutofillCompName);
+ }
+ if (componentName == null) {
+ Slog.w(TAG, "Invalid CredentialAutofillService");
+ }
+ return componentName;
+ }
+
@GuardedBy("mLock")
private int getAugmentedAutofillServiceUidLocked() {
if (mRemoteAugmentedAutofillServiceInfo == null) {
diff --git a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
index 28e8e3031a14..4f95893442a5 100644
--- a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
@@ -34,6 +34,7 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_RE
import static com.android.server.autofill.Helper.sVerbose;
import android.annotation.IntDef;
+import android.os.SystemClock;
import android.util.Slog;
import com.android.internal.util.FrameworkStatsLog;
@@ -45,7 +46,7 @@ import java.util.Optional;
/**
* Helper class to log Autofill Save event stats.
*/
-public final class SaveEventLogger {
+public class SaveEventLogger {
private static final String TAG = "SaveEventLogger";
/**
@@ -112,19 +113,21 @@ public final class SaveEventLogger {
public static final int NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG =
AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG;
+ public static final long UNINITIATED_TIMESTAMP = Long.MIN_VALUE;
+
private final int mSessionId;
private Optional<SaveEventInternal> mEventInternal;
+ private long mSessionStartTimestamp;
- private SaveEventLogger(int sessionId) {
- mSessionId = sessionId;
- mEventInternal = Optional.of(new SaveEventInternal());
+ private SaveEventLogger(int sessionId, long sessionStartTimestamp) {
+ mSessionId = sessionId;
+ mEventInternal = Optional.of(new SaveEventInternal());
+ mSessionStartTimestamp = sessionStartTimestamp;
}
- /**
- * A factory constructor to create FillRequestEventLogger.
- */
- public static SaveEventLogger forSessionId(int sessionId) {
- return new SaveEventLogger(sessionId);
+ /** A factory constructor to create FillRequestEventLogger. */
+ public static SaveEventLogger forSessionId(int sessionId, long sessionStartTimestamp) {
+ return new SaveEventLogger(sessionId, sessionStartTimestamp);
}
/**
@@ -224,6 +227,15 @@ public final class SaveEventLogger {
});
}
+ /* Returns timestamp (relative to mSessionStartTimestamp) or UNINITIATED_TIMESTAMP if mSessionStartTimestamp is not set */
+ private long getCurrentTimestamp() {
+ long timestamp = UNINITIATED_TIMESTAMP;
+ if (mSessionStartTimestamp != UNINITIATED_TIMESTAMP) {
+ timestamp = SystemClock.elapsedRealtime() - mSessionStartTimestamp;
+ }
+ return timestamp;
+ }
+
/**
* Set latency_save_ui_display_millis as long as mEventInternal presents.
*/
@@ -233,6 +245,11 @@ public final class SaveEventLogger {
});
}
+ /** Set latency_save_ui_display_millis as long as mEventInternal presents. */
+ public void maybeSetLatencySaveUiDisplayMillis() {
+ maybeSetLatencySaveUiDisplayMillis(getCurrentTimestamp());
+ }
+
/**
* Set latency_save_request_millis as long as mEventInternal presents.
*/
@@ -242,6 +259,11 @@ public final class SaveEventLogger {
});
}
+ /** Set latency_save_request_millis as long as mEventInternal presents. */
+ public void maybeSetLatencySaveRequestMillis() {
+ maybeSetLatencySaveRequestMillis(getCurrentTimestamp());
+ }
+
/**
* Set latency_save_finish_millis as long as mEventInternal presents.
*/
@@ -251,6 +273,11 @@ public final class SaveEventLogger {
});
}
+ /** Set latency_save_finish_millis as long as mEventInternal presents. */
+ public void maybeSetLatencySaveFinishMillis() {
+ maybeSetLatencySaveFinishMillis(getCurrentTimestamp());
+ }
+
/**
* Set is_framework_created_save_info as long as mEventInternal presents.
*/
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index cd1ef882868a..519236dc15f2 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1493,10 +1493,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mCredentialAutofillService = getCredentialAutofillService(context);
- ComponentName primaryServiceComponentName, secondaryServiceComponentName;
+ ComponentName primaryServiceComponentName, secondaryServiceComponentName = null;
if (isPrimaryCredential) {
primaryServiceComponentName = mCredentialAutofillService;
- secondaryServiceComponentName = serviceComponentName;
+ if (serviceComponentName != null
+ && !serviceComponentName.equals(mCredentialAutofillService)) {
+ // if service component name is credential autofill service, no need to initialize
+ // secondary provider. This happens if the user sets non-autofill provider as
+ // password provider.
+ secondaryServiceComponentName = serviceComponentName;
+ }
} else {
primaryServiceComponentName = serviceComponentName;
secondaryServiceComponentName = mCredentialAutofillService;
@@ -1531,7 +1537,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mFillResponseEventLogger = FillResponseEventLogger.forSessionId(sessionId);
mSessionCommittedEventLogger = SessionCommittedEventLogger.forSessionId(sessionId);
mSessionCommittedEventLogger.maybeSetComponentPackageUid(uid);
- mSaveEventLogger = SaveEventLogger.forSessionId(sessionId);
+ mSaveEventLogger = SaveEventLogger.forSessionId(sessionId, mLatencyBaseTime);
mIsPrimaryCredential = isPrimaryCredential;
mIgnoreViewStateResetToEmpty = AutofillFeatureFlags.shouldIgnoreViewStateResetToEmpty();
@@ -3931,13 +3937,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
return new SaveResult(/* logSaveShown= */ false, /* removeSession= */ true,
Event.NO_SAVE_UI_REASON_NONE);
}
- final long saveUiDisplayStartTimestamp = SystemClock.elapsedRealtime();
getUiForShowing().showSaveUi(serviceLabel, serviceIcon,
mService.getServicePackageName(), saveInfo, this,
mComponentName, this, mContext, mPendingSaveUi, isUpdate, mCompatMode,
response.getShowSaveDialogIcon(), mSaveEventLogger);
- mSaveEventLogger.maybeSetLatencySaveUiDisplayMillis(
- SystemClock.elapsedRealtime()- saveUiDisplayStartTimestamp);
if (client != null) {
try {
client.setSaveUiState(id, true);
diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
index 602855da7ddf..3b9c54f79e61 100644
--- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
@@ -413,6 +413,8 @@ public final class AutoFillUI {
callback.startIntentSender(intentSender, intent);
}
}, mUiModeMgr.isNightMode(), isUpdate, compatMode, showServiceIcon);
+
+ mSaveEventLogger.maybeSetLatencySaveUiDisplayMillis();
});
}
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index 64bca33569cc..04edb5756599 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -27,6 +27,7 @@ import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDI
import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START;
import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP;
import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_NOTIFICATION_APP_PROTECTION_SESSION;
+import static com.android.server.wm.WindowManagerInternal.OnWindowRemovedListener;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -210,6 +211,12 @@ public final class SensitiveContentProtectionManagerService extends SystemServic
}
};
+ private final OnWindowRemovedListener mOnWindowRemovedListener = token -> {
+ synchronized (mSensitiveContentProtectionLock) {
+ mPackagesShowingSensitiveContent.removeIf(pkgInfo -> pkgInfo.getWindowToken() == token);
+ }
+ };
+
public SensitiveContentProtectionManagerService(@NonNull Context context) {
super(context);
if (sensitiveNotificationAppProtection()) {
@@ -265,6 +272,10 @@ public final class SensitiveContentProtectionManagerService extends SystemServic
// Intra-process call, should never happen.
}
}
+
+ if (sensitiveContentAppProtection()) {
+ mWindowManager.registerOnWindowRemovedListener(mOnWindowRemovedListener);
+ }
}
/** Cleanup any callbacks and listeners */
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c372b3f55b8e..4ca9e3380a92 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2415,7 +2415,9 @@ public final class ActiveServices {
!= ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
// Calling startForeground on a FGS type which has a time limit will only be
// allowed if the app is in a state where it can normally start another FGS
- // and it hasn't hit the time limit for that type in the past 24hrs.
+ // and it hasn't hit its time limit in the past 24hrs, or it has been in the
+ // foreground after it hit its time limit, or it is currently in the
+ // TOP (or better) proc state.
// See if the app could start an FGS or not.
r.clearFgsAllowStart();
@@ -2441,11 +2443,13 @@ public final class ActiveServices {
SystemClock.elapsedRealtime() - (24 * 60 * 60 * 1000));
final long lastTimeOutAt = fgsTypeInfo.getTimeLimitExceededAt();
if (fgsTypeInfo.getFirstFgsStartRealtime() < before24Hr
+ || r.app.mState.getCurProcState() <= PROCESS_STATE_TOP
|| (lastTimeOutAt != Long.MIN_VALUE
&& r.app.mState.getLastTopTime() > lastTimeOutAt)) {
// Reset the time limit info for this fgs type if it has been
- // more than 24hrs since the first fgs start or if the app was
- // in the TOP state after time limit was exhausted.
+ // more than 24hrs since the first fgs start or if the app is
+ // currently in the TOP state or was in the TOP state after
+ // the time limit was exhausted previously.
fgsTypeInfo.reset();
} else if (lastTimeOutAt > 0) {
// Time limit was exhausted within the past 24 hours and the app
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1b59c1829d4c..1b3b19808dad 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4342,6 +4342,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mServices.bringDownDisabledPackageServicesLocked(
packageName, null, userId, false, true, true);
+ mServices.onUidRemovedLocked(uid);
if (mBooted) {
mAtmInternal.resumeTopActivities(true);
@@ -4372,9 +4373,10 @@ public class ActivityManagerService extends IActivityManager.Stub
Slog.w(TAG, "Can't force stop all processes of all users, that is insane!");
}
+ final int uid = getPackageManagerInternal().getPackageUid(packageName,
+ MATCH_DEBUG_TRIAGED_MISSING | MATCH_ANY_USER, UserHandle.USER_SYSTEM);
if (appId < 0 && packageName != null) {
- appId = UserHandle.getAppId(getPackageManagerInternal().getPackageUid(packageName,
- MATCH_DEBUG_TRIAGED_MISSING | MATCH_ANY_USER, UserHandle.USER_SYSTEM));
+ appId = UserHandle.getAppId(uid);
}
boolean didSomething;
@@ -4418,6 +4420,7 @@ public class ActivityManagerService extends IActivityManager.Stub
}
didSomething = true;
}
+ mServices.onUidRemovedLocked(uid);
if (packageName == null) {
// Remove all sticky broadcasts from this user.
@@ -19950,6 +19953,20 @@ public class ActivityManagerService extends IActivityManager.Stub
return !ActivityManagerService.this.mThemeOverlayReadyUsers.contains(userId);
}
}
+
+ @Override
+ public void addStartInfoTimestamp(int key, long timestampNs, int uid, int pid,
+ int userId) {
+ // For the simplification, we don't support USER_ALL nor USER_CURRENT here.
+ if (userId == UserHandle.USER_ALL || userId == UserHandle.USER_CURRENT) {
+ throw new IllegalArgumentException("Unsupported userId");
+ }
+
+ mUserController.handleIncomingUser(pid, uid, userId, true,
+ ALLOW_NON_FULL, "addStartInfoTimestampSystem", null);
+
+ addStartInfoTimestampInternal(key, timestampNs, userId, uid);
+ }
}
long inputDispatchingTimedOut(int pid, final boolean aboveSystem, TimeoutRecord timeoutRecord) {
@@ -20266,7 +20283,7 @@ public class ActivityManagerService extends IActivityManager.Stub
final int userId = UserHandle.getCallingUserId();
final long callingId = Binder.clearCallingIdentity();
try {
- if (uid == -1) {
+ if (uid == INVALID_UID) {
uid = mPackageManagerInt.getPackageUid(packageName, 0, userId);
}
mAppRestrictionController.noteAppRestrictionEnabled(packageName, uid, restrictionType,
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 5af9424a025c..3cea01464c10 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -4552,10 +4552,9 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" 1: crop_windows");
pw.println(" 2: resizeable");
pw.println(" 3: resizeable_and_pipable");
- pw.println(" resize <TASK_ID> <LEFT,TOP,RIGHT,BOTTOM>");
- pw.println(" Makes sure <TASK_ID> is in a stack with the specified bounds.");
- pw.println(" Forces the task to be resizeable and creates a stack if no existing stack");
- pw.println(" has the specified bounds.");
+ pw.println(" resize <TASK_ID> <LEFT> <TOP> <RIGHT> <BOTTOM>");
+ pw.println(" The task is resized only if it is in multi-window windowing");
+ pw.println(" mode or freeform windowing mode.");
pw.println(" update-appinfo <USER_ID> <PACKAGE_NAME> [<PACKAGE_NAME>...]");
pw.println(" Update the ApplicationInfo objects of the listed packages for <USER_ID>");
pw.println(" without restarting any processes.");
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index c5cad14ee3d7..f5f1928c7c72 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -2387,8 +2387,8 @@ public final class AppRestrictionController {
// Limit the length of the free-form subReason string
if (subReason != null && subReason.length() > RESTRICTION_SUBREASON_MAX_LENGTH) {
+ Slog.e(TAG, "subReason is too long, truncating " + subReason);
subReason = subReason.substring(0, RESTRICTION_SUBREASON_MAX_LENGTH);
- Slog.e(TAG, "Subreason is too long, truncating: " + subReason);
}
// Log the restriction reason
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9b83ede09da4..95206212c99d 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -128,6 +128,7 @@ public class SettingsToPropertiesMapper {
"aoc",
"app_widgets",
"arc_next",
+ "art_mainline",
"avic",
"biometrics",
"biometrics_framework",
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
index ce410796137b..e066c23d4d59 100644
--- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java
+++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
@@ -572,12 +572,8 @@ public final class AppHibernationService extends SystemService {
packageName, uid, ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED,
true, ActivityManager.RESTRICTION_REASON_DORMANT, null,
/* TODO: fetch actual timeout - 90 days */ 90 * 24 * 60 * 60_000L);
- } else {
- mIActivityManager.noteAppRestrictionEnabled(
- packageName, uid, ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED,
- false, ActivityManager.RESTRICTION_REASON_USAGE, null,
- 0L);
}
+ // No need to log the unhibernate case as an unstop is logged already in ActivityMS
} catch (RemoteException e) {
Slog.e(TAG, "Couldn't set restriction state change");
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index da528a2591a1..475334c69313 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -274,8 +274,11 @@ public class AudioDeviceBroker {
}
/*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
- mBluetoothA2dpEnabled.set(on);
- sendLMsgNoDelay(MSG_L_SET_FORCE_BT_A2DP_USE, SENDMSG_REPLACE, source);
+ boolean wasOn = mBluetoothA2dpEnabled.getAndSet(on);
+ // do not mute music if we do not anticipate a change in A2DP ON state
+ sendLMsgNoDelay(wasOn == on
+ ? MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE : MSG_L_SET_FORCE_BT_A2DP_USE,
+ SENDMSG_REPLACE, source);
}
/**
@@ -1803,6 +1806,7 @@ public class AudioDeviceBroker {
onSetForceUse(msg.arg1, msg.arg2, false, (String) msg.obj);
break;
case MSG_L_SET_FORCE_BT_A2DP_USE:
+ case MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE:
int forcedUsage = mBluetoothA2dpEnabled.get()
? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
onSetForceUse(AudioSystem.FOR_MEDIA, forcedUsage, true, (String) msg.obj);
@@ -2139,8 +2143,7 @@ public class AudioDeviceBroker {
private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
private static final int MSG_L_SYNCHRONIZE_ADI_DEVICES_IN_INVENTORY = 58;
private static final int MSG_IL_UPDATED_ADI_DEVICE_STATE = 59;
-
-
+ private static final int MSG_L_SET_FORCE_BT_A2DP_USE_NO_MUTE = 60;
private static boolean isMessageHandledUnderWakelock(int msgId) {
switch(msgId) {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index a46975fb3567..11ef5776a47a 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -285,7 +285,8 @@ public class BrightnessClamperController {
List<BrightnessStateModifier> modifiers = new ArrayList<>();
modifiers.add(new DisplayDimModifier(context));
modifiers.add(new BrightnessLowPowerModeModifier());
- if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null) {
+ if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null
+ && displayDeviceConfig.isEvenDimmerAvailable()) {
modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
displayDeviceConfig));
}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
index a3dfe229eb59..7ba4a4d9c4dd 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -87,9 +87,7 @@ public class BrightnessLowLuxModifier extends BrightnessModifier {
mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
/* def= */ MIN_NITS_DEFAULT, userId);
- boolean isActive = Settings.Secure.getFloatForUser(mContentResolver,
- Settings.Secure.EVEN_DIMMER_ACTIVATED,
- /* def= */ 0, userId) == 1.0f && mAutoBrightnessEnabled;
+ boolean isActive = isSettingEnabled() && mAutoBrightnessEnabled;
float luxBasedNitsLowerBound = mDisplayDeviceConfig.getMinNitsFromLux(mAmbientLux);
@@ -202,6 +200,17 @@ public class BrightnessLowLuxModifier extends BrightnessModifier {
pw.println(" mMinNitsAllowed=" + mMinNitsAllowed);
}
+ /**
+ * Defaults to true, on devices where setting is unset.
+ *
+ * @return if setting indicates feature is enabled
+ */
+ private boolean isSettingEnabled() {
+ return Settings.Secure.getFloatForUser(mContentResolver,
+ Settings.Secure.EVEN_DIMMER_ACTIVATED,
+ /* def= */ 1.0f, UserHandle.USER_CURRENT) == 1.0f;
+ }
+
private float getBrightnessFromNits(float nits) {
return mDisplayDeviceConfig.getBrightnessFromBacklight(
mDisplayDeviceConfig.getBacklightFromNits(nits));
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 61514abec102..d2d0279e768e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -304,6 +304,10 @@ public class HdmiControlService extends SystemService {
// Make sure HdmiCecConfig is instantiated and the XMLs are read.
private HdmiCecConfig mHdmiCecConfig;
+ // Timeout value for start ARC action after an established eARC connection was terminated,
+ // e.g. because eARC was disabled in Settings.
+ private static final int EARC_TRIGGER_START_ARC_ACTION_DELAY = 500;
+
/**
* Interface to report send result.
*/
@@ -5041,7 +5045,12 @@ public class HdmiControlService extends SystemService {
// AudioService here that the eARC connection is terminated.
HdmiLogger.debug("eARC state change [new: HDMI_EARC_STATUS_ARC_PENDING(2)]");
notifyEarcStatusToAudioService(false, new ArrayList<>());
- startArcAction(true, null);
+ mHandler.postDelayed( new Runnable() {
+ @Override
+ public void run() {
+ startArcAction(true, null);
+ }
+ }, EARC_TRIGGER_START_ARC_ACTION_DELAY);
getAtomWriter().earcStatusChanged(isEarcSupported(), isEarcEnabled(),
oldEarcStatus, status, HdmiStatsEnums.LOG_REASON_EARC_STATUS_CHANGED);
} else {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 3e23f972bd45..b7091744c3b6 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -21,6 +21,7 @@ import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -63,6 +64,7 @@ final class InputMethodBindingController {
/** Time in milliseconds that the IME service has to bind before it is reconnected. */
static final long TIME_TO_RECONNECT = 3 * 1000;
+ @UserIdInt final int mUserId;
@NonNull private final InputMethodManagerService mService;
@NonNull private final Context mContext;
@NonNull private final PackageManagerInternal mPackageManagerInternal;
@@ -107,12 +109,15 @@ final class InputMethodBindingController {
| Context.BIND_INCLUDE_CAPABILITIES
| Context.BIND_SHOWING_UI;
- InputMethodBindingController(@NonNull InputMethodManagerService service) {
- this(service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
+ InputMethodBindingController(@UserIdInt int userId,
+ @NonNull InputMethodManagerService service) {
+ this(userId, service, IME_CONNECTION_BIND_FLAGS, null /* latchForTesting */);
}
- InputMethodBindingController(@NonNull InputMethodManagerService service,
- int imeConnectionBindFlags, CountDownLatch latchForTesting) {
+ InputMethodBindingController(@UserIdInt int userId,
+ @NonNull InputMethodManagerService service, int imeConnectionBindFlags,
+ CountDownLatch latchForTesting) {
+ mUserId = userId;
mService = service;
mContext = mService.mContext;
mPackageManagerInternal = mService.mPackageManagerInternal;
@@ -301,7 +306,8 @@ final class InputMethodBindingController {
}
if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
final InputMethodInfo info =
- mService.queryInputMethodForCurrentUserLocked(mSelectedMethodId);
+ InputMethodSettingsRepository.get(mUserId).getMethodMap().get(
+ mSelectedMethodId);
boolean supportsStylusHwChanged =
mSupportsStylusHw != info.supportsStylusHandwriting();
mSupportsStylusHw = info.supportsStylusHandwriting();
@@ -339,7 +345,7 @@ final class InputMethodBindingController {
private void updateCurrentMethodUid() {
final String curMethodPackage = mCurIntent.getComponent().getPackageName();
final int curMethodUid = mPackageManagerInternal.getPackageUid(
- curMethodPackage, 0 /* flags */, mService.getCurrentImeUserIdLocked());
+ curMethodPackage, 0 /* flags */, mUserId);
if (curMethodUid < 0) {
Slog.e(TAG, "Failed to get UID for package=" + curMethodPackage);
mCurMethodUid = Process.INVALID_UID;
@@ -425,7 +431,8 @@ final class InputMethodBindingController {
return InputBindResult.NO_IME;
}
- InputMethodInfo info = mService.queryInputMethodForCurrentUserLocked(mSelectedMethodId);
+ InputMethodInfo info = InputMethodSettingsRepository.get(mUserId).getMethodMap().get(
+ mSelectedMethodId);
if (info == null) {
throw new IllegalArgumentException("Unknown id: " + mSelectedMethodId);
}
@@ -497,8 +504,7 @@ final class InputMethodBindingController {
Slog.e(TAG, "--- bind failed: service = " + mCurIntent + ", conn = " + conn);
return false;
}
- return mContext.bindServiceAsUser(mCurIntent, conn, flags,
- new UserHandle(mService.getCurrentImeUserIdLocked()));
+ return mContext.bindServiceAsUser(mCurIntent, conn, flags, new UserHandle(mUserId));
}
@GuardedBy("ImfLock.class")
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8985022ea1b8..1b9d6c598545 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -205,6 +205,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
+import java.util.function.IntFunction;
/**
* This class provides a system service that manages input methods.
@@ -306,16 +307,17 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@MultiUserUnawareField
private final InputMethodMenuController mMenuController;
@MultiUserUnawareField
- @NonNull private final InputMethodBindingController mBindingController;
- @MultiUserUnawareField
- @NonNull private final AutofillSuggestionsController mAutofillController;
+ @NonNull
+ private final AutofillSuggestionsController mAutofillController;
@GuardedBy("ImfLock.class")
@MultiUserUnawareField
- @NonNull private final ImeVisibilityStateComputer mVisibilityStateComputer;
+ @NonNull
+ private final ImeVisibilityStateComputer mVisibilityStateComputer;
@GuardedBy("ImfLock.class")
- @NonNull private final DefaultImeVisibilityApplier mVisibilityApplier;
+ @NonNull
+ private final DefaultImeVisibilityApplier mVisibilityApplier;
/**
* Cache the result of {@code LocalServices.getService(AudioManagerInternal.class)}.
@@ -364,7 +366,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@MultiUserUnawareField
private int mDeviceIdToShowIme = DEVICE_ID_DEFAULT;
- @Nullable private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Nullable
+ private StatusBarManagerInternal mStatusBarManagerInternal;
private boolean mShowOngoingImeSwitcherForPhones;
@GuardedBy("ImfLock.class")
@MultiUserUnawareField
@@ -478,7 +481,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
@Nullable
String getSelectedMethodIdLocked() {
- return mBindingController.getSelectedMethodId();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getSelectedMethodId();
}
/**
@@ -487,7 +491,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
*/
@GuardedBy("ImfLock.class")
private int getSequenceNumberLocked() {
- return mBindingController.getSequenceNumber();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getSequenceNumber();
}
/**
@@ -496,7 +501,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
*/
@GuardedBy("ImfLock.class")
private void advanceSequenceNumberLocked() {
- mBindingController.advanceSequenceNumber();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ userData.mBindingController.advanceSequenceNumber();
}
@GuardedBy("ImfLock.class")
@@ -536,7 +542,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
* The {@link IRemoteAccessibilityInputConnection} last provided by the current client.
*/
@MultiUserUnawareField
- @Nullable IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
+ @Nullable
+ IRemoteAccessibilityInputConnection mCurRemoteAccessibilityInputConnection;
/**
* The {@link EditorInfo} last provided by the current client.
@@ -556,7 +563,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
@Nullable
private String getCurIdLocked() {
- return mBindingController.getCurId();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getCurId();
}
/**
@@ -580,7 +588,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
*/
@GuardedBy("ImfLock.class")
private boolean hasConnectionLocked() {
- return mBindingController.hasMainConnection();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.hasMainConnection();
}
/**
@@ -603,7 +612,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
@Nullable
private Intent getCurIntentLocked() {
- return mBindingController.getCurIntent();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getCurIntent();
}
/**
@@ -613,7 +623,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
@Nullable
IBinder getCurTokenLocked() {
- return mBindingController.getCurToken();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getCurToken();
}
/**
@@ -654,7 +665,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
@Nullable
IInputMethodInvoker getCurMethodLocked() {
- return mBindingController.getCurMethod();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getCurMethod();
}
/**
@@ -662,7 +674,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
*/
@GuardedBy("ImfLock.class")
private int getCurMethodUidLocked() {
- return mBindingController.getCurMethodUid();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getCurMethodUid();
}
/**
@@ -671,7 +684,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
*/
@GuardedBy("ImfLock.class")
private long getLastBindTimeLocked() {
- return mBindingController.getLastBindTime();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ return userData.mBindingController.getLastBindTime();
}
/**
@@ -795,7 +809,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
mRegistered = true;
}
- @Override public void onChange(boolean selfChange, Uri uri) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
final Uri showImeUri = Settings.Secure.getUriFor(
Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD);
final Uri accessibilityRequestingNoImeUri = Settings.Secure.getUriFor(
@@ -888,10 +903,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
for (int userId : mUserManagerInternal.getUserIds()) {
final InputMethodSettings settings = queryInputMethodServicesInternal(
- mContext,
- userId,
- AdditionalSubtypeMapRepository.get(userId),
- DirectBootAwareness.AUTO);
+ mContext,
+ userId,
+ AdditionalSubtypeMapRepository.get(userId),
+ DirectBootAwareness.AUTO);
InputMethodSettingsRepository.put(userId, settings);
}
postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */);
@@ -1353,7 +1368,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
InputMethodManagerService(
Context context,
@Nullable ServiceThread serviceThreadForTesting,
- @Nullable InputMethodBindingController bindingControllerForTesting) {
+ @Nullable IntFunction<InputMethodBindingController> bindingControllerForTesting) {
synchronized (ImfLock.class) {
mContext = context;
mRes = context.getResources();
@@ -1392,7 +1407,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
AdditionalSubtypeMapRepository.initialize(mHandler, mContext);
mCurrentUserId = mActivityManagerInternal.getCurrentUserId();
- mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal);
+ @SuppressWarnings("GuardedBy") final IntFunction<InputMethodBindingController>
+ bindingControllerFactory = userId -> new InputMethodBindingController(userId,
+ InputMethodManagerService.this);
+ mUserDataRepository = new UserDataRepository(mHandler, mUserManagerInternal,
+ bindingControllerForTesting != null ? bindingControllerForTesting
+ : bindingControllerFactory);
for (int id : mUserManagerInternal.getUserIds()) {
mUserDataRepository.getOrCreate(id);
}
@@ -1406,12 +1426,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
new HardwareKeyboardShortcutController(settings.getMethodMap(),
settings.getUserId());
mMenuController = new InputMethodMenuController(this);
- mBindingController =
- bindingControllerForTesting != null
- ? bindingControllerForTesting
- : new InputMethodBindingController(this);
mAutofillController = new AutofillSuggestionsController(this);
-
mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
mVisibilityApplier = new DefaultImeVisibilityApplier(this);
@@ -1544,9 +1559,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
// Note that in b/197848765 we want to see if we can keep the binding alive for better
// profile switching.
- mBindingController.unbindCurrentMethod();
- // TODO(b/325515685): No need to do this once BindingController becomes per-user.
- mBindingController.setSelectedMethodId(null);
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ userData.mBindingController.unbindCurrentMethod();
+
unbindCurrentClientLocked(UnbindReason.SWITCH_USER);
// Hereafter we start initializing things for "newUserId".
@@ -1763,9 +1778,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
// Check if selected IME of current user supports handwriting.
if (userId == mCurrentUserId) {
- return mBindingController.supportsStylusHandwriting()
+ final var userData = mUserDataRepository.getOrCreate(userId);
+ return userData.mBindingController.supportsStylusHandwriting()
&& (!connectionless
- || mBindingController.supportsConnectionlessStylusHandwriting());
+ || userData.mBindingController.supportsConnectionlessStylusHandwriting());
}
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
final InputMethodInfo imi = settings.getMethodMap().get(
@@ -2095,7 +2111,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions);
- if (mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ if (userData.mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) {
mHwController.setInkWindowInitializer(new InkWindowInitializer());
}
return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
@@ -2216,6 +2233,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
if (connectionIsActive != connectionWasActive) {
mInputManagerInternal.notifyInputMethodConnectionActive(connectionIsActive);
}
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+
// If configured, we want to avoid starting up the IME if it is not supposed to be showing
if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags,
@@ -2224,7 +2243,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
Slog.d(TAG, "Avoiding IME startup and unbinding current input method.");
}
invalidateAutofillSessionLocked();
- mBindingController.unbindCurrentMethod();
+ userData.mBindingController.unbindCurrentMethod();
return InputBindResult.NO_EDITOR;
}
@@ -2256,9 +2275,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
}
- mBindingController.unbindCurrentMethod();
-
- return mBindingController.bindCurrentMethod();
+ userData.mBindingController.unbindCurrentMethod();
+ return userData.mBindingController.bindCurrentMethod();
}
/**
@@ -2404,7 +2422,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@FunctionalInterface
interface ImeDisplayValidator {
- @DisplayImePolicy int getDisplayImePolicy(int displayId);
+ @DisplayImePolicy
+ int getDisplayImePolicy(int displayId);
}
/**
@@ -2518,11 +2537,13 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
void resetCurrentMethodAndClientLocked(@UnbindReason int unbindClientReason) {
- mBindingController.setSelectedMethodId(null);
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ userData.mBindingController.setSelectedMethodId(null);
+
// Callback before clean-up binding states.
// TODO(b/338461930): Check if this is still necessary or not.
onUnbindCurrentMethodByReset();
- mBindingController.unbindCurrentMethod();
+ userData.mBindingController.unbindCurrentMethod();
unbindCurrentClientLocked(unbindClientReason);
}
@@ -2697,7 +2718,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
: null;
if (mStatusBarManagerInternal != null) {
mStatusBarManagerInternal.setIcon(mSlotIme, packageName, iconId, 0,
- contentDescription != null
+ contentDescription != null
? contentDescription.toString() : null);
mStatusBarManagerInternal.setIconVisibility(mSlotIme, true);
}
@@ -3099,7 +3120,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
// mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked()
// because mCurMethodId is stored as a history in
// setSelectedInputMethodAndSubtypeLocked().
- mBindingController.setSelectedMethodId(id);
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ userData.mBindingController.setSelectedMethodId(id);
if (mActivityManagerInternal.isSystemReady()) {
Intent intent = new Intent(Intent.ACTION_INPUT_METHOD_CHANGED);
@@ -3154,7 +3176,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@Nullable String delegatorPackageName,
@NonNull IConnectionlessHandwritingCallback callback) {
synchronized (ImfLock.class) {
- if (!mBindingController.supportsConnectionlessStylusHandwriting()) {
+ final var userData = mUserDataRepository.getOrCreate(userId);
+ if (!userData.mBindingController.supportsConnectionlessStylusHandwriting()) {
Slog.w(TAG, "Connectionless stylus handwriting mode unsupported by IME.");
try {
callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED);
@@ -3237,7 +3260,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
final long ident = Binder.clearCallingIdentity();
try {
- if (!mBindingController.supportsStylusHandwriting()) {
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ if (!userData.mBindingController.supportsStylusHandwriting()) {
Slog.w(TAG,
"Stylus HW unsupported by IME. Ignoring startStylusHandwriting()");
return false;
@@ -3420,7 +3444,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
mVisibilityStateComputer.requestImeVisibility(windowToken, true);
// Ensure binding the connection when IME is going to show.
- mBindingController.setCurrentMethodVisible();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ userData.mBindingController.setCurrentMethodVisible();
final IInputMethodInvoker curMethod = getCurMethodLocked();
ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
final boolean readyToDispatchToIme;
@@ -3528,7 +3553,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
} else {
ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
}
- mBindingController.setCurrentMethodNotVisible();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ userData.mBindingController.setCurrentMethodNotVisible();
mVisibilityStateComputer.clearImeShowFlags();
// Cancel existing statsToken for show IME as we got a hide request.
ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
@@ -3810,7 +3836,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
// Note that we can trust client's display ID as long as it matches
// to the display ID obtained from the window.
if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) {
- mBindingController.unbindCurrentMethod();
+ final var userData = mUserDataRepository.getOrCreate(userId);
+ userData.mBindingController.unbindCurrentMethod();
}
}
}
@@ -4271,8 +4298,9 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
mStylusIds.add(deviceId);
// a new Stylus is detected. If IME supports handwriting, and we don't have
// handwriting initialized, lets do it now.
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
if (!mHwController.getCurrentRequestId().isPresent()
- && mBindingController.supportsStylusHandwriting()) {
+ && userData.mBindingController.supportsStylusHandwriting()) {
scheduleResetStylusHandwriting();
}
}
@@ -4684,7 +4712,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
SparseArray<IAccessibilityInputMethodSession> disabledSessions = new SparseArray<>();
for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) {
if (!accessibilitySessions.contains(mEnabledAccessibilitySessions.keyAt(i))) {
- AccessibilitySessionState sessionState = mEnabledAccessibilitySessions.valueAt(i);
+ AccessibilitySessionState sessionState = mEnabledAccessibilitySessions.valueAt(i);
if (sessionState != null) {
disabledSessions.append(mEnabledAccessibilitySessions.keyAt(i),
sessionState.mSession);
@@ -4841,7 +4869,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
case MSG_RESET_HANDWRITING: {
synchronized (ImfLock.class) {
- if (mBindingController.supportsStylusHandwriting()
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ if (userData.mBindingController.supportsStylusHandwriting()
&& getCurMethodLocked() != null && hasSupportedStylusLocked()) {
Slog.d(TAG, "Initializing Handwriting Spy");
mHwController.initializeHandwritingSpy(mCurTokenDisplayId);
@@ -4866,11 +4895,12 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
if (curMethod == null || mImeBindingState.mFocusedWindow == null) {
return true;
}
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
final HandwritingModeController.HandwritingSession session =
mHwController.startHandwritingSession(
msg.arg1 /*requestId*/,
msg.arg2 /*pid*/,
- mBindingController.getCurMethodUid(),
+ userData.mBindingController.getCurMethodUid(),
mImeBindingState.mFocusedWindow);
if (session == null) {
Slog.e(TAG,
@@ -5164,7 +5194,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@GuardedBy("ImfLock.class")
void sendOnNavButtonFlagsChangedLocked() {
- final IInputMethodInvoker curMethod = mBindingController.getCurMethod();
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
+ final IInputMethodInvoker curMethod = userData.mBindingController.getCurMethod();
if (curMethod == null) {
// No need to send the data if the IME is not yet bound.
return;
@@ -5416,7 +5447,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
if (!settings.getMethodMap().containsKey(imeId)
|| !settings.getEnabledInputMethodList().contains(
- settings.getMethodMap().get(imeId))) {
+ settings.getMethodMap().get(imeId))) {
return false; // IME is not found or not enabled.
}
settings.putSelectedInputMethod(imeId);
@@ -5917,9 +5948,10 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
p.println(" mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
p.println(" mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
mImeBindingState.dump(" ", p);
+ final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
p.println(" mCurId=" + getCurIdLocked() + " mHaveConnection=" + hasConnectionLocked()
+ " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
- + mBindingController.isVisibleBound());
+ + userData.mBindingController.isVisibleBound());
p.println(" mCurToken=" + getCurTokenLocked());
p.println(" mCurTokenDisplayId=" + mCurTokenDisplayId);
p.println(" mCurHostInputToken=" + mCurHostInputToken);
@@ -6413,7 +6445,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
if (userId == mCurrentUserId) {
hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */,
SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
- mBindingController.unbindCurrentMethod();
+ final var userData = mUserDataRepository.getOrCreate(userId);
+ userData.mBindingController.unbindCurrentMethod();
// Enable default IMEs, disable others
var toDisable = settings.getEnabledInputMethodList();
@@ -6557,6 +6590,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
private final InputMethodManagerService mImms;
@NonNull
private final IBinder mToken;
+
InputMethodPrivilegedOperationsImpl(InputMethodManagerService imms,
@NonNull IBinder token) {
mImms = imms;
@@ -6585,8 +6619,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@Override
public void createInputContentUriToken(Uri contentUri, String packageName,
AndroidFuture future /* T=IBinder */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<IBinder> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<IBinder> typedFuture = future;
try {
typedFuture.complete(mImms.createInputContentUriToken(
mToken, contentUri, packageName).asBinder());
@@ -6604,8 +6637,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@BinderThread
@Override
public void setInputMethod(String id, AndroidFuture future /* T=Void */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Void> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
try {
mImms.setInputMethod(mToken, id);
typedFuture.complete(null);
@@ -6618,8 +6650,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@Override
public void setInputMethodAndSubtype(String id, InputMethodSubtype subtype,
AndroidFuture future /* T=Void */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Void> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
try {
mImms.setInputMethodAndSubtype(mToken, id, subtype);
typedFuture.complete(null);
@@ -6633,8 +6664,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
public void hideMySoftInput(@NonNull ImeTracker.Token statsToken,
@InputMethodManager.HideFlags int flags, @SoftInputShowHideReason int reason,
AndroidFuture future /* T=Void */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Void> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
try {
mImms.hideMySoftInput(mToken, statsToken, flags, reason);
typedFuture.complete(null);
@@ -6648,8 +6678,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
public void showMySoftInput(@NonNull ImeTracker.Token statsToken,
@InputMethodManager.ShowFlags int flags, @SoftInputShowHideReason int reason,
AndroidFuture future /* T=Void */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Void> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Void> typedFuture = future;
try {
mImms.showMySoftInput(mToken, statsToken, flags, reason);
typedFuture.complete(null);
@@ -6667,8 +6696,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@BinderThread
@Override
public void switchToPreviousInputMethod(AndroidFuture future /* T=Boolean */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Boolean> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
try {
typedFuture.complete(mImms.switchToPreviousInputMethod(mToken));
} catch (Throwable e) {
@@ -6680,8 +6708,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@Override
public void switchToNextInputMethod(boolean onlyCurrentIme,
AndroidFuture future /* T=Boolean */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Boolean> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
try {
typedFuture.complete(mImms.switchToNextInputMethod(mToken, onlyCurrentIme));
} catch (Throwable e) {
@@ -6692,8 +6719,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
@BinderThread
@Override
public void shouldOfferSwitchingToNextInputMethod(AndroidFuture future /* T=Boolean */) {
- @SuppressWarnings("unchecked")
- final AndroidFuture<Boolean> typedFuture = future;
+ @SuppressWarnings("unchecked") final AndroidFuture<Boolean> typedFuture = future;
try {
typedFuture.complete(mImms.shouldOfferSwitchingToNextInputMethod(mToken));
} catch (Throwable e) {
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 7f00229395f8..825cfcbdf505 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -15,6 +15,7 @@
*/
package com.android.server.inputmethod;
+
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.pm.UserInfo;
@@ -25,18 +26,21 @@ import com.android.internal.annotations.GuardedBy;
import com.android.server.pm.UserManagerInternal;
import java.util.function.Consumer;
+import java.util.function.IntFunction;
final class UserDataRepository {
@GuardedBy("ImfLock.class")
private final SparseArray<UserData> mUserData = new SparseArray<>();
+ private final IntFunction<InputMethodBindingController> mBindingControllerFactory;
+
@GuardedBy("ImfLock.class")
@NonNull
UserData getOrCreate(@UserIdInt int userId) {
UserData userData = mUserData.get(userId);
if (userData == null) {
- userData = new UserData(userId);
+ userData = new UserData(userId, mBindingControllerFactory.apply(userId));
mUserData.put(userId, userData);
}
return userData;
@@ -49,7 +53,9 @@ final class UserDataRepository {
}
}
- UserDataRepository(@NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal) {
+ UserDataRepository(@NonNull Handler handler, @NonNull UserManagerInternal userManagerInternal,
+ @NonNull IntFunction<InputMethodBindingController> bindingControllerFactory) {
+ mBindingControllerFactory = bindingControllerFactory;
userManagerInternal.addUserLifecycleListener(
new UserManagerInternal.UserLifecycleListener() {
@Override
@@ -79,11 +85,16 @@ final class UserDataRepository {
@UserIdInt
final int mUserId;
- /**
+ @NonNull
+ final InputMethodBindingController mBindingController;
+
+ /**
* Intended to be instantiated only from this file.
*/
- private UserData(@UserIdInt int userId) {
+ private UserData(@UserIdInt int userId,
+ @NonNull InputMethodBindingController bindingController) {
mUserId = userId;
+ mBindingController = bindingController;
}
}
}
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index babb6c219714..13cc99c804f6 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -108,16 +108,25 @@ public class GroupHelper {
return (flags & mask) != 0;
}
- public void onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+ /**
+ * Called when a notification is newly posted. Checks whether that notification, and all other
+ * active notifications should be grouped or ungrouped atuomatically, and returns whether.
+ * @param sbn The posted notification.
+ * @param autogroupSummaryExists Whether a summary for this notification already exists.
+ * @return Whether the provided notification should be autogrouped synchronously.
+ */
+ public boolean onNotificationPosted(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+ boolean sbnToBeAutogrouped = false;
try {
if (!sbn.isAppGroup()) {
- maybeGroup(sbn, autogroupSummaryExists);
+ sbnToBeAutogrouped = maybeGroup(sbn, autogroupSummaryExists);
} else {
maybeUngroup(sbn, false, sbn.getUserId());
}
} catch (Exception e) {
Slog.e(TAG, "Failure processing new notification", e);
}
+ return sbnToBeAutogrouped;
}
public void onNotificationRemoved(StatusBarNotification sbn) {
@@ -137,20 +146,22 @@ public class GroupHelper {
*
* And stores the list of upgrouped notifications & their flags
*/
- private void maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) {
+ private boolean maybeGroup(StatusBarNotification sbn, boolean autogroupSummaryExists) {
int flags = 0;
List<String> notificationsToGroup = new ArrayList<>();
List<NotificationAttributes> childrenAttr = new ArrayList<>();
+ // Indicates whether the provided sbn should be autogrouped by the caller.
+ boolean sbnToBeAutogrouped = false;
synchronized (mUngroupedNotifications) {
- String key = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
+ String packageKey = generatePackageKey(sbn.getUserId(), sbn.getPackageName());
final ArrayMap<String, NotificationAttributes> children =
- mUngroupedNotifications.getOrDefault(key, new ArrayMap<>());
+ mUngroupedNotifications.getOrDefault(packageKey, new ArrayMap<>());
NotificationAttributes attr = new NotificationAttributes(sbn.getNotification().flags,
sbn.getNotification().getSmallIcon(), sbn.getNotification().color,
sbn.getNotification().visibility);
children.put(sbn.getKey(), attr);
- mUngroupedNotifications.put(key, children);
+ mUngroupedNotifications.put(packageKey, children);
if (children.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
flags = getAutogroupSummaryFlags(children);
@@ -187,10 +198,20 @@ public class GroupHelper {
mCallback.addAutoGroupSummary(sbn.getUserId(), sbn.getPackageName(), sbn.getKey(),
attr);
}
- for (String key : notificationsToGroup) {
- mCallback.addAutoGroup(key);
+ for (String keyToGroup : notificationsToGroup) {
+ if (android.app.Flags.checkAutogroupBeforePost()) {
+ if (keyToGroup.equals(sbn.getKey())) {
+ // Autogrouping for the provided notification is to be done synchronously.
+ sbnToBeAutogrouped = true;
+ } else {
+ mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true);
+ }
+ } else {
+ mCallback.addAutoGroup(keyToGroup, /*requestSort=*/true);
+ }
}
}
+ return sbnToBeAutogrouped;
}
/**
@@ -406,7 +427,7 @@ public class GroupHelper {
}
protected interface Callback {
- void addAutoGroup(String key);
+ void addAutoGroup(String key, boolean requestSort);
void removeAutoGroup(String key);
void addAutoGroupSummary(int userId, String pkg, String triggeringKey,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index ca6ae63e74fc..42ec1c3ad4ed 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -245,7 +245,6 @@ import android.os.DeadObjectException;
import android.os.DeviceIdleManager;
import android.os.Environment;
import android.os.Handler;
-import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IInterface;
@@ -2038,19 +2037,21 @@ public class NotificationManagerService extends SystemService {
mSnoozeHelper.clearData(userHandle);
}
} else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
- final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
- mUserProfiles.updateCache(context);
- if (!mUserProfiles.isProfileUser(userId, context)) {
- // reload per-user settings
- mSettingsObserver.update(null);
- // Refresh managed services
- 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);
+ if (!Flags.useSsmUserSwitchSignal()) {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
+ mUserProfiles.updateCache(context);
+ if (!mUserProfiles.isProfileUser(userId, context)) {
+ // reload per-user settings
+ mSettingsObserver.update(null);
+ // Refresh managed services
+ 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);
+ }
} else if (action.equals(Intent.ACTION_USER_ADDED)) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
if (userId != USER_NULL) {
@@ -2570,7 +2571,9 @@ public class NotificationManagerService extends SystemService {
// calling onDestroy()
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_STOPPED);
- filter.addAction(Intent.ACTION_USER_SWITCHED);
+ if (!Flags.useSsmUserSwitchSignal()) {
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ }
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_UNLOCKED);
@@ -2793,10 +2796,10 @@ public class NotificationManagerService extends SystemService {
return new GroupHelper(getContext(), getContext().getPackageManager(),
mAutoGroupAtCount, new GroupHelper.Callback() {
@Override
- public void addAutoGroup(String key) {
- synchronized (mNotificationLock) {
- addAutogroupKeyLocked(key);
- }
+ public void addAutoGroup(String key, boolean requestSort) {
+ synchronized (mNotificationLock) {
+ addAutogroupKeyLocked(key, requestSort);
+ }
}
@Override
@@ -2966,6 +2969,26 @@ public class NotificationManagerService extends SystemService {
}
@Override
+ public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
+ if (!Flags.useSsmUserSwitchSignal()) {
+ return;
+ }
+ final int userId = to.getUserIdentifier();
+ mUserProfiles.updateCache(getContext());
+ if (!mUserProfiles.isProfileUser(userId, getContext())) {
+ // reload per-user settings
+ mSettingsObserver.update(null);
+ // Refresh managed services
+ 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);
+ }
+
+ @Override
public void onUserStopping(@NonNull TargetUser user) {
mHandler.post(() -> {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "notifHistoryStopUser");
@@ -6538,7 +6561,7 @@ public class NotificationManagerService extends SystemService {
}
@GuardedBy("mNotificationLock")
- void addAutogroupKeyLocked(String key) {
+ void addAutogroupKeyLocked(String key, boolean requestSort) {
NotificationRecord r = mNotificationsByKey.get(key);
if (r == null) {
return;
@@ -6546,7 +6569,9 @@ public class NotificationManagerService extends SystemService {
if (r.getSbn().getOverrideGroupKey() == null) {
addAutoGroupAdjustment(r, GroupHelper.AUTOGROUP_KEY);
EventLogTags.writeNotificationAutogrouped(key);
- mRankingHandler.requestSort();
+ if (!android.app.Flags.checkAutogroupBeforePost() || requestSort) {
+ mRankingHandler.requestSort();
+ }
}
}
@@ -8609,6 +8634,29 @@ public class NotificationManagerService extends SystemService {
notification.flags |= FLAG_NO_CLEAR;
}
+ // Posts the notification if it has a small icon, and potentially autogroup
+ // the new notification.
+ if (android.app.Flags.checkAutogroupBeforePost()) {
+ if (notification.getSmallIcon() != null && !isCritical(r)) {
+ StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
+ if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())
+ || oldSbn.getNotification().flags
+ != n.getNotification().flags) {
+ synchronized (mNotificationLock) {
+ boolean willBeAutogrouped = mGroupHelper.onNotificationPosted(n,
+ hasAutoGroupSummaryLocked(n));
+ if (willBeAutogrouped) {
+ // The newly posted notification will be autogrouped, but
+ // was not autogrouped onPost, to avoid an unnecessary sort.
+ // We add the autogroup key to the notification without a
+ // sort here, and it'll be sorted below with extractSignals.
+ addAutogroupKeyLocked(key, /*requestSort=*/false);
+ }
+ }
+ }
+ }
+ }
+
mRankingHelper.extractSignals(r);
mRankingHelper.sort(mNotificationList);
final int position = mRankingHelper.indexOf(mNotificationList, r);
@@ -8629,17 +8677,20 @@ public class NotificationManagerService extends SystemService {
notifyListenersPostedAndLogLocked(r, old, mTracker, maybeReport);
posted = true;
- StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
- if (oldSbn == null
- || !Objects.equals(oldSbn.getGroup(), n.getGroup())
- || oldSbn.getNotification().flags != n.getNotification().flags) {
- if (!isCritical(r)) {
- mHandler.post(() -> {
- synchronized (mNotificationLock) {
- mGroupHelper.onNotificationPosted(
- n, hasAutoGroupSummaryLocked(n));
- }
- });
+ if (!android.app.Flags.checkAutogroupBeforePost()) {
+ StatusBarNotification oldSbn = (old != null) ? old.getSbn() : null;
+ if (oldSbn == null
+ || !Objects.equals(oldSbn.getGroup(), n.getGroup())
+ || oldSbn.getNotification().flags
+ != n.getNotification().flags) {
+ if (!isCritical(r)) {
+ mHandler.post(() -> {
+ synchronized (mNotificationLock) {
+ mGroupHelper.onNotificationPosted(
+ n, hasAutoGroupSummaryLocked(n));
+ }
+ });
+ }
}
}
} else {
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index af3db6c6be4f..9dcca494ca24 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -128,3 +128,10 @@ flag {
description: "Adds an IPCDataCache for notification channel/group lookups"
bug: "331677193"
}
+
+flag {
+ name: "use_ssm_user_switch_signal"
+ namespace: "systemui"
+ description: "This flag controls which signal is used to handle a user switch system event"
+ bug: "337077643"
+}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 6c93fe787816..56e459057bfa 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -89,6 +89,7 @@ import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import com.android.server.SystemService;
import com.android.server.pm.KnownPackages;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.pkg.PackageState;
@@ -289,6 +290,9 @@ public final class OverlayManagerService extends SystemService {
getContext().registerReceiverAsUser(new UserReceiver(), UserHandle.ALL,
userFilter, null, null);
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ umi.addUserLifecycleListener(new UserLifecycleListener());
+
restoreSettings();
// Wipe all shell overlays on boot, to recover from a potentially broken device
@@ -339,6 +343,7 @@ public final class OverlayManagerService extends SystemService {
if (newUserId == mPrevStartedUserId) {
return;
}
+ Slog.i(TAG, "Updating overlays for starting user " + newUserId);
try {
traceBegin(TRACE_TAG_RRO, "OMS#onStartUser " + newUserId);
// ensure overlays in the settings are up-to-date, and propagate
@@ -515,14 +520,46 @@ public final class OverlayManagerService extends SystemService {
}
}
+ /**
+ * Indicates that the given user is of great importance so that when it is created, we quickly
+ * update its overlays by using a Listener mechanism rather than a Broadcast mechanism. This
+ * is especially important for {@link UserManager#isHeadlessSystemUserMode() HSUM}'s MainUser,
+ * which is created and switched-to immediately on first boot.
+ */
+ private static boolean isHighPriorityUserCreation(UserInfo user) {
+ // TODO: Consider extending this to all created users (guarded behind a flag in that case).
+ return user != null && user.isMain();
+ }
+
+ private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
+ @Override
+ public void onUserCreated(UserInfo user, Object token) {
+ if (isHighPriorityUserCreation(user)) {
+ final int userId = user.id;
+ try {
+ Slog.i(TAG, "Updating overlays for onUserCreated " + userId);
+ traceBegin(TRACE_TAG_RRO, "OMS#onUserCreated " + userId);
+ synchronized (mLock) {
+ updatePackageManagerLocked(mImpl.updateOverlaysForUser(userId));
+ }
+ } finally {
+ traceEnd(TRACE_TAG_RRO);
+ }
+ }
+ }
+ }
+
private final class UserReceiver extends BroadcastReceiver {
@Override
public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
switch (intent.getAction()) {
case ACTION_USER_ADDED:
- if (userId != UserHandle.USER_NULL) {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ UserInfo userInfo = umi.getUserInfo(userId);
+ if (userId != UserHandle.USER_NULL && !isHighPriorityUserCreation(userInfo)) {
try {
+ Slog.i(TAG, "Updating overlays for added user " + userId);
traceBegin(TRACE_TAG_RRO, "OMS ACTION_USER_ADDED");
synchronized (mLock) {
updatePackageManagerLocked(mImpl.updateOverlaysForUser(userId));
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 8a853287738b..71a7d0d2a638 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -46,6 +46,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.LocalLog;
+import android.util.MutableBoolean;
import android.util.Pair;
import android.util.Slog;
import android.util.Xml;
@@ -104,6 +105,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
private final TelephonyManager mTelephonyManager;
private final ArraySet<String> mBugreportAllowlistedPackages;
private final BugreportFileManager mBugreportFileManager;
+ private static final FeatureFlags sFeatureFlags = new FeatureFlagsImpl();
@GuardedBy("mLock")
@@ -429,9 +431,51 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub {
ensureUserCanTakeBugReport(bugreportMode);
Slogf.i(TAG, "Starting bugreport for %s / %d", callingPackage, callingUid);
- synchronized (mLock) {
- startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd,
- bugreportMode, bugreportFlags, listener, isScreenshotRequested);
+ final MutableBoolean handoffLock = new MutableBoolean(false);
+ if (sFeatureFlags.asyncStartBugreport()) {
+ synchronized (handoffLock) {
+ new Thread(()-> {
+ try {
+ synchronized (mLock) {
+ synchronized (handoffLock) {
+ handoffLock.value = true;
+ handoffLock.notifyAll();
+ }
+ startBugreportLocked(
+ callingUid,
+ callingPackage,
+ bugreportFd,
+ screenshotFd,
+ bugreportMode,
+ bugreportFlags,
+ listener,
+ isScreenshotRequested);
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Cannot start a new bugreport due to an unknown error", e);
+ reportError(listener, IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR);
+ }
+ }, "BugreportManagerServiceThread").start();
+ try {
+ while (!handoffLock.value) { // handle the rare case of a spurious wakeup
+ handoffLock.wait(DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS);
+ }
+ } catch (InterruptedException e) {
+ Slog.e(TAG, "Unexpectedly interrupted waiting for startBugreportLocked", e);
+ }
+ }
+ } else {
+ synchronized (mLock) {
+ startBugreportLocked(
+ callingUid,
+ callingPackage,
+ bugreportFd,
+ screenshotFd,
+ bugreportMode,
+ bugreportFlags,
+ listener,
+ isScreenshotRequested);
+ }
}
}
diff --git a/services/core/java/com/android/server/os/core_os_flags.aconfig b/services/core/java/com/android/server/os/core_os_flags.aconfig
index ae33df83e3aa..efdc9b8c164f 100644
--- a/services/core/java/com/android/server/os/core_os_flags.aconfig
+++ b/services/core/java/com/android/server/os/core_os_flags.aconfig
@@ -7,3 +7,13 @@ flag {
description: "Use proto tombstones as source of truth for adding to dropbox"
bug: "323857385"
}
+
+flag {
+ name: "async_start_bugreport"
+ namespace: "crumpet"
+ description: "Don't block callers on the start of dumpsys service"
+ bug: "180123623"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
index 3862b79eb780..5ac883c58f03 100644
--- a/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
+++ b/services/core/java/com/android/server/pm/DefaultCrossProfileIntentFiltersUtils.java
@@ -688,6 +688,29 @@ public class DefaultCrossProfileIntentFiltersUtils {
);
}
+ /** Call intent should be handled by the main user. */
+ private static final DefaultCrossProfileIntentFilter CALL_PRIVATE_PROFILE =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ SKIP_CURRENT_PROFILE,
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_CALL)
+ .addCategory(Intent.CATEGORY_DEFAULT)
+ .addDataScheme("tel")
+ .addDataScheme("sip")
+ .addDataScheme("voicemail")
+ .build();
+
+ /** Pressing the call button should be handled by the main user. */
+ private static final DefaultCrossProfileIntentFilter CALL_BUTTON_PRIVATE_PROFILE =
+ new DefaultCrossProfileIntentFilter.Builder(
+ DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
+ ONLY_IF_NO_MATCH_FOUND,
+ /* letsPersonalDataIntoProfile= */ false)
+ .addAction(Intent.ACTION_CALL_BUTTON)
+ .addCategory(Intent.CATEGORY_DEFAULT)
+ .build();
+
/** Dial intent with mime type can be handled by either private profile or its parent user. */
private static final DefaultCrossProfileIntentFilter DIAL_MIME_PRIVATE_PROFILE =
new DefaultCrossProfileIntentFilter.Builder(
@@ -755,6 +778,10 @@ public class DefaultCrossProfileIntentFiltersUtils {
DIAL_MIME_PRIVATE_PROFILE,
DIAL_DATA_PRIVATE_PROFILE,
DIAL_RAW_PRIVATE_PROFILE,
+ CALL_PRIVATE_PROFILE,
+ CALL_BUTTON_PRIVATE_PROFILE,
+ EMERGENCY_CALL_DATA,
+ EMERGENCY_CALL_MIME,
SMS_MMS_PRIVATE_PROFILE
);
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index fa54f6e69b19..b369f03d002f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -1667,15 +1667,6 @@ public class PackageManagerServiceUtils {
if (appMetadataFile.exists()) {
return true;
}
- if (isSystem) {
- try {
- makeDirRecursive(new File(appMetadataFilePath).getParentFile(), 0700);
- } catch (Exception e) {
- Slog.e(TAG, "Failed to create app metadata dir for package "
- + pkg.getPackageName() + ": " + e.getMessage());
- return false;
- }
- }
Map<String, Property> properties = pkg.getProperties();
if (!properties.containsKey(PROPERTY_ANDROID_SAFETY_LABEL_PATH)) {
return false;
@@ -1684,6 +1675,15 @@ public class PackageManagerServiceUtils {
if (!fileInAPkPathProperty.isString()) {
return false;
}
+ if (isSystem && !appMetadataFile.getParentFile().exists()) {
+ try {
+ makeDirRecursive(appMetadataFile.getParentFile(), 0700);
+ } catch (Exception e) {
+ Slog.e(TAG, "Failed to create app metadata dir for package "
+ + pkg.getPackageName() + ": " + e.getMessage());
+ return false;
+ }
+ }
String fileInApkPath = fileInAPkPathProperty.getString();
List<AndroidPackageSplit> splits = pkg.getSplits();
for (int i = 0; i < splits.size(); i++) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 82902d45f1f2..9edf3b14bad7 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -172,7 +172,7 @@ public class ShortcutService extends IShortcutService.Stub {
static final boolean DEBUG = false; // STOPSHIP if true
static final boolean DEBUG_LOAD = false; // STOPSHIP if true
static final boolean DEBUG_PROCSTATE = false; // STOPSHIP if true
- static final boolean DEBUG_REBOOT = false; // STOPSHIP if true
+ static final boolean DEBUG_REBOOT = true;
@VisibleForTesting
static final long DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day
@@ -3798,24 +3798,36 @@ public class ShortcutService extends IShortcutService.Stub {
final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
final boolean archival = intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false);
+ Slog.d(TAG, "received package broadcast intent: " + intent);
switch (action) {
case Intent.ACTION_PACKAGE_ADDED:
if (replacing) {
+ Slog.d(TAG, "replacing package: " + packageName + " userId" + userId);
handlePackageUpdateFinished(packageName, userId);
} else {
+ Slog.d(TAG, "adding package: " + packageName + " userId" + userId);
handlePackageAdded(packageName, userId);
}
break;
case Intent.ACTION_PACKAGE_REMOVED:
if (!replacing || (replacing && archival)) {
+ if (!replacing) {
+ Slog.d(TAG, "removing package: "
+ + packageName + " userId" + userId);
+ } else if (archival) {
+ Slog.d(TAG, "archiving package: "
+ + packageName + " userId" + userId);
+ }
handlePackageRemoved(packageName, userId);
}
break;
case Intent.ACTION_PACKAGE_CHANGED:
+ Slog.d(TAG, "changing package: " + packageName + " userId" + userId);
handlePackageChanged(packageName, userId);
-
break;
case Intent.ACTION_PACKAGE_DATA_CLEARED:
+ Slog.d(TAG, "clearing data for package: "
+ + packageName + " userId" + userId);
handlePackageDataCleared(packageName, userId);
break;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 3c702b4125b7..b1976cd0d13b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -7155,6 +7155,7 @@ public class UserManagerService extends IUserManager.Stub {
synchronized (mUsersLock) {
pw.println(" Boot user: " + mBootUser);
}
+ pw.println("Can add private profile: "+ canAddPrivateProfile(currentUserId));
pw.println();
pw.println("Number of listeners for");
diff --git a/services/core/java/com/android/server/power/batterysaver/flags.aconfig b/services/core/java/com/android/server/power/batterysaver/flags.aconfig
index fa29dc19b555..1dea5239dbe5 100644
--- a/services/core/java/com/android/server/power/batterysaver/flags.aconfig
+++ b/services/core/java/com/android/server/power/batterysaver/flags.aconfig
@@ -3,7 +3,7 @@ container: "system"
flag {
name: "update_auto_turn_on_notification_string_and_action"
- namespace: "battery_saver"
+ namespace: "backstage_power"
description: "Improve the string and hightligh settings item for battery saver auto-turn-on notification"
bug: "336960905"
metadata {
diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
index 36192537493a..474253223628 100644
--- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
+++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java
@@ -115,6 +115,10 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor {
// validation failure.
private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT = 12;
+ /** Carriers can disable the detector by setting the threshold to -1 */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR = -1;
+
private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20;
// By default, there's no maximum limit enforced
@@ -271,7 +275,10 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor {
// When multiple parallel inbound transforms are created, NetworkMetricMonitor will be
// enabled on the last one as a sample
mInboundTransform = inboundTransform;
- start();
+
+ if (!Flags.allowDisableIpsecLossDetector() || canStart()) {
+ start();
+ }
}
@Override
@@ -284,6 +291,14 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor {
mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig);
}
+
+ if (Flags.allowDisableIpsecLossDetector() && canStart() != isStarted()) {
+ if (canStart()) {
+ start();
+ } else {
+ stop();
+ }
+ }
}
@Override
@@ -298,6 +313,12 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor {
mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L);
}
+ private boolean canStart() {
+ return mInboundTransform != null
+ && mPacketLossRatePercentThreshold
+ != IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR;
+ }
+
@Override
protected void start() {
super.start();
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index b19bc7d43920..dd3d512e471c 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -313,10 +313,14 @@ public class WallpaperCropper {
adjustedCrop.right -= widthToRemove / 2 + widthToRemove % 2;
}
} else {
- // TODO (b/281648899) the third case is not always correct, fix that.
+ // Note: the third case when MODE == BALANCE, -W + sqrt(W * H * R), is the width to add
+ // so that, when removing the appropriate height, we get a bitmap of aspect ratio R and
+ // total surface of W * H. In other words it is the width to add to get the desired
+ // aspect ratio R, while preserving the total number of pixels W * H.
int widthToAdd = mode == REMOVE ? 0
: mode == ADD ? (int) (0.5 + crop.height() * screenRatio - crop.width())
- : (int) (0.5 + crop.height() - crop.width());
+ : (int) (0.5 - crop.width()
+ + Math.sqrt(crop.width() * crop.height() * screenRatio));
int availableWidth = bitmapSize.x - crop.width();
if (availableWidth >= widthToAdd) {
int widthToAddLeft = widthToAdd / 2;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index d20b3b22d778..f8eb78914857 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3646,7 +3646,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
// System wallpaper does not support multiple displays, attach this display to
// the fallback wallpaper.
- if (mFallbackWallpaper != null) {
+ if (mFallbackWallpaper != null && mFallbackWallpaper
+ .connection != null) {
final DisplayConnector connector = mFallbackWallpaper
.connection.getDisplayConnectorOrCreate(displayId);
if (connector == null) return;
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index c5683f31f3e7..c9395daff974 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -957,6 +957,7 @@ class ActivityClientController extends IActivityClientController.Stub {
public boolean enterPictureInPictureMode(IBinder token, final PictureInPictureParams params) {
final long origId = Binder.clearCallingIdentity();
try {
+ ensureSetPipAspectRatioQuotaTracker();
synchronized (mGlobalLock) {
final ActivityRecord r = ensureValidPictureInPictureActivityParams(
"enterPictureInPictureMode", token, params);
@@ -971,6 +972,7 @@ class ActivityClientController extends IActivityClientController.Stub {
public void setPictureInPictureParams(IBinder token, final PictureInPictureParams params) {
final long origId = Binder.clearCallingIdentity();
try {
+ ensureSetPipAspectRatioQuotaTracker();
synchronized (mGlobalLock) {
final ActivityRecord r = ensureValidPictureInPictureActivityParams(
"setPictureInPictureParams", token, params);
@@ -1023,6 +1025,19 @@ class ActivityClientController extends IActivityClientController.Stub {
}
/**
+ * Initialize the {@link #mSetPipAspectRatioQuotaTracker} if applicable, which should happen
+ * out of {@link #mGlobalLock} to avoid deadlock (AM lock is used in QuotaTrack ctor).
+ */
+ private void ensureSetPipAspectRatioQuotaTracker() {
+ if (mSetPipAspectRatioQuotaTracker == null) {
+ mSetPipAspectRatioQuotaTracker = new CountQuotaTracker(mContext,
+ Categorizer.SINGLE_CATEGORIZER);
+ mSetPipAspectRatioQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY,
+ SET_PIP_ASPECT_RATIO_LIMIT, SET_PIP_ASPECT_RATIO_TIME_WINDOW_MS);
+ }
+ }
+
+ /**
* Checks the state of the system and the activity associated with the given {@param token} to
* verify that picture-in-picture is supported for that activity.
*
@@ -1049,12 +1064,6 @@ class ActivityClientController extends IActivityClientController.Stub {
// Rate limit how frequent an app can request aspect ratio change via
// Activity#setPictureInPictureParams
final int userId = UserHandle.getCallingUserId();
- if (mSetPipAspectRatioQuotaTracker == null) {
- mSetPipAspectRatioQuotaTracker = new CountQuotaTracker(mContext,
- Categorizer.SINGLE_CATEGORIZER);
- mSetPipAspectRatioQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY,
- SET_PIP_ASPECT_RATIO_LIMIT, SET_PIP_ASPECT_RATIO_TIME_WINDOW_MS);
- }
if (r.pictureInPictureArgs.hasSetAspectRatio()
&& params.hasSetAspectRatio()
&& !r.pictureInPictureArgs.getAspectRatio().equals(
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 6ec557a15134..b3208bfcc93d 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -88,6 +88,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.ActivityOptions.SourceInfo;
+import android.app.ApplicationStartInfo;
import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.WaitResult;
import android.app.WindowConfiguration.WindowingMode;
@@ -845,6 +846,16 @@ class ActivityMetricsLogger {
&& !r.mTransitionController.isCollecting(r))) {
done(false /* abort */, info, "notifyWindowsDrawn", timestampNs);
}
+
+ if (android.app.Flags.appStartInfoTimestamps()) {
+ // Log here to match StatsD for time to first frame.
+ mLoggerHandler.post(
+ () -> mSupervisor.mService.mWindowManager.mAmInternal.addStartInfoTimestamp(
+ ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME,
+ timestampNs, r.getUid(), r.getPid(),
+ info.mLastLaunchedActivity.mUserId));
+ }
+
return infoSnapshot;
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f3e1dfb3cabd..5e95a4b79d00 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4381,7 +4381,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
*/
protected boolean dumpActivity(FileDescriptor fd, PrintWriter pw, String name, String[] args,
int opti, boolean dumpAll, boolean dumpVisibleRootTasksOnly,
- boolean dumpFocusedRootTaskOnly, int displayIdFilter, @UserIdInt int userId) {
+ boolean dumpFocusedRootTaskOnly, int displayIdFilter, @UserIdInt int userId,
+ long timeout) {
ArrayList<ActivityRecord> activities;
synchronized (mGlobalLock) {
@@ -4426,7 +4427,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
}
}
}
- dumpActivity(" ", fd, pw, activities.get(i), newArgs, dumpAll);
+ dumpActivity(" ", fd, pw, activities.get(i), newArgs, dumpAll, timeout);
}
if (!printedAnything) {
// Typically happpens when no task matches displayIdFilter
@@ -4440,7 +4441,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
* there is a thread associated with the activity.
*/
private void dumpActivity(String prefix, FileDescriptor fd, PrintWriter pw,
- ActivityRecord r, String[] args, boolean dumpAll) {
+ ActivityRecord r, String[] args, boolean dumpAll, long timeout) {
String innerPrefix = prefix + " ";
IApplicationThread appThread = null;
synchronized (mGlobalLock) {
@@ -4471,7 +4472,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
pw.flush();
try (TransferPipe tp = new TransferPipe()) {
appThread.dumpActivity(tp.getWriteFd(), r.token, innerPrefix, args);
- tp.go(fd);
+ tp.go(fd, timeout);
} catch (IOException e) {
pw.println(innerPrefix + "Failure while dumping the activity: " + e);
} catch (RemoteException e) {
@@ -6970,7 +6971,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
boolean dumpFocusedRootTaskOnly, int displayIdFilter,
@UserIdInt int userId) {
return ActivityTaskManagerService.this.dumpActivity(fd, pw, name, args, opti, dumpAll,
- dumpVisibleRootTasksOnly, dumpFocusedRootTaskOnly, displayIdFilter, userId);
+ dumpVisibleRootTasksOnly, dumpFocusedRootTaskOnly, displayIdFilter, userId,
+ /* timeout= */ 5000);
}
@Override
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index 1c599777e497..6aa00397fbf0 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -80,8 +80,8 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
LaunchParamsController.LaunchParams outParams) {
if (!canEnterDesktopMode(mContext)) {
- appendLog("desktop mode is not enabled, continuing");
- return RESULT_CONTINUE;
+ appendLog("desktop mode is not enabled, skipping");
+ return RESULT_SKIP;
}
if (task == null) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2f37e8813365..414724963c76 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2738,6 +2738,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
if (!mVisibleBackgroundUserEnabled) {
return true;
}
+ if (isPrivate()) {
+ // UserManager doesn't track the user visibility for private displays.
+ return true;
+ }
final int userId = UserHandle.getUserId(uid);
return userId == UserHandle.USER_SYSTEM
|| mWmService.mUmInternal.isUserVisible(userId, mDisplayId);
diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java
index ef1b02d8accc..119fafde6f77 100644
--- a/services/core/java/com/android/server/wm/InputConfigAdapter.java
+++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java
@@ -58,8 +58,8 @@ class InputConfigAdapter {
LayoutParams.INPUT_FEATURE_SPY,
InputConfig.SPY, false /* inverted */),
new FlagMapping(
- LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING,
- InputConfig.SENSITIVE_FOR_TRACING, false /* inverted */));
+ LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
+ InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */));
@InputConfigFlags
private static final int INPUT_FEATURE_TO_CONFIG_MASK =
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 8e78d25d8373..6dec71260fa7 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -708,26 +708,6 @@ class RecentTasks {
}
}
- /**
- * Removes the oldest recent task that is compatible with the given one. This is possible if
- * the task windowing mode changed after being added to the Recents.
- */
- void removeCompatibleRecentTask(Task task) {
- final int taskIndex = mTasks.indexOf(task);
- if (taskIndex < 0) {
- return;
- }
-
- final int candidateIndex = findRemoveIndexForTask(task, false /* includingSelf */);
- if (candidateIndex == -1) {
- // Nothing to trim
- return;
- }
-
- final Task taskToRemove = taskIndex > candidateIndex ? task : mTasks.get(candidateIndex);
- remove(taskToRemove);
- }
-
void removeTasksByPackageName(String packageName, int userId) {
for (int i = mTasks.size() - 1; i >= 0; --i) {
final Task task = mTasks.get(i);
@@ -1615,10 +1595,6 @@ class RecentTasks {
* list (if any).
*/
private int findRemoveIndexForAddTask(Task task) {
- return findRemoveIndexForTask(task, true /* includingSelf */);
- }
-
- private int findRemoveIndexForTask(Task task, boolean includingSelf) {
final int recentsCount = mTasks.size();
final Intent intent = task.intent;
final boolean document = intent != null && intent.isDocument();
@@ -1674,8 +1650,6 @@ class RecentTasks {
// existing task
continue;
}
- } else if (!includingSelf) {
- continue;
}
return i;
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 56e5d76ac4e0..a9c47b89b8ff 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3176,6 +3176,11 @@ class Task extends TaskFragment {
mTaskId, snapshot);
}
+ void onSnapshotInvalidated() {
+ mAtmService.getTaskChangeNotificationController().notifyTaskSnapshotInvalidated(mTaskId);
+ }
+
+
TaskDescription getTaskDescription() {
return mTaskDescription;
}
@@ -6883,7 +6888,7 @@ class Task extends TaskFragment {
mIsBoosted = isBoosted;
// The client transaction will be applied together with the next assignLayer.
if (clientTransaction != null) {
- mDecorSurfaceContainer.mPendingClientTransactions.add(clientTransaction);
+ mPendingClientTransactions.add(clientTransaction);
}
}
diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
index 9324e29daafb..21e7a8d63773 100644
--- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
+++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
@@ -61,6 +61,7 @@ class TaskChangeNotificationController {
private static final int NOTIFY_ACTIVITY_ROTATED_MSG = 26;
private static final int NOTIFY_TASK_MOVED_TO_BACK_LISTENERS_MSG = 27;
private static final int NOTIFY_LOCK_TASK_MODE_CHANGED_MSG = 28;
+ private static final int NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG = 29;
// Delay in notifying task stack change listeners (in millis)
private static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100;
@@ -150,6 +151,9 @@ class TaskChangeNotificationController {
private final TaskStackConsumer mNotifyTaskSnapshotChanged = (l, m) -> {
l.onTaskSnapshotChanged(m.arg1, (TaskSnapshot) m.obj);
};
+ private final TaskStackConsumer mNotifyTaskSnapshotInvalidated = (l, m) -> {
+ l.onTaskSnapshotInvalidated(m.arg1);
+ };
private final TaskStackConsumer mNotifyTaskDisplayChanged = (l, m) -> {
l.onTaskDisplayChanged(m.arg1, m.arg2);
@@ -271,6 +275,9 @@ class TaskChangeNotificationController {
case NOTIFY_LOCK_TASK_MODE_CHANGED_MSG:
forAllRemoteListeners(mNotifyLockTaskModeChanged, msg);
break;
+ case NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG:
+ forAllRemoteListeners(mNotifyTaskSnapshotInvalidated, msg);
+ break;
}
if (msg.obj instanceof SomeArgs) {
((SomeArgs) msg.obj).recycle();
@@ -485,6 +492,16 @@ class TaskChangeNotificationController {
}
/**
+ * Notify listeners that the snapshot of a task is invalidated.
+ */
+ void notifyTaskSnapshotInvalidated(int taskId) {
+ final Message msg = mHandler.obtainMessage(NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG,
+ taskId, 0 /* unused */);
+ forAllLocalListeners(mNotifyTaskSnapshotInvalidated, msg);
+ msg.sendToTarget();
+ }
+
+ /**
* Notify listeners that an activity received a back press when there are no other activities
* in the back stack.
*/
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index eeedec398be4..19053f70e7e6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -306,6 +306,18 @@ public abstract class WindowManagerInternal {
}
/**
+ * An interface to be notified on window removal.
+ */
+ public interface OnWindowRemovedListener {
+ /**
+ * Called when a window is removed.
+ *
+ * @param token the client token
+ */
+ void onWindowRemoved(IBinder token);
+ }
+
+ /**
* An interface to be notified when keyguard exit animation should start.
*/
public interface KeyguardExitAnimationStartListener {
@@ -1076,6 +1088,20 @@ public abstract class WindowManagerInternal {
public abstract void clearBlockedApps();
/**
+ * Register a listener to receive a callback on window removal.
+ *
+ * @param listener the listener to be registered.
+ */
+ public abstract void registerOnWindowRemovedListener(OnWindowRemovedListener listener);
+
+ /**
+ * Removes the listener.
+ *
+ * @param listener the listener to be removed.
+ */
+ public abstract void unregisterOnWindowRemovedListener(OnWindowRemovedListener listener);
+
+ /**
* Moves the current focus to the adjacent activity if it has the latest created window.
*/
public abstract boolean moveFocusToAdjacentEmbeddedActivityIfNeeded();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index feede01bdf41..dbe3d369db7d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -44,6 +44,7 @@ import static android.os.Process.myUid;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.permission.flags.Flags.sensitiveContentImprovements;
import static android.permission.flags.Flags.sensitiveContentMetricsBugfix;
+import static android.permission.flags.Flags.sensitiveContentRecentsScreenshotBugfix;
import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
@@ -53,6 +54,7 @@ import static android.service.dreams.Flags.dreamHandlesConfirmKeys;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
+import static android.view.flags.Flags.sensitiveContentAppProtection;
import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
@@ -67,7 +69,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
@@ -147,6 +149,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.SHOW_STACK_CRAWLS;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_VERBOSE_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+import static com.android.server.wm.WindowManagerInternal.OnWindowRemovedListener;
import static com.android.server.wm.WindowManagerServiceDumpProto.BACK_NAVIGATION;
import static com.android.server.wm.WindowManagerServiceDumpProto.DISPLAY_FROZEN;
import static com.android.server.wm.WindowManagerServiceDumpProto.FOCUSED_APP;
@@ -489,6 +492,9 @@ public class WindowManagerService extends IWindowManager.Stub
private final RemoteCallbackList<IKeyguardLockedStateListener> mKeyguardLockedStateListeners =
new RemoteCallbackList<>();
+
+ private final List<OnWindowRemovedListener> mOnWindowRemovedListeners = new ArrayList<>();
+
private boolean mDispatchedKeyguardLockedState = false;
// VR Vr2d Display Id.
@@ -534,6 +540,21 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
+ public void dumpHigh(FileDescriptor fd, PrintWriter pw, String[] args,
+ boolean asProto) {
+ if (asProto) {
+ return;
+ }
+ mAtmService.dumpActivity(fd, pw, /* name= */ "all", /* args= */ new String[]{},
+ /* opti= */ 0,
+ /* dumpAll= */ true,
+ /* dumpVisibleRootTasksOnly= */ true,
+ /* dumpFocusedRootTaskOnly= */ false, INVALID_DISPLAY, UserHandle.USER_ALL,
+ /* timeout= */ 1000
+ );
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
doDump(fd, pw, args, asProto);
}
@@ -2073,7 +2094,11 @@ public class WindowManagerService extends IWindowManager.Stub
*/
void postWindowRemoveCleanupLocked(WindowState win) {
ProtoLog.v(WM_DEBUG_ADD_REMOVE, "postWindowRemoveCleanupLocked: %s", win);
- mWindowMap.remove(win.mClient.asBinder());
+ final IBinder client = win.mClient.asBinder();
+ mWindowMap.remove(client);
+ if (sensitiveContentAppProtection()) {
+ notifyWindowRemovedListeners(client);
+ }
final DisplayContent dc = win.getDisplayContent();
dc.getDisplayRotation().markForSeamlessRotation(win, false /* seamlesslyRotated */);
@@ -5335,6 +5360,23 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ private void notifyWindowRemovedListeners(IBinder client) {
+ OnWindowRemovedListener[] windowRemovedListeners;
+ synchronized (mGlobalLock) {
+ if (mOnWindowRemovedListeners.isEmpty()) {
+ return;
+ }
+ windowRemovedListeners = new OnWindowRemovedListener[mOnWindowRemovedListeners.size()];
+ mOnWindowRemovedListeners.toArray(windowRemovedListeners);
+ }
+ mH.post(() -> {
+ int size = windowRemovedListeners.length;
+ for (int i = 0; i < size; i++) {
+ windowRemovedListeners[i].onWindowRemoved(client);
+ }
+ });
+ }
+
private void notifyWindowsChanged() {
WindowChangeListener[] windowChangeListeners;
synchronized (mGlobalLock) {
@@ -8827,6 +8869,14 @@ public class WindowManagerService extends IWindowManager.Stub
mRoot.forAllWindows((w) -> {
if (w.isVisible()) {
WindowManagerService.this.showToastIfBlockingScreenCapture(w);
+ } else if (sensitiveContentRecentsScreenshotBugfix()
+ && shouldInvalidateSnapshot(w)) {
+ final Task task = w.getTask();
+ // preventing from showing up in starting window.
+ mTaskSnapshotController.removeAndDeleteSnapshot(
+ task.mTaskId, task.mUserId);
+ // Refresh TaskThumbnailCache
+ task.onSnapshotInvalidated();
}
}, /* traverseTopToBottom= */ true);
}
@@ -8834,6 +8884,12 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ private boolean shouldInvalidateSnapshot(WindowState w) {
+ return w.getTask() != null
+ && mSensitiveContentPackages.shouldBlockScreenCaptureForApp(
+ w.getOwningPackage(), w.getOwningUid(), w.getWindowToken());
+ }
+
@Override
public void removeBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) {
synchronized (mGlobalLock) {
@@ -8868,6 +8924,20 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
+ public void registerOnWindowRemovedListener(OnWindowRemovedListener listener) {
+ synchronized (mGlobalLock) {
+ mOnWindowRemovedListeners.add(listener);
+ }
+ }
+
+ @Override
+ public void unregisterOnWindowRemovedListener(OnWindowRemovedListener listener) {
+ synchronized (mGlobalLock) {
+ mOnWindowRemovedListeners.remove(listener);
+ }
+ }
+
+ @Override
public boolean moveFocusToAdjacentEmbeddedActivityIfNeeded() {
synchronized (mGlobalLock) {
final WindowState focusedWindow = getFocusedWindow();
@@ -9241,11 +9311,11 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
- // You can only use INPUT_FEATURE_SENSITIVE_FOR_TRACING on a trusted overlay.
- if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) != 0 && !isTrustedOverlay) {
- Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_TRACING from '" + windowName
+ // You can only use INPUT_FEATURE_SENSITIVE_FOR_PRIVACY on a trusted overlay.
+ if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) != 0 && !isTrustedOverlay) {
+ Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_PRIVACY from '" + windowName
+ "' because it isn't a trusted overlay");
- return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+ return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
}
return inputFeatures;
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 1573d09364ea..90e7bd7b99e6 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1916,7 +1916,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
final int count = tasksToReparent.size();
for (int i = 0; i < count; ++i) {
final Task task = tasksToReparent.get(i);
- final int prevWindowingMode = task.getWindowingMode();
if (syncId >= 0) {
addToSyncSet(syncId, task);
}
@@ -1930,12 +1929,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM,
false /*moveParents*/, "processChildrenTaskReparentHierarchyOp");
}
- // Trim the compatible Recent task (if any) after the Task is reparented and now has
- // a different windowing mode, in order to prevent redundant Recent tasks after
- // reparenting.
- if (prevWindowingMode != task.getWindowingMode()) {
- mService.mTaskSupervisor.mRecentTasks.removeCompatibleRecentTask(task);
- }
}
if (transition != null) transition.collect(newParent);
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 6ef14366b9e7..0c83e8e468d9 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -66,6 +66,7 @@ import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.server.credentials.metrics.ApiName;
import com.android.server.credentials.metrics.ApiStatus;
@@ -1166,11 +1167,17 @@ public final class CredentialManagerService
settingsWrapper.getStringForUser(
Settings.Secure.AUTOFILL_SERVICE, UserHandle.myUserId());
- // If there is an autofill provider and it is the placeholder indicating
+ // If there is an autofill provider and it is the credential autofill service indicating
// that the currently selected primary provider does not support autofill
- // then we should wipe the setting to keep it in sync.
- if (autofillProvider != null && primaryProviders.isEmpty()) {
- if (autofillProvider.equals(AUTOFILL_PLACEHOLDER_VALUE)) {
+ // then we should keep as is
+ String credentialAutofillService = settingsWrapper.mContext.getResources().getString(
+ R.string.config_defaultCredentialManagerAutofillService);
+ if (autofillProvider != null && primaryProviders.isEmpty() && !TextUtils.equals(
+ autofillProvider, credentialAutofillService)) {
+ // If the existing autofill provider is from the app being removed
+ // then erase the autofill service setting.
+ ComponentName cn = ComponentName.unflattenFromString(autofillProvider);
+ if (cn != null && cn.getPackageName().equals(packageName)) {
if (!settingsWrapper.putStringForUser(
Settings.Secure.AUTOFILL_SERVICE,
"",
@@ -1178,19 +1185,6 @@ public final class CredentialManagerService
/* overrideableByRestore= */ true)) {
Slog.e(TAG, "Failed to remove autofill package: " + packageName);
}
- } else {
- // If the existing autofill provider is from the app being removed
- // then erase the autofill service setting.
- ComponentName cn = ComponentName.unflattenFromString(autofillProvider);
- if (cn != null && cn.getPackageName().equals(packageName)) {
- if (!settingsWrapper.putStringForUser(
- Settings.Secure.AUTOFILL_SERVICE,
- "",
- UserHandle.myUserId(),
- /* overrideableByRestore= */ true)) {
- Slog.e(TAG, "Failed to remove autofill package: " + packageName);
- }
- }
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
index 950ec77f5ba8..502607be7310 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java
@@ -19,7 +19,6 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.BooleanPolicyValue;
-import android.app.admin.PolicyKey;
import android.util.Log;
import com.android.modules.utils.TypedXmlPullParser;
@@ -37,8 +36,7 @@ final class BooleanPolicySerializer extends PolicySerializer<Boolean> {
private static final String TAG = "BooleanPolicySerializer";
@Override
- void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, @NonNull Boolean value)
- throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull Boolean value) throws IOException {
Objects.requireNonNull(value);
serializer.attributeBoolean(/* namespace= */ null, ATTR_VALUE, value);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
index d24afabe95a4..a65c7e1870f3 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java
@@ -18,8 +18,6 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.app.admin.BundlePolicyValue;
-import android.app.admin.PackagePolicyKey;
-import android.app.admin.PolicyKey;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
@@ -53,14 +51,8 @@ final class BundlePolicySerializer extends PolicySerializer<Bundle> {
private static final String ATTR_TYPE_BUNDLE_ARRAY = "BA";
@Override
- void saveToXml(@NonNull PolicyKey policyKey, TypedXmlSerializer serializer,
- @NonNull Bundle value) throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull Bundle value) throws IOException {
Objects.requireNonNull(value);
- Objects.requireNonNull(policyKey);
- if (!(policyKey instanceof PackagePolicyKey)) {
- throw new IllegalArgumentException("policyKey is not of type "
- + "PackagePolicyKey");
- }
writeBundle(value, serializer);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
index 6303a1a8b860..01f56e07e157 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java
@@ -19,7 +19,6 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.ComponentNamePolicyValue;
-import android.app.admin.PolicyKey;
import android.content.ComponentName;
import android.util.Log;
@@ -37,8 +36,7 @@ final class ComponentNamePolicySerializer extends PolicySerializer<ComponentName
private static final String ATTR_CLASS_NAME = "class-name";
@Override
- void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
- @NonNull ComponentName value) throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull ComponentName value) throws IOException {
Objects.requireNonNull(value);
serializer.attribute(
/* namespace= */ null, ATTR_PACKAGE_NAME, value.getPackageName());
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 85ab562902e2..375fc5a0280a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -349,8 +349,8 @@ import android.app.admin.PolicyValue;
import android.app.admin.PreferentialNetworkServiceConfig;
import android.app.admin.SecurityLog;
import android.app.admin.SecurityLog.SecurityEvent;
+import android.app.admin.PackageSetPolicyValue;
import android.app.admin.StartInstallingUpdateCallback;
-import android.app.admin.StringSetPolicyValue;
import android.app.admin.SystemUpdateInfo;
import android.app.admin.SystemUpdatePolicy;
import android.app.admin.UnsafeStateException;
@@ -455,10 +455,10 @@ import android.security.IKeyChainAliasCallback;
import android.security.IKeyChainService;
import android.security.KeyChain;
import android.security.KeyChain.KeyChainConnection;
-import android.security.KeyStore;
import android.security.keymaster.KeymasterCertificateChain;
import android.security.keystore.AttestationUtils;
import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
import android.security.keystore.ParcelableKeyGenParameterSpec;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.telecom.TelecomManager;
@@ -1985,11 +1985,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
CryptoTestHelper.runAndLogSelfTest();
}
- public String[] getPersonalAppsForSuspension(@UserIdInt int userId) {
- return PersonalAppsSuspensionHelper.forUser(mContext, userId)
- .getPersonalAppsForSuspension();
- }
-
public long systemCurrentTimeMillis() {
return System.currentTimeMillis();
}
@@ -6248,7 +6243,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
try (KeyChainConnection keyChainConnection =
KeyChain.bindAsUser(mContext, caller.getUserHandle())) {
IKeyChainService keyChain = keyChainConnection.getService();
- if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyStore.UID_SELF)) {
+ if (!keyChain.installKeyPair(privKey, cert, chain, alias, KeyProperties.UID_SELF)) {
logInstallKeyPairFailure(caller, isCredentialManagementApp);
return false;
}
@@ -6583,7 +6578,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
// As the caller will be granted access to the key, ensure no UID was specified, as
// it will not have the desired effect.
- if (keySpec.getUid() != KeyStore.UID_SELF) {
+ if (keySpec.getUid() != KeyProperties.UID_SELF) {
Slogf.e(LOG_TAG, "Only the caller can be granted access to the generated keypair.");
logGenerateKeyPairFailure(caller, isCredentialManagementApp);
return false;
@@ -12078,7 +12073,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.PERMITTED_INPUT_METHODS,
admin,
- new StringSetPolicyValue(new HashSet<>(packageList)),
+ new PackageSetPolicyValue(new HashSet<>(packageList)),
userId);
}
}
@@ -20363,12 +20358,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mDevicePolicyEngine.setGlobalPolicy(
PolicyDefinition.USER_CONTROLLED_DISABLED_PACKAGES,
enforcingAdmin,
- new StringSetPolicyValue(packages));
+ new PackageSetPolicyValue(packages));
} else {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.USER_CONTROLLED_DISABLED_PACKAGES,
enforcingAdmin,
- new StringSetPolicyValue(packages),
+ new PackageSetPolicyValue(packages),
caller.getUserId());
}
}
@@ -21610,9 +21605,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
== HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
}
- if (Flags.headlessSingleUserFixes() && mInjector.userManagerIsHeadlessSystemUserMode()
- && isSingleUserMode && !mInjector.isChangeEnabled(
- PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) {
+ if (Flags.headlessSingleMinTargetSdk()
+ && mInjector.userManagerIsHeadlessSystemUserMode()
+ && isSingleUserMode
+ && !mInjector.isChangeEnabled(
+ PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(),
+ caller.getUserId())) {
throw new IllegalStateException("Device admin is not targeting Android V.");
}
@@ -24047,7 +24045,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.PERMITTED_INPUT_METHODS,
enforcingAdmin,
- new StringSetPolicyValue(
+ new PackageSetPolicyValue(
new HashSet<>(admin.permittedInputMethods)),
admin.getUserHandle().getIdentifier());
}
@@ -24056,7 +24054,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.PERMITTED_INPUT_METHODS,
enforcingAdmin,
- new StringSetPolicyValue(
+ new PackageSetPolicyValue(
new HashSet<>(admin.getParentActiveAdmin()
.permittedInputMethods)),
getProfileParentId(admin.getUserHandle().getIdentifier()));
@@ -24112,12 +24110,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
mDevicePolicyEngine.setGlobalPolicy(
PolicyDefinition.USER_CONTROLLED_DISABLED_PACKAGES,
enforcingAdmin,
- new StringSetPolicyValue(new HashSet<>(admin.protectedPackages)));
+ new PackageSetPolicyValue(
+ new HashSet<>(admin.protectedPackages)));
} else {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.USER_CONTROLLED_DISABLED_PACKAGES,
enforcingAdmin,
- new StringSetPolicyValue(new HashSet<>(admin.protectedPackages)),
+ new PackageSetPolicyValue(
+ new HashSet<>(admin.protectedPackages)),
admin.getUserHandle().getIdentifier());
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
index 45a2d2a7bda1..ebbf22cfef69 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java
@@ -19,7 +19,6 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.IntegerPolicyValue;
-import android.app.admin.PolicyKey;
import android.util.Log;
import com.android.modules.utils.TypedXmlPullParser;
@@ -37,8 +36,7 @@ final class IntegerPolicySerializer extends PolicySerializer<Integer> {
private static final String ATTR_VALUE = "value";
@Override
- void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
- @NonNull Integer value) throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull Integer value) throws IOException {
Objects.requireNonNull(value);
serializer.attributeInt(/* namespace= */ null, ATTR_VALUE, value);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
index 20bd2d75f846..13412d05aba3 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
@@ -18,7 +18,6 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.app.admin.LockTaskPolicy;
-import android.app.admin.PolicyKey;
import android.util.Log;
import com.android.modules.utils.TypedXmlPullParser;
@@ -39,8 +38,8 @@ final class LockTaskPolicySerializer extends PolicySerializer<LockTaskPolicy> {
private static final String ATTR_FLAGS = "flags";
@Override
- void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
- @NonNull LockTaskPolicy value) throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull LockTaskPolicy value)
+ throws IOException {
Objects.requireNonNull(value);
serializer.attribute(
/* namespace= */ null,
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java
index 522c4b5e84be..c363e6626de3 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java
@@ -19,7 +19,6 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.LongPolicyValue;
-import android.app.admin.PolicyKey;
import android.util.Log;
import com.android.modules.utils.TypedXmlPullParser;
@@ -37,8 +36,7 @@ final class LongPolicySerializer extends PolicySerializer<Long> {
private static final String ATTR_VALUE = "value";
@Override
- void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
- @NonNull Long value) throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull Long value) throws IOException {
Objects.requireNonNull(value);
serializer.attributeLong(/* namespace= */ null, ATTR_VALUE, value);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSetPolicySerializer.java
index 0265453eecc7..c4da029c6315 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSetPolicySerializer.java
@@ -18,9 +18,8 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.admin.PolicyKey;
import android.app.admin.PolicyValue;
-import android.app.admin.StringSetPolicyValue;
+import android.app.admin.PackageSetPolicyValue;
import android.util.Log;
import com.android.modules.utils.TypedXmlPullParser;
@@ -31,12 +30,11 @@ import java.util.Objects;
import java.util.Set;
// TODO(scottjonathan): Replace with generic set implementation
-final class StringSetPolicySerializer extends PolicySerializer<Set<String>> {
+final class PackageSetPolicySerializer extends PolicySerializer<Set<String>> {
private static final String ATTR_VALUES = "strings";
private static final String ATTR_VALUES_SEPARATOR = ";";
@Override
- void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
- @NonNull Set<String> value) throws IOException {
+ void saveToXml(TypedXmlSerializer serializer, @NonNull Set<String> value) throws IOException {
Objects.requireNonNull(value);
serializer.attribute(
/* namespace= */ null, ATTR_VALUES, String.join(ATTR_VALUES_SEPARATOR, value));
@@ -47,10 +45,10 @@ final class StringSetPolicySerializer extends PolicySerializer<Set<String>> {
PolicyValue<Set<String>> readFromXml(TypedXmlPullParser parser) {
String valuesStr = parser.getAttributeValue(/* namespace= */ null, ATTR_VALUES);
if (valuesStr == null) {
- Log.e(DevicePolicyEngine.TAG, "Error parsing StringSet policy value.");
+ Log.e(DevicePolicyEngine.TAG, "Error parsing PackageSet policy value.");
return null;
}
Set<String> values = Set.of(valuesStr.split(ATTR_VALUES_SEPARATOR));
- return new StringSetPolicyValue(values);
+ return new PackageSetPolicyValue(values);
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetUnion.java b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSetUnion.java
index 5298960892a3..d1e241b365a6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetUnion.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSetUnion.java
@@ -18,14 +18,15 @@ package com.android.server.devicepolicy;
import android.annotation.NonNull;
import android.app.admin.PolicyValue;
-import android.app.admin.StringSetPolicyValue;
+import android.app.admin.PackageSetPolicyValue;
+import android.app.admin.StringSetUnion;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Set;
-final class StringSetUnion extends ResolutionMechanism<Set<String>> {
+final class PackageSetUnion extends ResolutionMechanism<Set<String>> {
@Override
PolicyValue<Set<String>> resolve(
@@ -38,17 +39,17 @@ final class StringSetUnion extends ResolutionMechanism<Set<String>> {
for (PolicyValue<Set<String>> policy : adminPolicies.values()) {
unionOfPolicies.addAll(policy.getValue());
}
- return new StringSetPolicyValue(unionOfPolicies);
+ return new PackageSetPolicyValue(unionOfPolicies);
}
@Override
- android.app.admin.StringSetUnion getParcelableResolutionMechanism() {
- return new android.app.admin.StringSetUnion();
+ StringSetUnion getParcelableResolutionMechanism() {
+ return new StringSetUnion();
}
@Override
public String toString() {
- return "SetUnion {}";
+ return "PackageSetUnion {}";
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
index 8cb511e8727c..7483b43baf13 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -37,7 +37,6 @@ import android.provider.Telephony;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
-import android.util.Log;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManager;
import android.view.inputmethod.InputMethodInfo;
@@ -107,10 +106,6 @@ public final class PersonalAppsSuspensionHelper {
for (final String pkg : unsuspendablePackages) {
result.remove(pkg);
}
-
- if (Log.isLoggable(LOG_TAG, Log.INFO)) {
- Slogf.i(LOG_TAG, "Packages subject to suspension: %s", String.join(",", result));
- }
return result.toArray(new String[0]);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 7a9fa0fb5658..8d980b5031e2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -162,9 +162,9 @@ final class PolicyDefinition<V> {
new PolicyDefinition<>(
new NoArgsPolicyKey(
DevicePolicyIdentifiers.USER_CONTROL_DISABLED_PACKAGES_POLICY),
- new StringSetUnion(),
+ new PackageSetUnion(),
PolicyEnforcerCallbacks::setUserControlDisabledPackages,
- new StringSetPolicySerializer());
+ new PackageSetPolicySerializer());
// This is saved in the static map sPolicyDefinitions so that we're able to reconstruct the
// actual policy with the correct arguments (i.e. packageName) when reading the policies from
@@ -328,7 +328,7 @@ final class PolicyDefinition<V> {
new MostRecent<>(),
POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE,
(Set<String> value, Context context, Integer userId, PolicyKey policyKey) -> true,
- new StringSetPolicySerializer());
+ new PackageSetPolicySerializer());
static PolicyDefinition<Boolean> SCREEN_CAPTURE_DISABLED = new PolicyDefinition<>(
@@ -684,7 +684,7 @@ final class PolicyDefinition<V> {
void savePolicyValueToXml(TypedXmlSerializer serializer, V value)
throws IOException {
- mPolicySerializer.saveToXml(mPolicyKey, serializer, value);
+ mPolicySerializer.saveToXml(serializer, value);
}
@Nullable
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index eeb49765cc9d..4bf3ff4265d4 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -375,6 +375,7 @@ final class PolicyEnforcerCallbacks {
private static void suspendPersonalAppsInPackageManager(Context context, int userId) {
final String[] appsToSuspend = PersonalAppsSuspensionHelper.forUser(context, userId)
.getPersonalAppsForSuspension();
+ Slogf.i(LOG_TAG, "Suspending personal apps: %s", String.join(",", appsToSuspend));
final String[] failedApps = LocalServices.getService(PackageManagerInternal.class)
.setPackagesSuspendedByAdmin(userId, appsToSuspend, true);
if (!ArrayUtils.isEmpty(failedApps)) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
index 5af2fa285483..e83b031fd3c0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java
@@ -17,7 +17,6 @@
package com.android.server.devicepolicy;
import android.annotation.NonNull;
-import android.app.admin.PolicyKey;
import android.app.admin.PolicyValue;
import com.android.modules.utils.TypedXmlPullParser;
@@ -26,7 +25,6 @@ import com.android.modules.utils.TypedXmlSerializer;
import java.io.IOException;
abstract class PolicySerializer<V> {
- abstract void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, @NonNull V value)
- throws IOException;
+ abstract void saveToXml(TypedXmlSerializer serializer, @NonNull V value) throws IOException;
abstract PolicyValue<V> readFromXml(TypedXmlPullParser parser);
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8caf5aedda49..8755a8077ddb 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1618,7 +1618,8 @@ public final class SystemServer implements Dumpable {
wm = WindowManagerService.main(context, inputManager, !mFirstBoot,
new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,
- DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);
+ DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH
+ | DUMP_FLAG_PROTO);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
/* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);
t.traceEnd();
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 1f0a37509989..70903cbc2b94 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -77,9 +77,13 @@ public class InputMethodBindingControllerTest extends InputMethodManagerServiceT
mCountDownLatch = new CountDownLatch(1);
// Remove flag Context.BIND_SCHEDULE_LIKE_TOP_APP because in tests we are not calling
// from system.
- mBindingController =
- new InputMethodBindingController(
- mInputMethodManagerService, mImeConnectionBindFlags, mCountDownLatch);
+ synchronized (ImfLock.class) {
+ mBindingController =
+ new InputMethodBindingController(
+ mInputMethodManagerService.getCurrentImeUserIdLocked(),
+ mInputMethodManagerService, mImeConnectionBindFlags,
+ mCountDownLatch);
+ }
}
@Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
index b4cf79941c33..cff22654e30c 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java
@@ -222,7 +222,7 @@ public class InputMethodManagerServiceTestBase {
Process.THREAD_PRIORITY_FOREGROUND, /* allowIo */
false);
mInputMethodManagerService = new InputMethodManagerService(mContext, mServiceThread,
- mMockInputMethodBindingController);
+ unusedUserId -> mMockInputMethodBindingController);
spyOn(mInputMethodManagerService);
// Start a InputMethodManagerService.Lifecycle to publish and manage the lifecycle of
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
index a15b17042174..c3a87dafe7ce 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java
@@ -18,6 +18,7 @@ package com.android.server.inputmethod;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -38,6 +39,7 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.IntFunction;
// This test is designed to run on both device and host (Ravenwood) side.
public final class UserDataRepositoryTest {
@@ -51,19 +53,34 @@ public final class UserDataRepositoryTest {
@Mock
private UserManagerInternal mMockUserManagerInternal;
+ @Mock
+ private InputMethodManagerService mMockInputMethodManagerService;
+
private Handler mHandler;
+ private IntFunction<InputMethodBindingController> mBindingControllerFactory;
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mHandler = new Handler(Looper.getMainLooper());
+ mBindingControllerFactory = new IntFunction<InputMethodBindingController>() {
+
+ @Override
+ public InputMethodBindingController apply(int userId) {
+ return new InputMethodBindingController(userId, mMockInputMethodManagerService);
+ }
+ };
}
@Test
public void testUserDataRepository_addsNewUserInfoOnUserCreatedEvent() {
// Create UserDataRepository and capture the user lifecycle listener
final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
- final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
+ final var bindingControllerFactorySpy = spy(mBindingControllerFactory);
+ final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal,
+ bindingControllerFactorySpy);
+
verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
final var listener = captor.getValue();
@@ -77,14 +94,20 @@ public final class UserDataRepositoryTest {
// Assert UserDataRepository contains the expected UserData
final var allUserData = collectUserData(repository);
assertThat(allUserData).hasSize(1);
- assertThat(allUserData.get(0).mUserId).isEqualTo(userInfo.id);
+ assertThat(allUserData.get(0).mUserId).isEqualTo(ANY_USER_ID);
+
+ // Assert UserDataRepository called the InputMethodBindingController creator function.
+ verify(bindingControllerFactorySpy).apply(ANY_USER_ID);
+ assertThat(allUserData.get(0).mBindingController.mUserId).isEqualTo(ANY_USER_ID);
}
@Test
public void testUserDataRepository_removesUserInfoOnUserRemovedEvent() {
// Create UserDataRepository and capture the user lifecycle listener
final var captor = ArgumentCaptor.forClass(UserManagerInternal.UserLifecycleListener.class);
- final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
+ final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal,
+ userId -> new InputMethodBindingController(userId, mMockInputMethodManagerService));
+
verify(mMockUserManagerInternal, times(1)).addUserLifecycleListener(captor.capture());
final var listener = captor.getValue();
@@ -104,7 +127,8 @@ public final class UserDataRepositoryTest {
@Test
public void testGetOrCreate() {
- final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal);
+ final var repository = new UserDataRepository(mHandler, mMockUserManagerInternal,
+ mBindingControllerFactory);
synchronized (ImfLock.class) {
final var userData = repository.getOrCreate(ANY_USER_ID);
@@ -114,6 +138,9 @@ public final class UserDataRepositoryTest {
final var allUserData = collectUserData(repository);
assertThat(allUserData).hasSize(1);
assertThat(allUserData.get(0).mUserId).isEqualTo(ANY_USER_ID);
+
+ // Assert UserDataRepository called the InputMethodBindingController creator function.
+ assertThat(allUserData.get(0).mBindingController.mUserId).isEqualTo(ANY_USER_ID);
}
private List<UserDataRepository.UserData> collectUserData(UserDataRepository repository) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
index 7aafa8e92690..5ddd8a50135b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
@@ -26,7 +26,6 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
import android.content.pm.PackageManagerInternal;
import android.media.projection.MediaProjectionInfo;
@@ -108,7 +107,7 @@ public class SensitiveContentProtectionManagerServiceContentTest {
mMediaPorjectionCallback.onStart(exemptedRecorderPackage);
mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -135,7 +134,7 @@ public class SensitiveContentProtectionManagerServiceContentTest {
// when screen sharing is not active, no app window should be blocked.
mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -158,8 +157,7 @@ public class SensitiveContentProtectionManagerServiceContentTest {
mMediaPorjectionCallback.onStart(mMediaProjectionInfo);
mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
- verify(mWindowManager, never())
- .addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -168,7 +166,7 @@ public class SensitiveContentProtectionManagerServiceContentTest {
mMediaProjectionCallbackCaptor.getValue().onStart(mMediaProjectionInfo);
mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
private void mockDisabledViaDeveloperOption() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
index a20d935c50aa..8b653378664e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
@@ -30,7 +30,6 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManagerInternal;
@@ -102,6 +101,8 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
@Captor
ArgumentCaptor<MediaProjectionManager.Callback> mMediaProjectionCallbackCaptor;
+ @Captor
+ private ArgumentCaptor<ArraySet<PackageInfo>> mPackageInfoCaptor;
@Mock
private MediaProjectionManager mProjectionManager;
@@ -309,7 +310,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -469,7 +470,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -480,7 +481,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -495,7 +496,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -519,7 +520,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -530,7 +531,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -541,7 +542,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -557,7 +558,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -574,7 +575,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -586,7 +587,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -598,7 +599,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verifyNoBlockOrClearInteractionWithWindowManager();
}
@Test
@@ -614,7 +615,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verifyNoBlockOrClearInteractionWithWindowManager();
}
@Test
@@ -640,7 +641,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -652,7 +653,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -666,7 +667,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(null);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -684,7 +685,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -702,7 +703,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -715,7 +716,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationRankingUpdate(mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -727,7 +728,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(mNotification1, mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verifyNoBlockOrClearInteractionWithWindowManager();
}
@Test
@@ -743,7 +744,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(mNotification1, mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verifyNoBlockOrClearInteractionWithWindowManager();
}
@Test
@@ -773,7 +774,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(mNotification2, mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -787,7 +788,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(null, mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -801,7 +802,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(mNotification1, null);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -816,7 +817,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(mNotification1, mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
@Test
@@ -829,7 +830,14 @@ public class SensitiveContentProtectionManagerServiceNotificationTest {
mSensitiveContentProtectionManagerService.mNotificationListener
.onNotificationPosted(mNotification1, mRankingMap);
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+ }
+
+ private void verifyNoBlockOrClearInteractionWithWindowManager() {
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+ verify(mWindowManager, never()).clearBlockedApps();
+ verify(mWindowManager, never())
+ .removeBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
}
private void mockDisabledViaDevelopOption() {
diff --git a/services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java b/services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java
new file mode 100644
index 000000000000..0bca59d4b379
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/autofill/SaveEventLoggerTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.autofill;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(JUnit4.class)
+public class SaveEventLoggerTest {
+
+ @Test
+ public void testTimestampsInitialized() {
+ SaveEventLogger mLogger = spy(SaveEventLogger.forSessionId(1, 1));
+
+ mLogger.maybeSetLatencySaveUiDisplayMillis();
+ mLogger.maybeSetLatencySaveRequestMillis();
+ mLogger.maybeSetLatencySaveFinishMillis();
+
+ ArgumentCaptor<Long> latencySaveUiDisplayMillis = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> latencySaveRequestMillis = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> latencySaveFinishMillis = ArgumentCaptor.forClass(Long.class);
+
+ verify(mLogger, times(1))
+ .maybeSetLatencySaveUiDisplayMillis(latencySaveUiDisplayMillis.capture());
+ verify(mLogger, times(1))
+ .maybeSetLatencySaveRequestMillis(latencySaveRequestMillis.capture());
+ verify(mLogger, times(1))
+ .maybeSetLatencySaveFinishMillis(latencySaveFinishMillis.capture());
+
+ assertThat(latencySaveUiDisplayMillis.getValue())
+ .isNotEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+ assertThat(latencySaveRequestMillis.getValue())
+ .isNotEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+ assertThat(latencySaveFinishMillis.getValue())
+ .isNotEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+ }
+
+ @Test
+ public void testTimestampsNotInitialized() {
+ SaveEventLogger mLogger =
+ spy(SaveEventLogger.forSessionId(1, SaveEventLogger.UNINITIATED_TIMESTAMP));
+
+ mLogger.maybeSetLatencySaveUiDisplayMillis();
+ mLogger.maybeSetLatencySaveRequestMillis();
+ mLogger.maybeSetLatencySaveFinishMillis();
+ ArgumentCaptor<Long> latencySaveUiDisplayMillis = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> latencySaveRequestMillis = ArgumentCaptor.forClass(Long.class);
+ ArgumentCaptor<Long> latencySaveFinishMillis = ArgumentCaptor.forClass(Long.class);
+
+ verify(mLogger, times(1))
+ .maybeSetLatencySaveUiDisplayMillis(latencySaveUiDisplayMillis.capture());
+ verify(mLogger, times(1))
+ .maybeSetLatencySaveRequestMillis(latencySaveRequestMillis.capture());
+ verify(mLogger, times(1))
+ .maybeSetLatencySaveFinishMillis(latencySaveFinishMillis.capture());
+
+ assertThat(latencySaveUiDisplayMillis.getValue())
+ .isEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+ assertThat(latencySaveRequestMillis.getValue())
+ .isEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+ assertThat(latencySaveFinishMillis.getValue())
+ .isEqualTo(SaveEventLogger.UNINITIATED_TIMESTAMP);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 855c6582dfec..b4cc3434e013 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -441,11 +441,6 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi
@Override
public void runCryptoSelfTest() {}
- @Override
- public String[] getPersonalAppsForSuspension(int userId) {
- return new String[]{};
- }
-
public void setSystemCurrentTimeMillis(long value) {
mCurrentTimeMillis = value;
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 9a92c704c247..e1b66b53ecbe 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -106,6 +106,7 @@ public class HdmiControlServiceTest {
private HdmiPortInfo[] mHdmiPortInfo;
private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
private static final int PORT_ID_EARC_SUPPORTED = 3;
+ private static final int EARC_TRIGGER_START_ARC_ACTION_DELAY = 500;
@Before
public void setUp() throws Exception {
@@ -1374,6 +1375,11 @@ public class HdmiControlServiceTest {
PORT_ID_EARC_SUPPORTED);
verify(mHdmiControlServiceSpy, times(1))
.notifyEarcStatusToAudioService(eq(false), eq(new ArrayList<>()));
+ // ARC should be never initiated here. It should be started after 500 ms.
+ verify(mHdmiControlServiceSpy, times(0)).startArcAction(anyBoolean(), any());
+ // We move 500 ms forward because the action is only started 500 ms later.
+ mTestLooper.moveTimeForward(EARC_TRIGGER_START_ARC_ACTION_DELAY);
+ mTestLooper.dispatchAll();
verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(true), any());
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 1194973b4cad..c7c97e40b424 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -34,6 +34,7 @@ import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
@@ -54,6 +55,7 @@ import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.StatusBarNotification;
@@ -271,7 +273,8 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
- public void testAddSummary() {
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAddSummary_alwaysAutogroup() {
final String pkg = "package";
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
mGroupHelper.onNotificationPosted(
@@ -279,13 +282,52 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAddSummary() {
+ final String pkg = "package";
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ assertThat(mGroupHelper.onNotificationPosted(
+ getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM), false)).isFalse();
+ }
+ assertThat(mGroupHelper.onNotificationPosted(
+ getSbn(pkg, AUTOGROUP_AT_COUNT - 1, String.valueOf(AUTOGROUP_AT_COUNT - 1),
+ UserHandle.SYSTEM), false)).isTrue();
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAddSummary_oneChildOngoing_summaryOngoing_alwaysAutogroup() {
+ final String pkg = "package";
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+ if (i == 0) {
+ sbn.getNotification().flags |= FLAG_ONGOING_EVENT;
+ }
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
public void testAddSummary_oneChildOngoing_summaryOngoing() {
final String pkg = "package";
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -297,13 +339,33 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
eq(getNotificationAttributes(BASE_FLAGS | FLAG_ONGOING_EVENT)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel_alwaysAutogroup() {
+ final String pkg = "package";
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+ if (i == 0) {
+ sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+ }
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
public void testAddSummary_oneChildAutoCancel_summaryNotAutoCancel() {
final String pkg = "package";
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -315,13 +377,31 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel_alwaysAutogroup() {
+ final String pkg = "package";
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+ sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
public void testAddSummary_allChildrenAutoCancel_summaryAutoCancel() {
final String pkg = "package";
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -331,13 +411,34 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
+ }
+
+ @Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAddSummary_summaryAutoCancelNoClear_alwaysAutogroup() {
+ final String pkg = "package";
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+ sbn.getNotification().flags |= FLAG_AUTO_CANCEL;
+ if (i == 0) {
+ sbn.getNotification().flags |= FLAG_NO_CLEAR;
+ }
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
public void testAddSummary_summaryAutoCancelNoClear() {
final String pkg = "package";
for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -350,7 +451,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
eq(getNotificationAttributes(BASE_FLAGS | FLAG_AUTO_CANCEL | FLAG_NO_CLEAR)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), any());
@@ -617,7 +718,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
Mockito.reset(mCallback);
@@ -645,7 +746,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
Mockito.reset(mCallback);
@@ -664,6 +765,47 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() {
+ final String pkg = "package";
+ List<StatusBarNotification> posted = new ArrayList<>();
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+ posted.add(sbn);
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ Mockito.reset(mCallback);
+
+ for (int i = posted.size() - 2; i >= 0; i--) {
+ mGroupHelper.onNotificationRemoved(posted.remove(i));
+ }
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ Mockito.reset(mCallback);
+
+ // only one child remains
+ assertEquals(1, mGroupHelper.getNotGroupedByAppCount(UserHandle.USER_SYSTEM, pkg));
+
+ // Add new notification; it should be autogrouped even though the total count is
+ // < AUTOGROUP_AT_COUNT
+ final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM);
+ posted.add(sbn);
+ assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isFalse();
+ verify(mCallback, times(1)).addAutoGroup(sbn.getKey(), true);
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
+ eq(getNotificationAttributes(BASE_FLAGS)));
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), any());
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled() {
final String pkg = "package";
List<StatusBarNotification> posted = new ArrayList<>();
@@ -672,9 +814,10 @@ public class GroupHelperTest extends UiServiceTestCase {
posted.add(sbn);
mGroupHelper.onNotificationPosted(sbn, false);
}
+
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
Mockito.reset(mCallback);
@@ -693,8 +836,9 @@ public class GroupHelperTest extends UiServiceTestCase {
// < AUTOGROUP_AT_COUNT
final StatusBarNotification sbn = getSbn(pkg, 5, String.valueOf(5), UserHandle.SYSTEM);
posted.add(sbn);
- mGroupHelper.onNotificationPosted(sbn, true);
- verify(mCallback, times(1)).addAutoGroup(sbn.getKey());
+ assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue();
+ // addAutoGroup not called on sbn, because the autogrouping is expected to be done
+ // synchronously.
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
verify(mCallback).updateAutogroupSummary(anyInt(), anyString(),
@@ -703,7 +847,42 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
@EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
+ public void testAddSummary_sameIcon_sameColor_alwaysAutogroup() {
+ final String pkg = "package";
+ final Icon icon = mock(Icon.class);
+ when(icon.sameAs(icon)).thenReturn(true);
+ final int iconColor = Color.BLUE;
+ final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+ DEFAULT_VISIBILITY);
+
+ // Add notifications with same icon and color
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+ icon, iconColor);
+ mGroupHelper.onNotificationPosted(sbn, false);
+ }
+ // Check that the summary would have the same icon and color
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(attr));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+ // After auto-grouping, add new notification with the same color
+ StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+ String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
+ mGroupHelper.onNotificationPosted(sbn, true);
+
+ // Check that the summary was updated
+ //NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor);
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(attr));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
public void testAddSummary_sameIcon_sameColor() {
final String pkg = "package";
final Icon icon = mock(Icon.class);
@@ -721,7 +900,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Check that the summary would have the same icon and color
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(attr));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
@@ -761,7 +940,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Check that the summary would have the same icon and color
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(initialAttr));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
@@ -780,8 +959,9 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
+ @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
@EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
- public void testAddSummary_diffVisibility() {
+ public void testAddSummary_diffVisibility_alwaysAutogroup() {
final String pkg = "package";
final Icon icon = mock(Icon.class);
when(icon.sameAs(icon)).thenReturn(true);
@@ -798,7 +978,8 @@ public class GroupHelperTest extends UiServiceTestCase {
// Check that the summary has private visibility
verify(mCallback, times(1)).addAutoGroupSummary(
anyInt(), eq(pkg), anyString(), eq(attr));
- verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), anyBoolean());
verify(mCallback, never()).removeAutoGroup(anyString());
verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
@@ -815,6 +996,48 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+ public void testAddSummary_diffVisibility() {
+ final String pkg = "package";
+ final Icon icon = mock(Icon.class);
+ when(icon.sameAs(icon)).thenReturn(true);
+ final int iconColor = Color.BLUE;
+ final NotificationAttributes attr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+ VISIBILITY_PRIVATE);
+
+ // Add notifications with same icon and color and default visibility (private)
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+ icon, iconColor);
+ assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isFalse();
+ }
+ // The last notification added will reach the autogroup threshold.
+ StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT - 1,
+ String.valueOf(AUTOGROUP_AT_COUNT - 1), UserHandle.SYSTEM, null, icon, iconColor);
+ assertThat(mGroupHelper.onNotificationPosted(sbn, false)).isTrue();
+
+ // Check that the summary has private visibility
+ verify(mCallback, times(1)).addAutoGroupSummary(
+ anyInt(), eq(pkg), anyString(), eq(attr));
+ // The last sbn is expected to be added to autogroup synchronously.
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+ // After auto-grouping, add new notification with public visibility
+ sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+ String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, icon, iconColor);
+ sbn.getNotification().visibility = VISIBILITY_PUBLIC;
+ assertThat(mGroupHelper.onNotificationPosted(sbn, true)).isTrue();
+
+ // Check that the summary visibility was updated
+ NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, icon, iconColor,
+ VISIBILITY_PUBLIC);
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
public void testAutoGrouped_diffIcon_diffColor_removeChild_updateTo_sameIcon_sameColor() {
final String pkg = "package";
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 3a0eba11d488..5e2fe6a080eb 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -311,6 +311,7 @@ import com.android.server.wm.WindowManagerInternal;
import com.google.android.collect.Lists;
import com.google.common.collect.ImmutableList;
+
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -331,8 +332,6 @@ import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -342,12 +341,14 @@ import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@RunWithLooper
@@ -501,7 +502,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Mock
MultiRateLimiter mToastRateLimiter;
BroadcastReceiver mPackageIntentReceiver;
- BroadcastReceiver mUserSwitchIntentReceiver;
+ BroadcastReceiver mUserIntentReceiver;
BroadcastReceiver mNotificationTimeoutReceiver;
NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
@@ -800,11 +801,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
&& filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
mPackageIntentReceiver = broadcastReceivers.get(i);
}
- if (filter.hasAction(Intent.ACTION_USER_SWITCHED)) {
+ if (filter.hasAction(Intent.ACTION_USER_SWITCHED)
+ || filter.hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)
+ || filter.hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) {
// There may be multiple receivers, get the NMS one
if (broadcastReceivers.get(i).toString().contains(
NotificationManagerService.class.getName())) {
- mUserSwitchIntentReceiver = broadcastReceivers.get(i);
+ mUserIntentReceiver = broadcastReceivers.get(i);
}
}
if (filter.hasAction(ACTION_NOTIFICATION_TIMEOUT)
@@ -813,7 +816,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
}
assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
- assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver);
+ assertNotNull("User receiver should exist", mUserIntentReceiver);
if (!Flags.allNotifsNeedTtl()) {
assertNotNull("Notification timeout receiver should exist",
mNotificationTimeoutReceiver);
@@ -974,7 +977,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
private void simulateProfileAvailabilityActions(String intentAction) {
final Intent intent = new Intent(intentAction);
intent.putExtra(Intent.EXTRA_USER_HANDLE, TEST_PROFILE_USERHANDLE);
- mUserSwitchIntentReceiver.onReceive(mContext, intent);
+ mUserIntentReceiver.onReceive(mContext, intent);
}
private ArrayMap<Boolean, ArrayList<ComponentName>> generateResetComponentValues() {
@@ -5542,7 +5545,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testAddAutogroup_requestsSort() throws Exception {
final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
mService.addNotification(r);
- mService.addAutogroupKeyLocked(r.getKey());
+ mService.addAutogroupKeyLocked(r.getKey(), true);
verify(mRankingHandler, times(1)).requestSort();
}
@@ -5562,12 +5565,30 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
r.setOverrideGroupKey("TEST");
mService.addNotification(r);
- mService.addAutogroupKeyLocked(r.getKey());
+ mService.addAutogroupKeyLocked(r.getKey(), true);
+
+ verify(mRankingHandler, never()).requestSort();
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAutogroupSuppressSort_noSort() throws Exception {
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+ mService.addNotification(r);
+ mService.addAutogroupKeyLocked(r.getKey(), false);
verify(mRankingHandler, never()).requestSort();
}
@Test
+ @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+ public void testAutogroupOnPost_skipManualSort() throws Exception {
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel);
+ mService.addNotification(r);
+ verify(mRankingHandler, never()).requestSort();
+ }
+
+ @Test
public void testHandleRankingSort_sendsUpdateOnSignalExtractorChange() throws Exception {
mService.setPreferencesHelper(mPreferencesHelper);
NotificationManagerService.WorkerHandler handler = mock(
@@ -14462,13 +14483,33 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags(Flags.FLAG_USE_SSM_USER_SWITCH_SIGNAL)
public void onUserSwitched_updatesZenModeAndChannelsBypassingDnd() {
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ mService.setPreferencesHelper(mPreferencesHelper);
+
+ UserInfo prevUser = new UserInfo();
+ prevUser.id = 10;
+ UserInfo newUser = new UserInfo();
+ newUser.id = 20;
+
+ mService.onUserSwitching(new TargetUser(prevUser), new TargetUser(newUser));
+
+ InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
+ inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
+ inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_USE_SSM_USER_SWITCH_SIGNAL)
+ public void onUserSwitched_broadcast_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);
+ mUserIntentReceiver.onReceive(mContext, intent);
InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index f92387cdf5c9..a268aa912183 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -79,19 +79,19 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase {
@Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void testReturnsContinueIfDesktopWindowingIsDisabled() {
+ public void testReturnsSkipIfDesktopWindowingIsDisabled() {
setupDesktopModeLaunchParamsModifier();
- assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(null).calculate());
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate());
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- public void testReturnsContinueIfDesktopWindowingIsEnabledOnUnsupportedDevice() {
+ public void testReturnsSkipIfDesktopWindowingIsEnabledOnUnsupportedDevice() {
setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ false,
/*enforceDeviceRestrictions=*/ true);
- assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(null).calculate());
+ assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 27d9d1356d7f..44d1b5421b5b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -114,6 +114,8 @@ import android.metrics.LogMaker;
import android.os.Binder;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.flag.junit.CheckFlagsRule;
@@ -2813,6 +2815,41 @@ public class DisplayContentTests extends WindowTestsBase {
mDisplayContent.getKeepClearAreas());
}
+ @Test
+ public void testHasAccessConsidersUserVisibilityForBackgroundVisibleUsers() {
+ doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
+ final int appId = 1234;
+ final int userId1 = 11;
+ final int userId2 = 12;
+ final int uid1 = UserHandle.getUid(userId1, appId);
+ final int uid2 = UserHandle.getUid(userId2, appId);
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ final DisplayContent dc = createNewDisplay(displayInfo);
+ int displayId = dc.getDisplayId();
+ doReturn(true).when(mWm.mUmInternal).isUserVisible(userId1, displayId);
+ doReturn(false).when(mWm.mUmInternal).isUserVisible(userId2, displayId);
+
+ assertTrue(dc.hasAccess(uid1));
+ assertFalse(dc.hasAccess(uid2));
+ }
+
+ @Test
+ public void testHasAccessIgnoresUserVisibilityForPrivateDisplay() {
+ doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
+ final int appId = 1234;
+ final int userId2 = 12;
+ final int uid2 = UserHandle.getUid(userId2, appId);
+ final DisplayInfo displayInfo = new DisplayInfo(mDisplayInfo);
+ displayInfo.flags = FLAG_PRIVATE;
+ displayInfo.ownerUid = uid2;
+ final DisplayContent dc = createNewDisplay(displayInfo);
+ int displayId = dc.getDisplayId();
+
+ assertTrue(dc.hasAccess(uid2));
+
+ verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId);
+ }
+
private void removeRootTaskTests(Runnable runnable) {
final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 75b84d1c8a64..6ec1429200e6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -1373,26 +1373,6 @@ public class RecentTasksTest extends WindowTestsBase {
assertTrue(info.supportsMultiWindow);
}
- @Test
- public void testRemoveCompatibleRecentTask() {
- final Task task1 = createTaskBuilder(".Task").setWindowingMode(
- WINDOWING_MODE_FULLSCREEN).build();
- mRecentTasks.add(task1);
- final Task task2 = createTaskBuilder(".Task").setWindowingMode(
- WINDOWING_MODE_MULTI_WINDOW).build();
- mRecentTasks.add(task2);
- assertEquals(2, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
- true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
-
- // Set windowing mode and ensure the same fullscreen task that created earlier is removed.
- task2.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- mRecentTasks.removeCompatibleRecentTask(task2);
- assertEquals(1, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
- true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
- assertEquals(task2.mTaskId, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
- true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().get(0).taskId);
- }
-
private TaskSnapshot createSnapshot(Point taskSize, Point bufferSize) {
HardwareBuffer buffer = null;
if (bufferSize != null) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index a78fc10bd893..7e6301fda872 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -21,13 +21,15 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_IMPROVEMENTS;
+import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX;
import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_OWN_FOCUS;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
-import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
@@ -1020,6 +1022,35 @@ public class WindowManagerServiceTests extends WindowTestsBase {
}
@Test
+ @RequiresFlagsEnabled(
+ {FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION, FLAG_SENSITIVE_CONTENT_IMPROVEMENTS,
+ FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX})
+ public void addBlockScreenCaptureForApps_appNotInForeground_invalidateSnapshot() {
+ spyOn(mWm.mTaskSnapshotController);
+
+ // createAppWindow uses package name of "test" and uid of "0"
+ String testPackage = "test";
+ int ownerId1 = 0;
+
+ final Task task = createTask(mDisplayContent);
+ final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow");
+ mWm.mWindowMap.put(win.mClient.asBinder(), win);
+ final ActivityRecord activity = win.mActivityRecord;
+ activity.setVisibleRequested(false);
+ activity.setVisible(false);
+ win.setHasSurface(false);
+
+ PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1);
+ ArraySet<PackageInfo> blockedPackages = new ArraySet();
+ blockedPackages.add(blockedPackage);
+
+ WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
+ wmInternal.addBlockScreenCaptureForApps(blockedPackages);
+
+ verify(mWm.mTaskSnapshotController).removeAndDeleteSnapshot(anyInt(), eq(ownerId1));
+ }
+
+ @Test
public void clearBlockedApps_clearsCache() {
String testPackage = "test";
int ownerId1 = 20;
@@ -1192,20 +1223,20 @@ public class WindowManagerServiceTests extends WindowTestsBase {
final InputChannel inputChannel = new InputChannel();
mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl,
window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */,
- INPUT_FEATURE_SENSITIVE_FOR_TRACING, TYPE_APPLICATION, null /* windowToken */,
+ INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, TYPE_APPLICATION, null /* windowToken */,
inputTransferToken,
"TestInputChannel", inputChannel);
verify(mTransaction).setInputWindowInfo(
eq(surfaceControl),
- argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) == 0));
+ argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) == 0));
mWm.updateInputChannel(inputChannel.getToken(), DEFAULT_DISPLAY, surfaceControl,
FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY,
- INPUT_FEATURE_SENSITIVE_FOR_TRACING,
+ INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
null /* region */);
verify(mTransaction).setInputWindowInfo(
eq(surfaceControl),
- argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) != 0));
+ argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) != 0));
}
@RequiresFlagsDisabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER)
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 4c719dd339e4..bc8f65edaa12 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -3888,7 +3888,7 @@ public class CarrierConfigManager {
/**
* Whether device resets all of NR timers when device is in a voice call and QOS is established.
- * The default value is false;
+ * The default value is true;
*
* @see #KEY_5G_ICON_DISPLAY_GRACE_PERIOD_STRING
* @see #KEY_5G_ICON_DISPLAY_SECONDARY_GRACE_PERIOD_STRING
@@ -10909,7 +10909,7 @@ public class CarrierConfigManager {
sDefaults.putString(KEY_5G_ICON_DISPLAY_SECONDARY_GRACE_PERIOD_STRING, "");
sDefaults.putInt(KEY_NR_ADVANCED_BANDS_SECONDARY_TIMER_SECONDS_INT, 0);
sDefaults.putBoolean(KEY_NR_TIMERS_RESET_IF_NON_ENDC_AND_RRC_IDLE_BOOL, false);
- sDefaults.putBoolean(KEY_NR_TIMERS_RESET_ON_VOICE_QOS_BOOL, false);
+ sDefaults.putBoolean(KEY_NR_TIMERS_RESET_ON_VOICE_QOS_BOOL, true);
sDefaults.putBoolean(KEY_NR_TIMERS_RESET_ON_PLMN_CHANGE_BOOL, false);
/* Default value is 1 hour. */
sDefaults.putLong(KEY_5G_WATCHDOG_TIME_MS_LONG, 3600000);
diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java
index f161f319b0c2..0ecafc70b9a4 100644
--- a/telephony/java/android/telephony/PhoneNumberUtils.java
+++ b/telephony/java/android/telephony/PhoneNumberUtils.java
@@ -1283,6 +1283,8 @@ public class PhoneNumberUtils {
private static final String JAPAN_ISO_COUNTRY_CODE = "JP";
+ private static final String SINGAPORE_ISO_COUNTRY_CODE = "SG";
+
/**
* Breaks the given number down and formats it according to the rules
* for the country the number is from.
@@ -1669,6 +1671,17 @@ public class PhoneNumberUtils {
* dialing format.
*/
result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+ } else if (Flags.removeCountryCodeFromLocalSingaporeCalls() &&
+ (SINGAPORE_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
+ pn.getCountryCode() ==
+ util.getCountryCodeForRegion(SINGAPORE_ISO_COUNTRY_CODE) &&
+ (pn.getCountryCodeSource() ==
+ PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN))) {
+ /*
+ * Need to reformat Singaporean phone numbers (when the user is in Singapore)
+ * with the country code (+65) removed to comply with Singaporean regulations.
+ */
+ result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
} else {
result = util.formatInOriginalFormat(pn, defaultCountryIso);
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 25e2d828068e..03ba8faa64de 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -13787,11 +13787,11 @@ public class TelephonyManager {
* <p>This method returns valid data on devices with {@link
* android.content.pm.PackageManager#FEATURE_TELEPHONY_CARRIERLOCK} enabled.
*
- * @deprecated Apps should use {@link getCarriersRestrictionRules} to retrieve the list of
+ * @deprecated Apps should use {@link #getCarrierRestrictionRules} to retrieve the list of
* allowed and excliuded carriers, as the result of this API is valid only when the excluded
* list is empty. This API could return an empty list, even if some restrictions are present.
*
- * @return List of {@link android.telephony.CarrierIdentifier}; empty list
+ * @return List of {@link android.service.carrier.CarrierIdentifier}; empty list
* means all carriers are allowed.
*
* @throws UnsupportedOperationException If the device does not have
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
index c8b60e5c335f..441a4ae6d9b6 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java
@@ -20,6 +20,7 @@ import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS
import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY;
import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY;
+import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR;
import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.MIN_VALID_EXPECTED_RX_PACKET_NUM;
import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.getMaxSeqNumIncreasePerSecond;
import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
@@ -584,4 +585,56 @@ public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase {
MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED,
getMaxSeqNumIncreasePerSecond(mCarrierConfig));
}
+
+ private IpSecPacketLossDetector newDetectorAndSetTransform(int threshold) throws Exception {
+ when(mCarrierConfig.getInt(
+ eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
+ anyInt()))
+ .thenReturn(threshold);
+
+ final IpSecPacketLossDetector detector =
+ new IpSecPacketLossDetector(
+ mVcnContext,
+ mNetwork,
+ mCarrierConfig,
+ mMetricMonitorCallback,
+ mDependencies);
+
+ detector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
+ detector.setInboundTransformInternal(mIpSecTransform);
+
+ return detector;
+ }
+
+ @Test
+ public void testDisableAndEnableDetectorWithCarrierConfig() throws Exception {
+ final IpSecPacketLossDetector detector =
+ newDetectorAndSetTransform(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR);
+
+ assertFalse(detector.isStarted());
+
+ when(mCarrierConfig.getInt(
+ eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
+ anyInt()))
+ .thenReturn(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD);
+ detector.setCarrierConfig(mCarrierConfig);
+
+ assertTrue(detector.isStarted());
+ }
+
+ @Test
+ public void testEnableAndDisableDetectorWithCarrierConfig() throws Exception {
+ final IpSecPacketLossDetector detector =
+ newDetectorAndSetTransform(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD);
+
+ assertTrue(detector.isStarted());
+
+ when(mCarrierConfig.getInt(
+ eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
+ anyInt()))
+ .thenReturn(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR);
+ detector.setCarrierConfig(mCarrierConfig);
+
+ assertFalse(detector.isStarted());
+ }
}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index edad67896e8e..0439d5f54e23 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -123,6 +123,7 @@ public abstract class NetworkEvaluationTestBase {
mSetFlagsRule.enableFlags(Flags.FLAG_VALIDATE_NETWORK_ON_IPSEC_LOSS);
mSetFlagsRule.enableFlags(Flags.FLAG_EVALUATE_IPSEC_LOSS_ON_LP_NC_CHANGE);
mSetFlagsRule.enableFlags(Flags.FLAG_HANDLE_SEQ_NUM_LEAP);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_DISABLE_IPSEC_LOSS_DETECTOR);
when(mNetwork.getNetId()).thenReturn(-1);
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index f1e4ead03314..669cddb9af5a 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -443,7 +443,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn
manifest_action.Action(AutoGenerateIsSplitRequired);
manifest_action.Action(VerifyManifest);
manifest_action.Action(FixCoreAppAttribute);
- manifest_action.Action([&](xml::Element* el) -> bool {
+ manifest_action.Action([this, diag](xml::Element* el) -> bool {
EnsureNamespaceIsDeclared("android", xml::kSchemaAndroid, &el->namespace_decls);
if (options_.version_name_default) {
@@ -506,7 +506,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn
manifest_action["eat-comment"];
// Uses-sdk actions.
- manifest_action["uses-sdk"].Action([&](xml::Element* el) -> bool {
+ manifest_action["uses-sdk"].Action([this](xml::Element* el) -> bool {
if (options_.min_sdk_version_default &&
el->FindAttribute(xml::kSchemaAndroid, "minSdkVersion") == nullptr) {
// There was no minSdkVersion defined and we have a default to assign.
@@ -528,7 +528,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn
// Instrumentation actions.
manifest_action["instrumentation"].Action(RequiredNameIsJavaClassName);
- manifest_action["instrumentation"].Action([&](xml::Element* el) -> bool {
+ manifest_action["instrumentation"].Action([this](xml::Element* el) -> bool {
if (!options_.rename_instrumentation_target_package) {
return true;
}
@@ -544,7 +544,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn
manifest_action["attribution"];
manifest_action["attribution"]["inherit-from"];
manifest_action["original-package"];
- manifest_action["overlay"].Action([&](xml::Element* el) -> bool {
+ manifest_action["overlay"].Action([this](xml::Element* el) -> bool {
if (options_.rename_overlay_target_package) {
if (xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "targetPackage")) {
attr->value = options_.rename_overlay_target_package.value();
@@ -625,7 +625,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn
uses_package_action["additional-certificate"];
if (options_.debug_mode) {
- application_action.Action([&](xml::Element* el) -> bool {
+ application_action.Action([](xml::Element* el) -> bool {
xml::Attribute *attr = el->FindOrCreateAttribute(xml::kSchemaAndroid, "debuggable");
attr->value = "true";
return true;
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index 5659a35170c1..2e144f5513bc 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -15,11 +15,11 @@
*/
package com.android.hoststubgen.filters
-import com.android.hoststubgen.UnknownApiException
import com.android.hoststubgen.addNonNullElement
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.asm.toHumanReadableMethodName
+import com.android.hoststubgen.log
// TODO: Validate all input names.
@@ -48,30 +48,30 @@ class InMemoryOutputFilter(
return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
}
- private fun ensureClassExists(className: String) {
+ private fun checkClass(className: String) {
if (classes.findClass(className) == null) {
- throw UnknownApiException("Unknown class $className")
+ log.w("Unknown class $className")
}
}
- private fun ensureFieldExists(className: String, fieldName: String) {
+ private fun checkField(className: String, fieldName: String) {
if (classes.findField(className, fieldName) == null) {
- throw UnknownApiException("Unknown field $className.$fieldName")
+ log.w("Unknown field $className.$fieldName")
}
}
- private fun ensureMethodExists(
+ private fun checkMethod(
className: String,
methodName: String,
descriptor: String
) {
if (classes.findMethod(className, methodName, descriptor) == null) {
- throw UnknownApiException("Unknown method $className.$methodName$descriptor")
+ log.w("Unknown method $className.$methodName$descriptor")
}
}
fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) {
- ensureClassExists(className)
+ checkClass(className)
mPolicies[getClassKey(className)] = policy
}
@@ -81,7 +81,7 @@ class InMemoryOutputFilter(
}
fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
- ensureFieldExists(className, fieldName)
+ checkField(className, fieldName)
mPolicies[getFieldKey(className, fieldName)] = policy
}
@@ -100,7 +100,7 @@ class InMemoryOutputFilter(
descriptor: String,
policy: FilterPolicyWithReason,
) {
- ensureMethodExists(className, methodName, descriptor)
+ checkMethod(className, methodName, descriptor)
mPolicies[getMethodKey(className, methodName, descriptor)] = policy
}
@@ -110,8 +110,8 @@ class InMemoryOutputFilter(
}
fun setRenameTo(className: String, methodName: String, descriptor: String, toName: String) {
- ensureMethodExists(className, methodName, descriptor)
- ensureMethodExists(className, toName, descriptor)
+ checkMethod(className, methodName, descriptor)
+ checkMethod(className, toName, descriptor)
mRenames[getMethodKey(className, methodName, descriptor)] = toName
}
@@ -121,7 +121,7 @@ class InMemoryOutputFilter(
}
fun setNativeSubstitutionClass(from: String, to: String) {
- ensureClassExists(from)
+ checkClass(from)
// Native substitute classes may be provided from other jars, so we can't do this check.
// ensureClassExists(to)