summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/aconfig/job.aconfig10
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java6
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java16
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java3
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java32
-rw-r--r--cmds/bootanimation/FORMAT.md2
-rw-r--r--core/java/android/app/ActivityTaskManager.java7
-rw-r--r--core/java/android/app/Notification.java12
-rw-r--r--core/java/android/app/notification.aconfig7
-rw-r--r--core/java/android/app/usage/flags.aconfig11
-rw-r--r--core/java/android/content/pm/flags.aconfig12
-rw-r--r--core/java/android/content/pm/multiuser.aconfig7
-rw-r--r--core/java/android/net/vcn/flags.aconfig10
-rw-r--r--core/java/android/view/ImeBackAnimationController.java22
-rw-r--r--core/java/android/view/InsetsController.java7
-rw-r--r--core/java/android/view/SurfaceView.java2
-rw-r--r--core/java/android/view/ViewRootImpl.java68
-rw-r--r--core/java/android/view/animation/AnimationSet.java2
-rw-r--r--core/java/android/view/animation/Transformation.java1
-rw-r--r--core/java/android/webkit/WebChromeClient.java22
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/java/android/window/flags/responsible_apis.aconfig7
-rw-r--r--core/jni/com_android_internal_os_Zygote.cpp14
-rw-r--r--core/res/AndroidManifest.xml1
-rw-r--r--core/res/res/drawable/ic_signal_cellular_1_4_bar.xml10
-rw-r--r--core/res/res/drawable/ic_signal_cellular_1_5_bar.xml10
-rw-r--r--core/res/res/drawable/ic_signal_cellular_2_4_bar.xml10
-rw-r--r--core/res/res/drawable/ic_signal_cellular_2_5_bar.xml10
-rw-r--r--core/res/res/drawable/ic_signal_cellular_3_4_bar.xml10
-rw-r--r--core/res/res/drawable/ic_signal_cellular_3_5_bar.xml10
-rw-r--r--core/res/res/drawable/ic_signal_cellular_4_5_bar.xml10
-rw-r--r--core/res/res/values/config.xml15
-rw-r--r--core/res/res/values/strings.xml19
-rw-r--r--core/res/res/values/symbols.xml13
-rw-r--r--core/tests/coretests/src/android/view/ViewFrameRateTest.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java43
-rw-r--r--libs/input/PointerController.cpp109
-rw-r--r--libs/input/PointerController.h18
-rw-r--r--libs/input/PointerControllerContext.h1
-rw-r--r--libs/input/SpriteController.h30
-rw-r--r--libs/input/tests/PointerController_test.cpp61
-rw-r--r--media/java/android/media/MediaRouter2.java128
-rw-r--r--nfc/java/android/nfc/cardemulation/CardEmulation.java6
-rw-r--r--packages/SystemUI/Android.bp2
-rw-r--r--packages/SystemUI/AndroidManifest.xml2
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt1
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt86
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt1
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt14
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt65
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt26
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt25
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt19
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt28
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt228
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt219
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt138
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt66
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt306
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt165
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt198
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt507
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt82
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt20
-rw-r--r--packages/SystemUI/res-keyguard/values/styles.xml1
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java67
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt72
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt154
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt112
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt58
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt102
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt69
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt86
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt (renamed from packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt)31
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt364
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt154
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt68
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java50
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java39
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt52
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt121
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt33
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt60
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt132
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt130
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt58
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt35
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt61
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt21
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java52
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt28
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt60
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt56
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt)2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt34
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt39
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt69
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt6
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java5
-rw-r--r--services/companion/java/com/android/server/companion/virtual/InputController.java64
-rw-r--r--services/core/java/com/android/server/VcnManagementService.java20
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java126
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java1
-rw-r--r--services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java93
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java144
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java26
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java10
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java6
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java10
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java6
-rw-r--r--services/core/java/com/android/server/input/InputManagerInternal.java23
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java213
-rw-r--r--services/core/java/com/android/server/input/NativeInputManagerService.java10
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java8
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubService.java207
-rw-r--r--services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java41
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider.java18
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java14
-rw-r--r--services/core/java/com/android/server/pm/DexOptHelper.java8
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerShellCommand.java104
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java13
-rw-r--r--services/core/java/com/android/server/pm/VerifyingSession.java12
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java3
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java156
-rw-r--r--services/core/java/com/android/server/wm/LetterboxConfiguration.java30
-rw-r--r--services/core/java/com/android/server/wm/LetterboxUiController.java69
-rw-r--r--services/core/java/com/android/server/wm/OWNERS1
-rw-r--r--services/core/java/com/android/server/wm/Task.java111
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java18
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java4
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp168
-rw-r--r--services/java/com/android/server/SystemServer.java38
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java12
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java111
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java605
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java43
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java132
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java19
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java16
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java2
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java124
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java9
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java55
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java95
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java33
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java10
-rw-r--r--services/usage/java/com/android/server/usage/UsageStatsService.java7
-rw-r--r--services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java18
-rw-r--r--tests/Input/src/com/android/server/input/InputManagerServiceTests.kt215
-rw-r--r--tests/UsbManagerTests/Android.bp3
-rw-r--r--tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java165
-rw-r--r--tests/vcn/java/com/android/server/VcnManagementServiceTest.java37
-rw-r--r--tools/hoststubgen/TEST_MAPPING60
-rw-r--r--wifi/wifi.aconfig8
263 files changed, 8319 insertions, 2485 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index e20f525fcdaf..e489c1ad891a 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -38,3 +38,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "thermal_restrictions_to_fgs_jobs"
+ namespace: "backstage_power"
+ description: "Apply thermal restrictions to FGS jobs."
+ bug: "315157163"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 3bb395f39123..ba8e3e8b48fc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1375,8 +1375,10 @@ class JobConcurrencyManager {
final JobServiceContext jsc = mActiveServices.get(i);
final JobStatus jobStatus = jsc.getRunningJobLocked();
- if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
- && restriction.isJobRestricted(jobStatus)) {
+ if (jobStatus != null
+ && !jsc.isWithinExecutionGuaranteeTime()
+ && restriction.isJobRestricted(
+ jobStatus, mService.evaluateJobBiasLocked(jobStatus))) {
jsc.cancelExecutingJobLocked(restriction.getStopReason(),
restriction.getInternalReason(),
JobParameters.getInternalReasonCodeDescription(
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 5d1433c815d6..384d78618c8b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -310,7 +310,8 @@ public class JobSchedulerService extends com.android.server.SystemService
* Note: do not add to or remove from this list at runtime except in the constructor, because we
* do not synchronize access to this list.
*/
- private final List<JobRestriction> mJobRestrictions;
+ @VisibleForTesting
+ final List<JobRestriction> mJobRestrictions;
@GuardedBy("mLock")
@VisibleForTesting
@@ -3498,8 +3499,6 @@ public class JobSchedulerService extends com.android.server.SystemService
/**
* Check if a job is restricted by any of the declared {@link JobRestriction JobRestrictions}.
- * Note, that the jobs with {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher may not
- * be restricted, thus we won't even perform the check, but simply return null early.
*
* @param job to be checked
* @return the first {@link JobRestriction} restricting the given job that has been found; null
@@ -3508,13 +3507,9 @@ public class JobSchedulerService extends com.android.server.SystemService
*/
@GuardedBy("mLock")
JobRestriction checkIfRestricted(JobStatus job) {
- if (evaluateJobBiasLocked(job) >= JobInfo.BIAS_FOREGROUND_SERVICE) {
- // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted
- return null;
- }
for (int i = mJobRestrictions.size() - 1; i >= 0; i--) {
final JobRestriction restriction = mJobRestrictions.get(i);
- if (restriction.isJobRestricted(job)) {
+ if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) {
return restriction;
}
}
@@ -4221,6 +4216,7 @@ public class JobSchedulerService extends com.android.server.SystemService
return curBias;
}
+ /** Gets and returns the adjusted Job Bias **/
int evaluateJobBiasLocked(JobStatus job) {
int bias = job.getBias();
if (bias >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) {
@@ -5907,7 +5903,7 @@ public class JobSchedulerService extends com.android.server.SystemService
if (isRestricted) {
for (int i = mJobRestrictions.size() - 1; i >= 0; i--) {
final JobRestriction restriction = mJobRestrictions.get(i);
- if (restriction.isJobRestricted(job)) {
+ if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) {
final int reason = restriction.getInternalReason();
pw.print(" ");
pw.print(JobParameters.getInternalReasonCodeDescription(reason));
@@ -6240,7 +6236,7 @@ public class JobSchedulerService extends com.android.server.SystemService
proto.write(JobSchedulerServiceDumpProto.JobRestriction.REASON,
restriction.getInternalReason());
proto.write(JobSchedulerServiceDumpProto.JobRestriction.IS_RESTRICTING,
- restriction.isJobRestricted(job));
+ restriction.isJobRestricted(job, evaluateJobBiasLocked(job)));
proto.end(restrictionsToken);
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
index 7aab67a00b1d..555a1186e0c9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -62,10 +62,11 @@ public abstract class JobRestriction {
* fine with it).
*
* @param job to be checked
+ * @param bias job bias to be checked
* @return false if the {@link JobSchedulerService} should not schedule this job at the moment,
* true - otherwise
*/
- public abstract boolean isJobRestricted(JobStatus job);
+ public abstract boolean isJobRestricted(JobStatus job, int bias);
/** Dump any internal constants the Restriction may have. */
public abstract void dumpConstants(IndentingPrintWriter pw);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index ef634b565b65..ba0111349bc9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -24,6 +24,7 @@ import android.os.PowerManager.OnThermalStatusChangedListener;
import android.util.IndentingPrintWriter;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.job.Flags;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.controllers.JobStatus;
@@ -85,7 +86,18 @@ public class ThermalStatusRestriction extends JobRestriction {
}
@Override
- public boolean isJobRestricted(JobStatus job) {
+ public boolean isJobRestricted(JobStatus job, int bias) {
+ if (Flags.thermalRestrictionsToFgsJobs()) {
+ if (bias >= JobInfo.BIAS_TOP_APP) {
+ // Jobs with BIAS_TOP_APP should not be restricted
+ return false;
+ }
+ } else {
+ if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) {
+ // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted
+ return false;
+ }
+ }
if (mThermalStatus >= UPPER_THRESHOLD) {
return true;
}
@@ -107,6 +119,17 @@ public class ThermalStatusRestriction extends JobRestriction {
|| (mService.isCurrentlyRunningLocked(job)
&& mService.isJobInOvertimeLocked(job));
}
+ if (Flags.thermalRestrictionsToFgsJobs()) {
+ // Only let foreground jobs run if:
+ // 1. They haven't previously run
+ // 2. They're already running and aren't yet in overtime
+ if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE
+ && job.getJob().isImportantWhileForeground()) {
+ return job.getNumPreviousAttempts() > 0
+ || (mService.isCurrentlyRunningLocked(job)
+ && mService.isJobInOvertimeLocked(job));
+ }
+ }
if (priority == JobInfo.PRIORITY_HIGH) {
return !mService.isCurrentlyRunningLocked(job)
|| mService.isJobInOvertimeLocked(job);
@@ -114,6 +137,13 @@ public class ThermalStatusRestriction extends JobRestriction {
return true;
}
if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) {
+ if (Flags.thermalRestrictionsToFgsJobs()) {
+ if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) {
+ // No restrictions on foreground jobs
+ // on LOW_PRIORITY_THRESHOLD and below
+ return false;
+ }
+ }
// For light throttling, throttle all min priority jobs and all low priority jobs that
// aren't already running or have been running for long enough.
return priority == JobInfo.PRIORITY_MIN
diff --git a/cmds/bootanimation/FORMAT.md b/cmds/bootanimation/FORMAT.md
index 01e8fe13fdf6..da8331af1492 100644
--- a/cmds/bootanimation/FORMAT.md
+++ b/cmds/bootanimation/FORMAT.md
@@ -126,7 +126,7 @@ the system property `service.bootanim.exit` to a nonzero string.)
Use `zopflipng` if you have it, otherwise `pngcrush` will do. e.g.:
for fn in *.png ; do
- zopflipng -m ${fn}s ${fn}s.new && mv -f ${fn}s.new ${fn}
+ zopflipng -m ${fn} ${fn}.new && mv -f ${fn}.new ${fn}
# or: pngcrush -q ....
done
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index be8f48df4aa6..c8ab260c8c4e 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -90,13 +90,6 @@ public class ActivityTaskManager {
public static final int RESIZE_MODE_USER = RESIZE_MODE_PRESERVE_WINDOW;
/**
- * Input parameter to {@link IActivityTaskManager#resizeTask} used by window
- * manager during a screen rotation.
- * @hide
- */
- public static final int RESIZE_MODE_SYSTEM_SCREEN_ROTATION = RESIZE_MODE_PRESERVE_WINDOW;
-
- /**
* Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
* that the resize should be performed even if the bounds appears unchanged.
* @hide
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c3bac710f9be..4ac6bac3a2e5 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6490,7 +6490,8 @@ public class Notification implements Parcelable
// visual regressions.
@SuppressWarnings("AndroidFrameworkCompatChange")
private boolean bigContentViewRequired() {
- if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
+ if (!Flags.notificationExpansionOptional()
+ && mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
return true;
}
// Notifications with contentView and without a bigContentView, style, or actions would
@@ -6593,12 +6594,7 @@ public class Notification implements Parcelable
* @hide
*/
public RemoteViews createCompactHeadsUpContentView() {
- // TODO(b/336225281): re-evaluate custom view usage.
- if (useExistingRemoteView(mN.headsUpContentView)) {
- return fullyCustomViewRequiresDecoration(false /* fromStyle */)
- ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView)
- : mN.headsUpContentView;
- } else if (mStyle != null) {
+ if (mStyle != null) {
final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView();
if (styleView != null) {
return styleView;
@@ -6611,7 +6607,7 @@ public class Notification implements Parcelable
// Notification text is shown as secondary header text
// for the minimal hun when it is provided.
// Time(when and chronometer) is not shown for the minimal hun.
- p.headerTextSecondary(p.mText).text(null).hideTime(true);
+ p.headerTextSecondary(p.mText).text(null).hideTime(true).summaryText("");
return applyStandardTemplate(
getCompactHeadsUpBaseLayoutResource(), p,
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 63ffaa0f0630..50c7b7f9798e 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -60,6 +60,13 @@ flag {
}
flag {
+ name: "notification_expansion_optional"
+ namespace: "systemui"
+ description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions."
+ bug: "339523906"
+}
+
+flag {
name: "keyguard_private_notifications"
namespace: "systemui"
description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()"
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index c7b168aaf81d..04c36867271c 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -47,3 +47,14 @@ flag {
description: "Feature flag for collecting app data size by file type API"
bug: "294088945"
}
+
+flag {
+ name: "disable_idle_check"
+ namespace: "backstage_power"
+ description: "disable idle check for USER_SYSTEM during boot up"
+ is_fixed_read_only: true
+ bug: "337864590"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 45591d79ee00..cee8d96fb0d7 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -137,6 +137,18 @@ flag {
}
flag {
+ name: "get_package_storage_stats"
+ namespace: "system_performance"
+ is_exported: true
+ description: "Add dumpsys entry point for package StorageStats"
+ bug: "332905331"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "provide_info_of_apk_in_apex"
is_exported: true
namespace: "package_manager_service"
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index cd1913bb26d9..83742eb7ae84 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -240,3 +240,10 @@ flag {
description: "Add entrypoint in Settings Reset options for deleting private space when lock is forgotten"
bug: "329601751"
}
+
+flag {
+ name: "allow_main_user_to_access_blocked_number_provider"
+ namespace: "multiuser"
+ description: "Allow MAIN user to access blocked number provider"
+ bug: "338579331"
+}
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 9fe0befb4f97..d4d1ed22dd4e 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -38,6 +38,16 @@ flag{
}
flag{
+ name: "enforce_main_user"
+ namespace: "vcn"
+ description: "Enforce main user to make VCN HSUM compatible"
+ bug: "310310661"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag{
name: "handle_seq_num_leap"
namespace: "vcn"
description: "Do not report bad network when there is a suspected sequence number leap"
diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java
index 4530157d2fe1..9503f4925e14 100644
--- a/core/java/android/view/ImeBackAnimationController.java
+++ b/core/java/android/view/ImeBackAnimationController.java
@@ -38,6 +38,8 @@ import android.window.OnBackAnimationCallback;
import com.android.internal.inputmethod.SoftInputShowHideReason;
+import java.io.PrintWriter;
+
/**
* Controller for IME predictive back animation
*
@@ -271,4 +273,24 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {
return mPostCommitAnimator != null && mTriggerBack;
}
+ /**
+ * Dump information about this ImeBackAnimationController
+ *
+ * @param prefix the prefix that will be prepended to each line of the produced output
+ * @param writer the writer that will receive the resulting text
+ */
+ public void dump(String prefix, PrintWriter writer) {
+ final String innerPrefix = prefix + " ";
+ writer.println(prefix + "ImeBackAnimationController:");
+ writer.println(innerPrefix + "mLastProgress=" + mLastProgress);
+ writer.println(innerPrefix + "mTriggerBack=" + mTriggerBack);
+ writer.println(innerPrefix + "mIsPreCommitAnimationInProgress="
+ + mIsPreCommitAnimationInProgress);
+ writer.println(innerPrefix + "mStartRootScrollY=" + mStartRootScrollY);
+ writer.println(innerPrefix + "isBackAnimationAllowed=" + isBackAnimationAllowed());
+ writer.println(innerPrefix + "isAdjustPan=" + isAdjustPan());
+ writer.println(innerPrefix + "isHideAnimationInProgress="
+ + isHideAnimationInProgress());
+ }
+
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index c54526747c5c..f1cb4103f008 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1799,8 +1799,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
}
void dump(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.println("InsetsController:");
- mState.dump(prefix + " ", pw);
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + "InsetsController:");
+ mState.dump(innerPrefix, pw);
+ pw.println(innerPrefix + "mIsPredictiveBackImeHideAnimInProgress="
+ + mIsPredictiveBackImeHideAnimInProgress);
}
void dumpDebug(ProtoOutputStream proto, long fieldId) {
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index a23df799da59..1d70d18ac4c8 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -986,7 +986,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
updateBackgroundVisibility(surfaceUpdateTransaction);
updateBackgroundColor(surfaceUpdateTransaction);
- if (mLimitedHdrEnabled && hdrHeadroomChanged) {
+ if (mLimitedHdrEnabled && (hdrHeadroomChanged || creating)) {
surfaceUpdateTransaction.setDesiredHdrHeadroom(
mBlastSurfaceControl, mHdrHeadroom);
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index db55a9d03c9f..6de50f4e6fdf 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -423,6 +423,12 @@ public final class ViewRootImpl implements ViewParent,
private static final long NANOS_PER_SEC = 1000000000;
+ // If the ViewRootImpl has been idle for more than 200ms, clear the preferred
+ // frame rate category and frame rate.
+ private static final int IDLE_TIME_MILLIS = 250;
+
+ private static final long NANOS_PER_MILLI = 1_000_000;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
@@ -655,6 +661,8 @@ public final class ViewRootImpl implements ViewParent,
private int mMinusOneFrameIntervalMillis = 0;
// VRR interval between the previous and the frame before
private int mMinusTwoFrameIntervalMillis = 0;
+ // VRR has the invalidation idle message been posted?
+ private boolean mInvalidationIdleMessagePosted = false;
/**
* Update the Choreographer's FrameInfo object with the timing information for the current
@@ -4266,6 +4274,10 @@ public final class ViewRootImpl implements ViewParent,
// when the values are applicable.
if (mDrawnThisFrame) {
mDrawnThisFrame = false;
+ if (!mInvalidationIdleMessagePosted) {
+ mInvalidationIdleMessagePosted = true;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS);
+ }
setCategoryFromCategoryCounts();
updateInfrequentCount();
setPreferredFrameRate(mPreferredFrameRate);
@@ -5300,10 +5312,12 @@ public final class ViewRootImpl implements ViewParent,
if (DEBUG_CONTENT_CAPTURE) {
Log.v(mTag, "performContentCaptureInitialReport() on " + rootView);
}
+ boolean traceDispatchCapture = false;
try {
if (!isContentCaptureEnabled()) return;
- if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ traceDispatchCapture = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+ if (traceDispatchCapture) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchContentCapture() for "
+ getClass().getSimpleName());
}
@@ -5319,7 +5333,9 @@ public final class ViewRootImpl implements ViewParent,
// Content capture is a go!
rootView.dispatchInitialProvideContentCaptureStructure();
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ if (traceDispatchCapture) {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
}
}
@@ -5327,10 +5343,12 @@ public final class ViewRootImpl implements ViewParent,
if (DEBUG_CONTENT_CAPTURE) {
Log.v(mTag, "handleContentCaptureFlush()");
}
+ boolean traceFlushContentCapture = false;
try {
if (!isContentCaptureEnabled()) return;
- if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ traceFlushContentCapture = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+ if (traceFlushContentCapture) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "flushContentCapture for "
+ getClass().getSimpleName());
}
@@ -5342,7 +5360,9 @@ public final class ViewRootImpl implements ViewParent,
}
ccm.flush(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ if (traceFlushContentCapture) {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
}
}
@@ -6499,6 +6519,8 @@ public final class ViewRootImpl implements ViewParent,
return "MSG_WINDOW_TOUCH_MODE_CHANGED";
case MSG_KEEP_CLEAR_RECTS_CHANGED:
return "MSG_KEEP_CLEAR_RECTS_CHANGED";
+ case MSG_CHECK_INVALIDATION_IDLE:
+ return "MSG_CHECK_INVALIDATION_IDLE";
case MSG_REFRESH_POINTER_ICON:
return "MSG_REFRESH_POINTER_ICON";
case MSG_TOUCH_BOOST_TIMEOUT:
@@ -6763,6 +6785,30 @@ public final class ViewRootImpl implements ViewParent,
mNumPausedForSync = 0;
scheduleTraversals();
break;
+ case MSG_CHECK_INVALIDATION_IDLE: {
+ long delta;
+ if (mIsTouchBoosting || mIsFrameRateBoosting || mInsetsAnimationRunning) {
+ delta = 0;
+ } else {
+ delta = System.nanoTime() / NANOS_PER_MILLI - mLastUpdateTimeMillis;
+ }
+ if (delta >= IDLE_TIME_MILLIS) {
+ mFrameRateCategoryHighCount = 0;
+ mFrameRateCategoryHighHintCount = 0;
+ mFrameRateCategoryNormalCount = 0;
+ mFrameRateCategoryLowCount = 0;
+ mPreferredFrameRate = 0;
+ mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+ setPreferredFrameRateCategory(FRAME_RATE_CATEGORY_NO_PREFERENCE);
+ setPreferredFrameRate(0f);
+ mInvalidationIdleMessagePosted = false;
+ } else {
+ mInvalidationIdleMessagePosted = true;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE,
+ IDLE_TIME_MILLIS - delta);
+ }
+ break;
+ }
case MSG_TOUCH_BOOST_TIMEOUT:
/**
* Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
@@ -9601,6 +9647,8 @@ public final class ViewRootImpl implements ViewParent,
mOnBackInvokedDispatcher.dump(prefix, writer);
+ mImeBackAnimationController.dump(prefix, writer);
+
writer.println(prefix + "View Hierarchy:");
dumpViewHierarchy(innerPrefix, writer, mView);
}
@@ -12657,10 +12705,12 @@ public final class ViewRootImpl implements ViewParent,
view = mFrameRateCategoryView;
}
+ boolean traceFrameRateCategory = false;
try {
if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT
&& mLastPreferredFrameRateCategory != frameRateCategory) {
- if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ traceFrameRateCategory = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+ if (traceFrameRateCategory) {
String reason = reasonToString(frameRateReason);
String sourceView = view == null ? "-" : view;
String category = categoryToString(frameRateCategory);
@@ -12678,7 +12728,9 @@ public final class ViewRootImpl implements ViewParent,
} catch (Exception e) {
Log.e(mTag, "Unable to set frame rate category", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ if (traceFrameRateCategory) {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
}
}
@@ -13002,6 +13054,10 @@ public final class ViewRootImpl implements ViewParent,
private void removeVrrMessages() {
mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT);
mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+ if (mInvalidationIdleMessagePosted) {
+ mInvalidationIdleMessagePosted = false;
+ mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE);
+ }
}
/**
diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java
index a2f3544c70ab..5aaa994f3f8f 100644
--- a/core/java/android/view/animation/AnimationSet.java
+++ b/core/java/android/view/animation/AnimationSet.java
@@ -374,7 +374,7 @@ public class AnimationSet extends Animation {
final Animation a = mAnimations.get(i);
temp.clear();
- a.getTransformationAt(interpolatedTime, t);
+ a.getTransformationAt(interpolatedTime, temp);
t.compose(temp);
}
}
diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java
index de31667c96cb..812ecd158b18 100644
--- a/core/java/android/view/animation/Transformation.java
+++ b/core/java/android/view/animation/Transformation.java
@@ -78,6 +78,7 @@ public class Transformation {
mHasClipRect = false;
mAlpha = 1.0f;
mTransformationType = TYPE_BOTH;
+ mInsets = Insets.NONE;
}
/**
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index a07141b260ee..b7ee0b8a238a 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -520,6 +520,13 @@ public class WebChromeClient {
* To cancel the request, call <code>filePathCallback.onReceiveValue(null)</code> and
* return {@code true}.
*
+ * <p class="note"><b>Note:</b> WebView does not enforce any restrictions on
+ * the chosen file(s). WebView can access all files that your app can access.
+ * In case the file(s) are chosen through an untrusted source such as a third-party
+ * app, it is your own app's responsibility to check what the returned Uris
+ * refer to before calling the <code>filePathCallback</code>. See
+ * {@link #createIntent} and {@link #parseResult} for more details.</p>
+ *
* @param webView The WebView instance that is initiating the request.
* @param filePathCallback Invoke this callback to supply the list of paths to files to upload,
* or {@code null} to cancel. Must only be called if the
@@ -556,6 +563,15 @@ public class WebChromeClient {
* Parse the result returned by the file picker activity. This method should be used with
* {@link #createIntent}. Refer to {@link #createIntent} for how to use it.
*
+ * <p class="note"><b>Note:</b> The intent returned by the file picker activity
+ * should be treated as untrusted. A third-party app handling the implicit
+ * intent created by {@link #createIntent} might return Uris that the third-party
+ * app itself does not have access to, such as your own app's sensitive data files.
+ * WebView does not enforce any restrictions on the returned Uris. It is the
+ * app's responsibility to ensure that the untrusted source (such as a third-party
+ * app) has access the Uris it has returned and that the Uris are not pointing
+ * to any sensitive data files.</p>
+ *
* @param resultCode the integer result code returned by the file picker activity.
* @param data the intent returned by the file picker activity.
* @return the Uris of selected file(s) or {@code null} if the resultCode indicates
@@ -618,6 +634,12 @@ public class WebChromeClient {
* WebChromeClient#onShowFileChooser}</li>
* </ol>
*
+ * <p class="note"><b>Note:</b> The created intent may be handled by
+ * third-party applications on device. The received result must be treated
+ * as untrusted as it can contain Uris pointing to your own app's sensitive
+ * data files. Your app should check the resultant Uris in {@link #parseResult}
+ * before calling the <code>filePathCallback</code>.</p>
+ *
* @return an Intent that supports basic file chooser sources.
*/
public abstract Intent createIntent();
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index e8b4f0bec6a8..760c9166959c 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -71,3 +71,10 @@ flag {
description: "Enables quick switch for desktop mode"
bug: "338066529"
}
+
+flag {
+ name: "enable_additional_windows_above_status_bar"
+ namespace: "lse_desktop_experience"
+ description: "Allows for additional windows tied to WindowDecoration to be layered between status bar and notification shade."
+ bug: "316186265"
+}
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 33af48636f02..69cac6f3dfa5 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -49,3 +49,10 @@ flag {
description: "Prevent BAL based on it is bound by foreground Uid but the app switch is stopped."
bug: "283801068"
}
+
+flag {
+ name: "bal_improved_metrics"
+ namespace: "responsible_apis"
+ description: "Improved metrics."
+ bug: "339245692"
+}
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 12d62ccf97b6..062fab38656d 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -116,7 +116,7 @@ using android::base::GetBoolProperty;
using android::zygote::ZygoteFailure;
-using Action = android_mallopt_gwp_asan_options_t::Action;
+using Mode = android_mallopt_gwp_asan_options_t::Mode;
// This type is duplicated in fd_utils.h
typedef const std::function<void(std::string)>& fail_fn_t;
@@ -2101,21 +2101,21 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids,
switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) {
default:
case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT:
- gwp_asan_options.desire = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true)
- ? Action::TURN_ON_FOR_APP_SAMPLED_NON_CRASHING
- : Action::DONT_TURN_ON_UNLESS_OVERRIDDEN;
+ gwp_asan_options.mode = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true)
+ ? Mode::APP_MANIFEST_DEFAULT
+ : Mode::APP_MANIFEST_NEVER;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
case RuntimeFlags::GWP_ASAN_LEVEL_NEVER:
- gwp_asan_options.desire = Action::DONT_TURN_ON_UNLESS_OVERRIDDEN;
+ gwp_asan_options.mode = Mode::APP_MANIFEST_NEVER;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS:
- gwp_asan_options.desire = Action::TURN_ON_FOR_APP;
+ gwp_asan_options.mode = Mode::APP_MANIFEST_ALWAYS;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY:
- gwp_asan_options.desire = Action::TURN_ON_WITH_SAMPLING;
+ gwp_asan_options.mode = Mode::APP_MANIFEST_DEFAULT;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index bfbfb3aa3d56..70d923b8a9bb 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8771,6 +8771,7 @@
<service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:enabled="@bool/config_enableContextSyncInCall"
android:exported="true">
<meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
android:value="true" />
diff --git a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
index c0fe536cf85b..7c45c20a758b 100644
--- a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 11 13 L 2 22 L 11 22 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 1 bar. move to higher ground. -->
+ <path
+ android:name="ic_signal_cellular_1_4_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H11 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
index 816da22cf7dc..02b646d310e5 100644
--- a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M8.72,15.28,2,22H8.72V15.28Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 1 bar. might have to call you back. -->
+ <path
+ android:name="ic_signal_cellular_1_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H12 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
index 69a966ba4fc9..514d1690abcf 100644
--- a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 13 11 L 2 22 L 13 22 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 2 bars. 2 out of 4 ain't bad. -->
+ <path
+ android:name="ic_signal_cellular_2_4_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H14 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
index 02c7a430cece..a97f771a6632 100644
--- a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
@@ -23,7 +23,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 11.45 12.55 L 2 22 L 11.45 22 L 11.45 12.55 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 2 bars. hanging in there. -->
+ <path
+ android:name="ic_signal_cellular_2_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H14 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
index 46ce47cdfcda..1bacf4ad678f 100644
--- a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 2 22 L 16 22 L 16 21 L 16 20 L 16 11 L 16 10 L 16 8 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 3 bars. quite nice. -->
+ <path
+ android:name="ic_signal_cellular_3_4_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H17 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
index 37435e6b75e2..2789d3e9305c 100644
--- a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 14.96 9.04 L 2 22 L 14.96 22 L 14.96 9.04 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 3 bars. not great, not terrible. -->
+ <path
+ android:name="ic_signal_cellular_3_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H16 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
index 6dc3646d89e3..8286dbb5576f 100644
--- a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 18.48 5.52 L 2 22 L 18.48 22 L 18.48 5.52 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 4 bars. extremely respectable. -->
+ <path
+ android:name="ic_signal_cellular_4_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H18 V20 H6 z" />
+ </clip-path>
</vector> \ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 877d11e0a09a..cefc648e45a5 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6093,6 +6093,18 @@
<!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
<bool name="config_letterboxIsEducationEnabled">false</bool>
+ <!-- The width in dp to use to detect vertical thin letterboxing.
+ If W is the available width and w is the letterbox width, an app
+ is thin letterboxed if the value here is < (W - w) / 2
+ If the value is < 0 the thin letterboxing policy is disabled -->
+ <dimen name="config_letterboxThinLetterboxWidthDp">-1dp</dimen>
+
+ <!-- The height in dp to use to detect horizontal thin letterboxing
+ If H is the available height and h is the letterbox height, an app
+ is thin letterboxed if the value here is < (H - h) / 2
+ If the value is < 0 the thin letterboxing policy is disabled -->
+ <dimen name="config_letterboxThinLetterboxHeightDp">-1dp</dimen>
+
<!-- Default min aspect ratio for unresizable apps which are eligible for size compat mode.
Values <= 1.0 will be ignored. Activity min/max aspect ratio restrictions will still be
espected so this override can control the maximum screen area that can be occupied by
@@ -7039,6 +7051,9 @@
event gets ignored. -->
<integer name="config_defaultMinEmergencyGestureTapDurationMillis">200</integer>
+ <!-- Control whether to enable CallMetadataSyncInCallService. -->
+ <bool name="config_enableContextSyncInCall">false</bool>
+
<!-- Whether the system uses auto-suspend mode. -->
<bool name="config_useAutoSuspend">true</bool>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 1fca4f859294..59e4161b2e0b 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6484,4 +6484,23 @@ ul.</string>
<string name="satellite_notification_how_it_works">How it works</string>
<!-- Initial/System provided label shown for an app which gets unarchived. [CHAR LIMIT=64]. -->
<string name="unarchival_session_app_label">Pending...</string>
+
+ <!-- Fingerprint dangling notification title -->
+ <string name="fingerprint_dangling_notification_title">Set up Fingerprint Unlock again</string>
+ <!-- Fingerprint dangling notification content for only 1 fingerprint deleted -->
+ <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted to improve performance</string>
+ <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted -->
+ <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted to improve performance</string>
+ <!-- Fingerprint dangling notification content for only 1 fingerprint deleted and no fingerprint left-->
+ <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted. Set it up again to unlock your phone with fingerprint.</string>
+ <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted and no fingerprint left -->
+ <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted. Set them up again to unlock your phone with your fingerprint.</string>
+ <!-- Face dangling notification title -->
+ <string name="face_dangling_notification_title">Set up Face Unlock again</string>
+ <!-- Face dangling notification content -->
+ <string name="face_dangling_notification_msg">Your face model wasn\'t working well and was deleted. Set it up again to unlock your phone with face.</string>
+ <!-- Biometric dangling notification "set up" action button -->
+ <string name="biometric_dangling_notification_action_set_up">Set up</string>
+ <!-- Biometric dangling notification "Not now" action button -->
+ <string name="biometric_dangling_notification_action_not_now">Not now</string>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 54dbc48c99fb..d058fb188d16 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4717,6 +4717,8 @@
<java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
<java-symbol type="bool" name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled" />
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
+ <java-symbol type="dimen" name="config_letterboxThinLetterboxWidthDp" />
+ <java-symbol type="dimen" name="config_letterboxThinLetterboxHeightDp" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
<java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" />
@@ -5430,4 +5432,15 @@
<!-- For PowerManagerService to determine whether to use auto-suspend mode -->
<java-symbol type="bool" name="config_useAutoSuspend" />
+
+ <!-- Biometric dangling notification strings -->
+ <java-symbol type="string" name="fingerprint_dangling_notification_title" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_1" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_2" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_1" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_2" />
+ <java-symbol type="string" name="face_dangling_notification_title" />
+ <java-symbol type="string" name="face_dangling_notification_msg" />
+ <java-symbol type="string" name="biometric_dangling_notification_action_set_up" />
+ <java-symbol type="string" name="biometric_dangling_notification_action_not_now" />
</resources>
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 0b1b40c8ba8b..bc0ae9f31904 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -623,6 +623,28 @@ public class ViewFrameRateTest {
assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory());
}
+ @Test
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY
+ })
+ public void idleDetected() throws Throwable {
+ waitForFrameRateCategoryToSettle();
+ mActivityRule.runOnUiThread(() -> {
+ mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
+ mMovingView.setFrameContentVelocity(Float.MAX_VALUE);
+ mMovingView.invalidate();
+ runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
+ mViewRoot.getLastPreferredFrameRateCategory()));
+ });
+ waitForAfterDraw();
+
+ // Wait for idle timeout
+ Thread.sleep(500);
+ assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+ assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+ mViewRoot.getLastPreferredFrameRateCategory());
+ }
+
private void runAfterDraw(@NonNull Runnable runnable) {
Handler handler = new Handler(Looper.getMainLooper());
mAfterDrawLatch = new CountDownLatch(1);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
new file mode 100644
index 000000000000..08c70314973e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module bubble owner
+madym@google.com
+atsjenk@google.com
+liranb@google.com
+sukeshram@google.com
+mpodolian@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
index 1e30d8feb132..ea86c79ae736 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -21,6 +21,7 @@ import android.app.WindowConfiguration
import android.content.ComponentName
import android.content.Context
import android.os.RemoteException
+import android.os.SystemProperties
import android.util.DisplayMetrics
import android.util.Log
import android.util.Pair
@@ -137,5 +138,6 @@ object PipUtils {
@JvmStatic
val isPip2ExperimentEnabled: Boolean
- get() = Flags.enablePip2Implementation()
+ get() = Flags.enablePip2Implementation() || SystemProperties.getBoolean(
+ "wm_shell.pip2", false)
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 17121c8de428..e729c7dd802b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -891,13 +891,13 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static Optional<DesktopTasksController> providesDesktopTasksController(
+ static Optional<DesktopTasksController> providesDesktopTasksController(Context context,
@DynamicOverride Optional<Lazy<DesktopTasksController>> desktopTasksController) {
// Use optional-of-lazy for the dependency that this provider relies on.
// Lazy ensures that this provider will not be the cause the dependency is created
// when it will not be returned due to the condition below.
return desktopTasksController.flatMap((lazy)-> {
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(lazy.get());
}
return Optional.empty();
@@ -910,13 +910,13 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(
+ static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(Context context,
@DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) {
// Use optional-of-lazy for the dependency that this provider relies on.
// Lazy ensures that this provider will not be the cause the dependency is created
// when it will not be returned due to the condition below.
return desktopModeTaskRepository.flatMap((lazy)-> {
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(lazy.get());
}
return Optional.empty();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b574b8159307..a1910c5eb3a3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -221,7 +221,7 @@ public abstract class WMShellModule {
Transitions transitions,
Optional<DesktopTasksController> desktopTasksController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
return new DesktopModeWindowDecorViewModel(
context,
mainExecutor,
@@ -278,8 +278,8 @@ public abstract class WMShellModule {
ShellInit init = FreeformComponents.isFreeformEnabled(context)
? shellInit
: null;
- return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository,
- windowDecorViewModel);
+ return new FreeformTaskListener(context, init, shellTaskOrganizer,
+ desktopModeTaskRepository, windowDecorViewModel);
}
@WMSingleton
@@ -535,10 +535,12 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter(
+ Context context,
Transitions transitions,
@DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
ShellTaskOrganizer shellTaskOrganizer) {
- if (!DesktopModeStatus.isEnabled() || !Flags.enableDesktopWindowingTaskLimit()) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)
+ || !Flags.enableDesktopWindowingTaskLimit()) {
return Optional.empty();
}
return Optional.of(
@@ -592,23 +594,26 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver(
+ Context context,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
Transitions transitions,
ShellInit shellInit
) {
return desktopModeTaskRepository.flatMap(repository ->
- Optional.of(new DesktopTasksTransitionObserver(repository, transitions, shellInit))
+ Optional.of(new DesktopTasksTransitionObserver(
+ context, repository, transitions, shellInit))
);
}
@WMSingleton
@Provides
static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
+ Context context,
ShellInit shellInit,
Transitions transitions,
DesktopModeEventLogger desktopModeEventLogger) {
return new DesktopModeLoggerTransitionObserver(
- shellInit, transitions, desktopModeEventLogger);
+ context, shellInit, transitions, desktopModeEventLogger);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index a10c7c093c60..9038aaad9178 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -20,6 +20,7 @@ import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.TaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
import android.os.IBinder
import android.util.SparseArray
import android.view.SurfaceControl
@@ -49,6 +50,7 @@ import com.android.wm.shell.util.KtProtoLog
* and other transitions that originate both within and outside shell.
*/
class DesktopModeLoggerTransitionObserver(
+ context: Context,
shellInit: ShellInit,
private val transitions: Transitions,
private val desktopModeEventLogger: DesktopModeEventLogger
@@ -57,7 +59,8 @@ class DesktopModeLoggerTransitionObserver(
private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) }
init {
- if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS &&
+ DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback(this::onInit, this)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index fcddcad3a949..9bf9fa749373 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -93,8 +93,10 @@ public class DesktopModeStatus {
"persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT);
/**
- * Return {@code true} if desktop windowing is enabled
+ * Return {@code true} if desktop windowing is enabled. Only to be used for testing. Callers
+ * should use {@link #canEnterDesktopMode(Context)} to query the state of desktop windowing.
*/
+ @VisibleForTesting
public static boolean isEnabled() {
return Flags.enableDesktopWindowingMode();
}
@@ -155,9 +157,9 @@ public class DesktopModeStatus {
}
/**
- * Return {@code true} if desktop mode can be entered on the current device.
+ * Return {@code true} if desktop mode is enabled and can be entered on the current device.
*/
public static boolean canEnterDesktopMode(@NonNull Context context) {
- return !enforceDeviceRestrictions() || isDesktopModeSupported(context);
+ return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index ecfb134e45a0..f7bfb86a5158 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -167,7 +167,7 @@ class DesktopTasksController(
init {
desktopMode = DesktopModeImpl()
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback({ onInit() }, this)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 20df26428649..451e09c3cd9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.content.Context
import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager
@@ -33,13 +34,15 @@ import com.android.wm.shell.util.KtProtoLog
* and other transitions that originate both within and outside shell.
*/
class DesktopTasksTransitionObserver(
+ context: Context,
private val desktopModeTaskRepository: DesktopModeTaskRepository,
private val transitions: Transitions,
shellInit: ShellInit
) : Transitions.TransitionObserver {
init {
- if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS &&
+ DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback(::onInit, this)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 6fea2036dbd1..a414a55eb633 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM;
import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
import android.util.SparseArray;
import android.view.SurfaceControl;
@@ -44,6 +45,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ShellTaskOrganizer.FocusListener {
private static final String TAG = "FreeformTaskListener";
+ private final Context mContext;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
private final WindowDecorViewModel mWindowDecorationViewModel;
@@ -56,10 +58,12 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
}
public FreeformTaskListener(
+ Context context,
ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
WindowDecorViewModel windowDecorationViewModel) {
+ mContext = context;
mShellTaskOrganizer = shellTaskOrganizer;
mWindowDecorationViewModel = windowDecorationViewModel;
mDesktopModeTaskRepository = desktopModeTaskRepository;
@@ -70,7 +74,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
private void onInit() {
mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mShellTaskOrganizer.addFocusListener(this);
}
}
@@ -92,7 +96,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
t.apply();
}
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
@@ -114,7 +118,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
repository.removeFreeformTask(taskInfo.taskId);
repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
@@ -125,7 +129,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false);
});
}
-
+ mWindowDecorationViewModel.onTaskVanished(taskInfo);
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
mWindowDecorationViewModel.destroyWindowDecoration(taskInfo);
}
@@ -139,7 +143,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
taskInfo.taskId);
mWindowDecorationViewModel.onTaskInfoChanged(taskInfo);
state.mTaskInfo = taskInfo;
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
if (taskInfo.isVisible) {
if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
@@ -161,7 +165,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG,
"Freeform Task Focus Changed: #%d focused=%b",
taskInfo.taskId, taskInfo.isFocused);
- if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) {
mDesktopModeTaskRepository.ifPresent(repository -> {
repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 998728d65e6a..2626e7380163 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -161,7 +161,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
-
+ mWindowDecorViewModelOptional.ifPresent(v -> v.onTaskVanished(taskInfo));
if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
if (mWindowDecorViewModelOptional.isPresent()) {
mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index c5f23a8ed034..a8611d966a29 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -327,7 +327,8 @@ public class RecentTasksController implements TaskStackListenerCallback,
private boolean shouldEnableRunningTasksForDesktopMode() {
return mPcFeatureEnabled
- || (DesktopModeStatus.isEnabled() && enableDesktopWindowingTaskbarRunningApps());
+ || (DesktopModeStatus.canEnterDesktopMode(mContext)
+ && enableDesktopWindowingTaskbarRunningApps());
}
@VisibleForTesting
@@ -371,7 +372,8 @@ public class RecentTasksController implements TaskStackListenerCallback,
continue;
}
- if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent()
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)
+ && mDesktopModeTaskRepository.isPresent()
&& mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
// Minimized freeform tasks should not be shown at all.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index f41bca36bb70..1e305c5dbbcf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -260,6 +260,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId);
final int taskId = taskInfo.taskId;
+ mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo));
if (mRootTaskInfo.taskId == taskId) {
mCallbacks.onRootTaskVanished();
mRootTaskInfo = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index 87dc3915082f..e85cb6400000 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -119,6 +119,21 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
}
@Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
+ // windowing mode changed. We're only interested in closing tasks so checking whether
+ // its info still exists in the task organizer is one way to disambiguate.
+ final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
+ if (closed) {
+ // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
+ // changes happen, but there are certain cases in which closing tasks aren't included
+ // in transitions, such as when a non-visible task is closed. See b/296921167.
+ // Destroy the decoration here in case the lack of transition missed it.
+ destroyWindowDecoration(taskInfo);
+ }
+ }
+
+ @Override
public void onTaskChanging(
RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 01175f598089..f3ef7c17fac3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -35,6 +35,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent;
import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import android.annotation.NonNull;
@@ -72,6 +73,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.window.flags.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
@@ -276,7 +278,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
if (visible && stage != STAGE_TYPE_UNDEFINED) {
DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
- if (decor != null && DesktopModeStatus.isEnabled()) {
+ if (decor != null && DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopTasksController.moveToSplit(decor.mTaskInfo);
}
}
@@ -309,6 +311,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
@Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
+ // windowing mode changed. We're only interested in closing tasks so checking whether
+ // its info still exists in the task organizer is one way to disambiguate.
+ final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "Task Vanished: #%d closed=%b", taskInfo.taskId, closed);
+ if (closed) {
+ // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
+ // changes happen, but there are certain cases in which closing tasks aren't included
+ // in transitions, such as when a non-visible task is closed. See b/296921167.
+ // Destroy the decoration here in case the lack of transition missed it.
+ destroyWindowDecoration(taskInfo);
+ }
+ }
+
+ @Override
public void onTaskChanging(
RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -594,7 +612,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
- if (DesktopModeStatus.isEnabled()
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
return false;
}
@@ -771,7 +789,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
*/
private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev);
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
if (!mInImmersiveMode && (relevantDecor == null
|| relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM
|| mTransitionDragActive)) {
@@ -780,7 +798,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
handleEventOutsideCaption(ev, relevantDecor);
// Prevent status bar from reacting to a caption drag.
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
if (mTransitionDragActive) {
inputMonitor.pilferPointers();
}
@@ -838,7 +856,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDragToDesktopAnimationStartBounds.set(
relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
boolean dragFromStatusBarAllowed = false;
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
// In proto2 any full screen or multi-window task can be dragged to
// freeform.
final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
@@ -1013,12 +1031,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& isSingleTopActivityTranslucent(taskInfo)) {
return false;
}
- return DesktopModeStatus.isEnabled()
+ return DesktopModeStatus.canEnterDesktopMode(mContext)
&& !DesktopWallpaperActivity.isWallpaperTask(taskInfo)
&& taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
&& taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
- && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop()
- && DesktopModeStatus.canEnterDesktopMode(mContext);
+ && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop();
}
private void createWindowDecoration(
@@ -1087,7 +1104,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + "DesktopModeWindowDecorViewModel");
- pw.println(innerPrefix + "DesktopModeStatus=" + DesktopModeStatus.isEnabled());
+ pw.println(innerPrefix + "DesktopModeStatus="
+ + DesktopModeStatus.canEnterDesktopMode(mContext));
pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive);
pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay);
pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 8c6bc73d10bf..f7516dabe1ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -638,7 +638,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
.setOnClickListener(mOnCaptionButtonClickListener)
.setOnTouchListener(mOnCaptionTouchListener)
.setLayoutId(mRelayoutParams.mLayoutResId)
- .setWindowingButtonsVisible(DesktopModeStatus.isEnabled())
+ .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext))
.setCaptionHeight(mResult.mCaptionHeight)
.build();
mWindowDecorViewHolder.onHandleMenuOpened();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 01a6012ea314..1563259f4a1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -67,6 +67,14 @@ public interface WindowDecorViewModel {
void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo);
/**
+ * Notifies a task has vanished, which can mean that the task changed windowing mode or was
+ * removed.
+ *
+ * @param taskInfo the task info of the task
+ */
+ void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo);
+
+ /**
* Notifies a transition is about to start about the given task to give the window decoration a
* chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates
* a window decoration if one does not exist but is required.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 65117f7e9eea..60a7dcda5351 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode
import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.content.Context
import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
@@ -35,6 +36,7 @@ import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.modules.utils.testing.ExtendedMockitoRule
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
@@ -58,6 +60,11 @@ import org.mockito.kotlin.never
import org.mockito.kotlin.same
import org.mockito.kotlin.times
+/**
+ * Test class for {@link DesktopModeLoggerTransitionObserver}
+ *
+ * Usage: atest WMShellUnitTests:DesktopModeLoggerTransitionObserverTest
+ */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopModeLoggerTransitionObserverTest {
@@ -74,6 +81,8 @@ class DesktopModeLoggerTransitionObserverTest {
private lateinit var mockShellInit: ShellInit
@Mock
private lateinit var transitions: Transitions
+ @Mock
+ private lateinit var context: Context
private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
private lateinit var shellInit: ShellInit
@@ -81,12 +90,12 @@ class DesktopModeLoggerTransitionObserverTest {
@Before
fun setup() {
- Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true)
+ doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
shellInit = Mockito.spy(ShellInit(testExecutor))
desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
transitionObserver = DesktopModeLoggerTransitionObserver(
- mockShellInit, transitions, desktopModeEventLogger)
+ context, mockShellInit, transitions, desktopModeEventLogger)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
val initRunnableCaptor = ArgumentCaptor.forClass(
Runnable::class.java)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index ad4b720facd7..df8a2223dadc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -159,6 +159,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
mockitoSession = mockitoSession().strictness(Strictness.LENIENT)
.spyStatic(DesktopModeStatus::class.java).startMocking()
whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
shellInit = Mockito.spy(ShellInit(testExecutor))
desktopModeTaskRepository = DesktopModeTaskRepository()
@@ -1347,7 +1348,6 @@ class DesktopTasksControllerTest : ShellTestCase() {
private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createFullscreenTask(displayId)
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
runningTasks.add(task)
@@ -1356,7 +1356,6 @@ class DesktopTasksControllerTest : ShellTestCase() {
private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createSplitScreenTask(displayId)
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 38ea03471b07..539d5b86453f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -27,6 +27,7 @@ import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
@@ -41,6 +42,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.any
import org.mockito.Mockito.`when`
import org.mockito.quality.Strictness
@@ -69,7 +71,7 @@ class DesktopTasksLimiterTest : ShellTestCase() {
fun setUp() {
mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT)
.spyStatic(DesktopModeStatus::class.java).startMocking()
- `when`(DesktopModeStatus.isEnabled()).thenReturn(true)
+ doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
desktopTaskRepo = DesktopModeTaskRepository()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 71eea4bb59b1..665077be3af7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -19,11 +19,12 @@ package com.android.wm.shell.freeform;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -72,8 +73,10 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
public void setup() {
mMockitoSession = mockitoSession().initMocks(this)
.strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(true);
+ doReturn(true).when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
mFreeformTaskListener = new FreeformTaskListener(
+ mContext,
mShellInit,
mTaskOrganizer,
Optional.of(mDesktopModeTaskRepository),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 5cf9be4e7aca..240324ba4420 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -59,6 +59,7 @@ import android.view.SurfaceControl;
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.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -75,11 +76,13 @@ import com.android.wm.shell.sysui.ShellSharedConstants;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import com.android.wm.shell.util.SplitBounds;
+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.quality.Strictness;
import java.util.ArrayList;
import java.util.Arrays;
@@ -88,7 +91,9 @@ import java.util.Optional;
import java.util.function.Consumer;
/**
- * Tests for {@link RecentTasksController}.
+ * Tests for {@link RecentTasksController}
+ *
+ * Usage: atest WMShellUnitTests:RecentTasksControllerTest
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -118,9 +123,15 @@ public class RecentTasksControllerTest extends ShellTestCase {
private ShellInit mShellInit;
private ShellController mShellController;
private TestShellExecutor mMainExecutor;
+ private static StaticMockitoSession sMockitoSession;
@Before
public void setUp() {
+ sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus.class).startMocking();
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
mMainExecutor = new TestShellExecutor();
when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
mShellInit = spy(new ShellInit(mMainExecutor));
@@ -136,6 +147,11 @@ public class RecentTasksControllerTest extends ShellTestCase {
mShellInit.init();
}
+ @After
+ public void tearDown() {
+ sMockitoSession.finishMocking();
+ }
+
@Test
public void instantiateController_addInitCallback() {
verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class));
@@ -275,10 +291,6 @@ public class RecentTasksControllerTest extends ShellTestCase {
@Test
public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -309,16 +321,10 @@ public class RecentTasksControllerTest extends ShellTestCase {
// Check single entries
assertEquals(t2, singleGroup1.getTaskInfo1());
assertEquals(t4, singleGroup2.getTaskInfo1());
-
- mockitoSession.finishMocking();
}
@Test
public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -357,15 +363,12 @@ public class RecentTasksControllerTest extends ShellTestCase {
// Check single entry
assertEquals(t4, singleGroup.getTaskInfo1());
-
- mockitoSession.finishMocking();
}
@Test
public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(false);
+ ExtendedMockito.doReturn(false)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
@@ -390,16 +393,10 @@ public class RecentTasksControllerTest extends ShellTestCase {
assertEquals(t2, recentTasks.get(1).getTaskInfo1());
assertEquals(t3, recentTasks.get(2).getTaskInfo1());
assertEquals(t4, recentTasks.get(3).getTaskInfo1());
-
- mockitoSession.finishMocking();
}
@Test
public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -435,8 +432,6 @@ public class RecentTasksControllerTest extends ShellTestCase {
// Check single entries
assertEquals(t2, singleGroup1.getTaskInfo1());
assertEquals(t4, singleGroup2.getTaskInfo1());
-
- mockitoSession.finishMocking();
}
@Test
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 933a33eb8b0e..f97992f7c9d1 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -24,7 +24,6 @@
#include <SkColor.h>
#include <android-base/stringprintf.h>
#include <android-base/thread_annotations.h>
-#include <com_android_input_flags.h>
#include <ftl/enum.h>
#include <mutex>
@@ -35,14 +34,10 @@
#define INDENT2 " "
#define INDENT3 " "
-namespace input_flags = com::android::input::flags;
-
namespace android {
namespace {
-static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer();
-
const ui::Transform kIdentityTransform;
} // namespace
@@ -68,27 +63,24 @@ void PointerController::DisplayInfoListener::onPointerControllerDestroyed() {
std::shared_ptr<PointerController> PointerController::create(
const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled, ControllerType type) {
+ SpriteController& spriteController, ControllerType type) {
// using 'new' to access non-public constructor
std::shared_ptr<PointerController> controller;
switch (type) {
case ControllerType::MOUSE:
controller = std::shared_ptr<PointerController>(
- new MousePointerController(policy, looper, spriteController, enabled));
+ new MousePointerController(policy, looper, spriteController));
break;
case ControllerType::TOUCH:
controller = std::shared_ptr<PointerController>(
- new TouchPointerController(policy, looper, spriteController, enabled));
+ new TouchPointerController(policy, looper, spriteController));
break;
case ControllerType::STYLUS:
controller = std::shared_ptr<PointerController>(
- new StylusPointerController(policy, looper, spriteController, enabled));
+ new StylusPointerController(policy, looper, spriteController));
break;
- case ControllerType::LEGACY:
default:
- controller = std::shared_ptr<PointerController>(
- new PointerController(policy, looper, spriteController, enabled));
- break;
+ LOG_ALWAYS_FATAL("Invalid ControllerType: %d", static_cast<int>(type));
}
/*
@@ -108,10 +100,9 @@ std::shared_ptr<PointerController> PointerController::create(
}
PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled)
+ const sp<Looper>& looper, SpriteController& spriteController)
: PointerController(
- policy, looper, spriteController, enabled,
+ policy, looper, spriteController,
[](const sp<android::gui::WindowInfosListener>& listener) {
auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{},
std::vector<android::gui::DisplayInfo>{});
@@ -125,11 +116,9 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>&
PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled,
const WindowListenerRegisterConsumer& registerListener,
WindowListenerUnregisterConsumer unregisterListener)
- : mEnabled(enabled),
- mContext(policy, looper, spriteController, *this),
+ : mContext(policy, looper, spriteController, *this),
mCursorController(mContext),
mDisplayInfoListener(sp<DisplayInfoListener>::make(this)),
mUnregisterWindowInfosListener(std::move(unregisterListener)) {
@@ -142,7 +131,6 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>&
PointerController::~PointerController() {
mDisplayInfoListener->onPointerControllerDestroyed();
mUnregisterWindowInfosListener(mDisplayInfoListener);
- mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0});
}
std::mutex& PointerController::getLock() const {
@@ -150,14 +138,10 @@ std::mutex& PointerController::getLock() const {
}
std::optional<FloatRect> PointerController::getBounds() const {
- if (!mEnabled) return {};
-
return mCursorController.getBounds();
}
void PointerController::move(float deltaX, float deltaY) {
- if (!mEnabled) return;
-
const int32_t displayId = mCursorController.getDisplayId();
vec2 transformed;
{
@@ -169,8 +153,6 @@ void PointerController::move(float deltaX, float deltaY) {
}
void PointerController::setPosition(float x, float y) {
- if (!mEnabled) return;
-
const int32_t displayId = mCursorController.getDisplayId();
vec2 transformed;
{
@@ -182,10 +164,6 @@ void PointerController::setPosition(float x, float y) {
}
FloatPoint PointerController::getPosition() const {
- if (!mEnabled) {
- return FloatPoint{0, 0};
- }
-
const int32_t displayId = mCursorController.getDisplayId();
const auto p = mCursorController.getPosition();
{
@@ -196,28 +174,20 @@ FloatPoint PointerController::getPosition() const {
}
int32_t PointerController::getDisplayId() const {
- if (!mEnabled) return ADISPLAY_ID_NONE;
-
return mCursorController.getDisplayId();
}
void PointerController::fade(Transition transition) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.fade(transition);
}
void PointerController::unfade(Transition transition) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.unfade(transition);
}
void PointerController::setPresentation(Presentation presentation) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
if (mLocked.presentation == presentation) {
@@ -226,33 +196,13 @@ void PointerController::setPresentation(Presentation presentation) {
mLocked.presentation = presentation;
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- // When pointer choreographer is enabled, the presentation mode is only set once when the
- // PointerController is constructed, before the display viewport is provided.
- // TODO(b/293587049): Clean up the PointerController interface after pointer choreographer
- // is permanently enabled. The presentation can be set in the constructor.
- mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
- return;
- }
-
- if (!mCursorController.isViewportValid()) {
- return;
- }
-
- if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) {
- // For now, we support stylus hover using the mouse cursor implementation.
- // TODO: Add proper support for stylus hover icons.
- mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
-
- mCursorController.getAdditionalMouseResources();
- clearSpotsLocked();
- }
+ // The presentation mode is only set once when the PointerController is constructed,
+ // before the display viewport is provided.
+ mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
}
void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
BitSet32 spotIdBits, int32_t displayId) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
std::array<PointerCoords, MAX_POINTERS> outSpotCoords{};
const ui::Transform& transform = getTransformForDisplayLocked(displayId);
@@ -279,8 +229,6 @@ void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t
}
void PointerController::clearSpots() {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
clearSpotsLocked();
}
@@ -313,12 +261,6 @@ void PointerController::reloadPointerResources() {
}
void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
- struct PointerDisplayChangeArgs {
- int32_t displayId;
- FloatPoint cursorPosition;
- };
- std::optional<PointerDisplayChangeArgs> pointerDisplayChanged;
-
{ // acquire lock
std::scoped_lock lock(getLock());
@@ -330,34 +272,21 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
if (viewport.displayId != mLocked.pointerDisplayId) {
mLocked.pointerDisplayId = viewport.displayId;
- pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()};
}
} // release lock
-
- if (pointerDisplayChanged) {
- // Notify the policy without holding the pointer controller lock.
- mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId,
- pointerDisplayChanged->cursorPosition);
- }
}
void PointerController::updatePointerIcon(PointerIconStyle iconId) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.updatePointerIcon(iconId);
}
void PointerController::setCustomPointerIcon(const SpriteIcon& icon) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
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);
@@ -406,10 +335,6 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display
}
std::string PointerController::dump() {
- if (!mEnabled) {
- return INDENT "PointerController: DISABLED due to ongoing PointerChoreographer refactor\n";
- }
-
std::string dump = INDENT "PointerController:\n";
std::scoped_lock lock(getLock());
dump += StringPrintf(INDENT2 "Presentation: %s\n",
@@ -430,8 +355,8 @@ std::string PointerController::dump() {
MousePointerController::MousePointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled)
- : PointerController(policy, looper, spriteController, enabled) {
+ SpriteController& spriteController)
+ : PointerController(policy, looper, spriteController) {
PointerController::setPresentation(Presentation::POINTER);
}
@@ -443,8 +368,8 @@ MousePointerController::~MousePointerController() {
TouchPointerController::TouchPointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled)
- : PointerController(policy, looper, spriteController, enabled) {
+ SpriteController& spriteController)
+ : PointerController(policy, looper, spriteController) {
PointerController::setPresentation(Presentation::SPOT);
}
@@ -456,8 +381,8 @@ TouchPointerController::~TouchPointerController() {
StylusPointerController::StylusPointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled)
- : PointerController(policy, looper, spriteController, enabled) {
+ SpriteController& spriteController)
+ : PointerController(policy, looper, spriteController) {
PointerController::setPresentation(Presentation::STYLUS_HOVER);
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index d76ca5d15a31..eaf34d527396 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -47,8 +47,7 @@ class PointerController : public PointerControllerInterface {
public:
static std::shared_ptr<PointerController> create(
const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled,
- ControllerType type = ControllerType::LEGACY);
+ SpriteController& spriteController, ControllerType type);
~PointerController() override;
@@ -87,12 +86,12 @@ protected:
// Constructor used to test WindowInfosListener registration.
PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled,
+ SpriteController& spriteController,
const WindowListenerRegisterConsumer& registerListener,
WindowListenerUnregisterConsumer unregisterListener);
PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled);
+ SpriteController& spriteController);
private:
friend PointerControllerContext::LooperCallback;
@@ -104,8 +103,6 @@ private:
// we use the DisplayInfoListener's lock in PointerController.
std::mutex& getLock() const;
- const bool mEnabled;
-
PointerControllerContext mContext;
MouseCursorController mCursorController;
@@ -144,8 +141,7 @@ class MousePointerController : public PointerController {
public:
/** A version of PointerController that controls one mouse pointer. */
MousePointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled);
+ const sp<Looper>& looper, SpriteController& spriteController);
~MousePointerController() override;
@@ -164,8 +160,7 @@ class TouchPointerController : public PointerController {
public:
/** A version of PointerController that controls touch spots. */
TouchPointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled);
+ const sp<Looper>& looper, SpriteController& spriteController);
~TouchPointerController() override;
@@ -210,8 +205,7 @@ class StylusPointerController : public PointerController {
public:
/** A version of PointerController that controls one stylus pointer. */
StylusPointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled);
+ const sp<Looper>& looper, SpriteController& spriteController);
~StylusPointerController() override;
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index 98c3988e7df4..e893c49d80e5 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -81,7 +81,6 @@ public:
virtual PointerIconStyle getDefaultPointerIconId() = 0;
virtual PointerIconStyle getDefaultStylusIconId() = 0;
virtual PointerIconStyle getCustomPointerIconId() = 0;
- virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0;
};
/*
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 4e4ba6551aec..fdb15506fd0c 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -165,29 +165,23 @@ private:
* on the sprites for a long time.
* Note that the SpriteIcon holds a reference to a shared (and immutable) bitmap. */
struct SpriteState {
- inline SpriteState() :
- dirty(0), visible(false),
- positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT),
- surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
- }
-
- uint32_t dirty;
+ uint32_t dirty{0};
SpriteIcon icon;
- bool visible;
- float positionX;
- float positionY;
- int32_t layer;
- float alpha;
+ bool visible{false};
+ float positionX{0};
+ float positionY{0};
+ int32_t layer{0};
+ float alpha{1.0f};
SpriteTransformationMatrix transformationMatrix;
- int32_t displayId;
+ int32_t displayId{ADISPLAY_ID_DEFAULT};
sp<SurfaceControl> surfaceControl;
- int32_t surfaceWidth;
- int32_t surfaceHeight;
- bool surfaceDrawn;
- bool surfaceVisible;
- bool skipScreenshot;
+ int32_t surfaceWidth{0};
+ int32_t surfaceHeight{0};
+ bool surfaceDrawn{false};
+ bool surfaceVisible{false};
+ bool skipScreenshot{false};
inline bool wantSurfaceVisible() const {
return visible && alpha > 0.0f && icon.isValid();
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index fcf226c9b949..3bc0e24b6e2e 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-#include <com_android_input_flags.h>
#include <flag_macros.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -30,8 +29,6 @@
namespace android {
-namespace input_flags = com::android::input::flags;
-
enum TestCursorType {
CURSOR_TYPE_DEFAULT = 0,
CURSOR_TYPE_HOVER,
@@ -64,11 +61,9 @@ public:
virtual PointerIconStyle getDefaultPointerIconId() override;
virtual PointerIconStyle getDefaultStylusIconId() override;
virtual PointerIconStyle getCustomPointerIconId() override;
- virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
bool allResourcesAreLoaded();
bool noResourcesAreLoaded();
- std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; }
private:
void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType);
@@ -76,7 +71,6 @@ private:
bool pointerIconLoaded{false};
bool pointerResourcesLoaded{false};
bool additionalMouseResourcesLoaded{false};
- std::optional<int32_t /*displayId*/> latestPointerDisplayId;
};
void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) {
@@ -146,12 +140,6 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic
icon->hotSpotY = hotSpot.second;
}
-void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId,
- const FloatPoint& /*position*/
-) {
- latestPointerDisplayId = displayId;
-}
-
class TestPointerController : public PointerController {
public:
TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener,
@@ -159,7 +147,6 @@ public:
SpriteController& spriteController)
: PointerController(
policy, looper, spriteController,
- /*enabled=*/true,
[&registeredListener](const sp<android::gui::WindowInfosListener>& listener)
-> std::vector<gui::DisplayInfo> {
// Register listener
@@ -267,8 +254,7 @@ TEST_F(PointerControllerTest, useStylusTypeForStylusHover) {
mPointerController->reloadPointerResources();
}
-TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources,
- REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) {
+TEST_F(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources) {
// Setting the presentation mode before a display viewport is set will not load any resources.
mPointerController->setPresentation(PointerController::Presentation::POINTER);
ASSERT_TRUE(mPolicy->noResourcesAreLoaded());
@@ -278,26 +264,7 @@ TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoe
ASSERT_TRUE(mPolicy->allResourcesAreLoaded());
}
-TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIcon,
- REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(input_flags,
- enable_pointer_choreographer))) {
- ensureDisplayViewportIsSet();
- mPointerController->setPresentation(PointerController::Presentation::POINTER);
- mPointerController->unfade(PointerController::Transition::IMMEDIATE);
-
- int32_t type = CURSOR_TYPE_ADDITIONAL;
- std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type);
- EXPECT_CALL(*mPointerSprite, setVisible(true));
- EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
- EXPECT_CALL(*mPointerSprite,
- setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)),
- Field(&SpriteIcon::hotSpotX, hotspot.first),
- Field(&SpriteIcon::hotSpotY, hotspot.second))));
- mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type));
-}
-
-TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIconWithChoreographer,
- REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) {
+TEST_F(PointerControllerTest, updatePointerIconWithChoreographer) {
// When PointerChoreographer is enabled, the presentation mode is set before the viewport.
mPointerController->setPresentation(PointerController::Presentation::POINTER);
ensureDisplayViewportIsSet();
@@ -348,30 +315,6 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) {
ensureDisplayViewportIsSet();
}
-TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) {
- EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId())
- << "A pointer display change does not occur when PointerController is created.";
-
- ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT);
-
- const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId();
- ASSERT_TRUE(lastReportedPointerDisplayId)
- << "The policy is notified of a pointer display change when the viewport is first set.";
- EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId)
- << "Incorrect pointer display notified.";
-
- ensureDisplayViewportIsSet(42);
-
- EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId())
- << "The policy is notified when the pointer display changes.";
-
- // Release the PointerController.
- mPointerController = nullptr;
-
- EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId())
- << "The pointer display changes to invalid when PointerController is destroyed.";
-}
-
TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) {
ensureDisplayViewportIsSet();
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index b2838c809854..7ddf11e41a41 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -50,6 +50,7 @@ import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.media.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -984,37 +985,7 @@ public final class MediaRouter2 {
@SystemApi
@RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
- mImpl.transfer(
- controller.getRoutingSessionInfo(),
- route,
- Process.myUserHandle(),
- mContext.getPackageName());
- }
-
- /**
- * Transfers the media of a routing controller to the given route.
- *
- * <p>This will be no-op for non-system media routers.
- *
- * @param controller a routing controller controlling media routing.
- * @param route the route you want to transfer the media to.
- * @param transferInitiatorUserHandle the user handle of the app that initiated the transfer
- * request.
- * @param transferInitiatorPackageName the package name of the app that initiated the transfer.
- * This value is used with the user handle to populate {@link
- * RoutingController#wasTransferInitiatedBySelf()}.
- * @hide
- */
- public void transfer(
- @NonNull RoutingController controller,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName) {
- mImpl.transfer(
- controller.getRoutingSessionInfo(),
- route,
- transferInitiatorUserHandle,
- transferInitiatorPackageName);
+ mImpl.transfer(controller.getRoutingSessionInfo(), route);
}
void requestCreateController(
@@ -1913,13 +1884,7 @@ public final class MediaRouter2 {
*/
@FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
public boolean wasTransferInitiatedBySelf() {
- RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
-
- UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
- String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
-
- return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
- && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+ return mImpl.wasTransferredBySelf(getRoutingSessionInfo());
}
/**
@@ -2082,12 +2047,28 @@ public final class MediaRouter2 {
Objects.requireNonNull(route, "route must not be null");
synchronized (mControllerLock) {
if (isReleased()) {
- Log.w(TAG, "transferToRoute: Called on released controller. Ignoring.");
+ Log.w(
+ TAG,
+ "tryTransferWithinProvider: Called on released controller. Ignoring.");
return true;
}
- if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) {
- Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route);
+ // If this call is trying to transfer to a selected system route, we let them
+ // through as a provider driven transfer in order to update the transfer reason and
+ // initiator data.
+ boolean isSystemRouteReselection =
+ Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+ && mSessionInfo.isSystemSession()
+ && route.isSystemRoute()
+ && mSessionInfo.getSelectedRoutes().contains(route.getId());
+ if (!isSystemRouteReselection
+ && !mSessionInfo.getTransferableRoutes().contains(route.getId())) {
+ Log.i(
+ TAG,
+ "Transferring to a non-transferable route="
+ + route
+ + " session= "
+ + mSessionInfo.getId());
return false;
}
}
@@ -2498,11 +2479,7 @@ public final class MediaRouter2 {
void stop();
- void transfer(
- @NonNull RoutingSessionInfo sessionInfo,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName);
+ void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route);
List<RoutingController> getControllers();
@@ -2523,6 +2500,11 @@ public final class MediaRouter2 {
boolean shouldNotifyStop,
RoutingController controller);
+ /**
+ * Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app
+ * associated with this router.
+ */
+ boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo);
}
/**
@@ -2723,7 +2705,7 @@ public final class MediaRouter2 {
List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
- transfer(targetSession, route, mClientUser, mContext.getPackageName());
+ transfer(targetSession, route);
}
@Override
@@ -2746,24 +2728,15 @@ public final class MediaRouter2 {
*
* @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer.
* @param route The {@link MediaRoute2Info route} to transfer to.
- * @param transferInitiatorUserHandle The user handle of the app that initiated the
- * transfer.
- * @param transferInitiatorPackageName The package name if of the app that initiated the
- * transfer.
* @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String)
* @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)
*/
@Override
@SuppressWarnings("AndroidFrameworkRequiresPermission")
public void transfer(
- @NonNull RoutingSessionInfo sessionInfo,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName) {
+ @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
Objects.requireNonNull(route, "route must not be null");
- Objects.requireNonNull(transferInitiatorUserHandle);
- Objects.requireNonNull(transferInitiatorPackageName);
Log.v(
TAG,
@@ -2780,15 +2753,19 @@ public final class MediaRouter2 {
return;
}
- if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
- transferToRoute(
- sessionInfo,
- route,
- transferInitiatorUserHandle,
- transferInitiatorPackageName);
+ // If this call is trying to transfer to a selected system route, we let them
+ // through as a provider driven transfer in order to update the transfer reason and
+ // initiator data.
+ boolean isSystemRouteReselection =
+ Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+ && sessionInfo.isSystemSession()
+ && route.isSystemRoute()
+ && sessionInfo.getSelectedRoutes().contains(route.getId());
+ if (sessionInfo.getTransferableRoutes().contains(route.getId())
+ || isSystemRouteReselection) {
+ transferToRoute(sessionInfo, route, mClientUser, mClientPackageName);
} else {
- requestCreateSession(sessionInfo, route, transferInitiatorUserHandle,
- transferInitiatorPackageName);
+ requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName);
}
}
@@ -3043,6 +3020,14 @@ public final class MediaRouter2 {
releaseSession(controller.getRoutingSessionInfo());
}
+ @Override
+ public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
+ UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+ String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+ return Objects.equals(mClientUser, transferInitiatorUserHandle)
+ && Objects.equals(mClientPackageName, transferInitiatorPackageName);
+ }
+
/**
* Retrieves the system session info for the given package.
*
@@ -3619,10 +3604,7 @@ public final class MediaRouter2 {
*/
@Override
public void transfer(
- @NonNull RoutingSessionInfo sessionInfo,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName) {
+ @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
// Do nothing.
}
@@ -3741,6 +3723,14 @@ public final class MediaRouter2 {
}
}
+ @Override
+ public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
+ UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+ String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+ return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
+ && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+ }
+
@GuardedBy("mLock")
private void registerRouterStubIfNeededLocked() throws RemoteException {
if (mStub == null) {
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index de9eada18104..2fe2ce39813b 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -1212,16 +1212,16 @@ public final class CardEmulation {
*
* @param service The ComponentName of the service
* @param status true to enable, false to disable
+ * @param userId the user handle of the user whose information is being requested.
* @return set service for the category and true if service is already set return false.
*
* @hide
*/
- public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status) {
+ public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status,
+ int userId) {
if (service == null) {
throw new NullPointerException("activity or service or category is null");
}
- int userId = mContext.getUser().getIdentifier();
-
try {
return sService.setServiceEnabledForCategoryOther(userId, service, status);
} catch (RemoteException e) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 65c570840368..fce7a00f9744 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -382,6 +382,7 @@ android_library {
"androidx.compose.material_material-icons-extended",
"androidx.activity_activity-compose",
"androidx.compose.animation_animation-graphics",
+ "device_policy_aconfig_flags_lib",
],
libs: [
"keepanno-annotations",
@@ -622,6 +623,7 @@ android_app {
"//frameworks/libs/systemui:compilelib",
"SystemUI-tests-base",
"androidx.compose.runtime_runtime",
+ "SystemUI-core",
],
libs: [
"keepanno-annotations",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 59e2b9175711..b9e70ef14007 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1110,7 +1110,7 @@
android:resource="@xml/home_controls_dream_metadata" />
</service>
- <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity"
+ <activity android:name="com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity"
android:exported="false"
android:theme="@style/ShortcutHelperTheme"
android:excludeFromRecents="true"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index de090f4b2a6d..21881f657e6d 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -171,16 +171,6 @@ flag {
}
flag {
- name: "nssl_falsing_fix"
- namespace: "systemui"
- description: "Minor touch changes to prevent falsing errors in NSSL"
- bug: "316551193"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "refactor_get_current_user"
namespace: "systemui"
description: "KeyguardUpdateMonitor.getCurrentUser() was providing outdated results."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
index 3eb1b14e72ba..604b517e19e5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
@@ -35,6 +35,7 @@ data class DisplayCutout(
val viewDisplayCutoutKeyguardStatusBarView: ViewDisplayCutout? = null,
) {
fun width() = abs(right.value - left.value).dp
+ fun height() = abs(bottom.value - top.value).dp
}
enum class CutoutLocation {
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 79b57ca74f7d..3227611b841d 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
@@ -804,6 +804,8 @@ private fun CommunalContent(
is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
is CommunalContentModel.WidgetContent.DisabledWidget ->
DisabledWidgetPlaceholder(model, viewModel, modifier)
+ is CommunalContentModel.WidgetContent.PendingWidget ->
+ PendingWidgetPlaceholder(model, modifier)
is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, modifier)
is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
is CommunalContentModel.Tutorial -> TutorialContent(modifier)
@@ -929,36 +931,36 @@ private fun WidgetContent(
Modifier.semantics {
contentDescription = accessibilityLabel
onClick(label = clickActionLabel, action = null)
- val deleteAction =
- CustomAccessibilityAction(removeWidgetActionLabel) {
- contentListState.onRemove(index)
+ 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
}
- 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
+ customActions = actions
}
}
) {
@@ -1074,13 +1076,43 @@ fun DisabledWidgetPlaceholder(
Image(
painter = rememberDrawablePainter(icon.loadDrawable(context)),
contentDescription = stringResource(R.string.icon_description_for_disabled_widget),
- modifier = Modifier.size(48.dp),
+ modifier = Modifier.size(Dimensions.IconSize),
colorFilter = ColorFilter.colorMatrix(Colors.DisabledColorFilter),
)
}
}
@Composable
+fun PendingWidgetPlaceholder(
+ model: CommunalContentModel.WidgetContent.PendingWidget,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val icon: Icon =
+ if (model.icon != null) {
+ Icon.createWithBitmap(model.icon)
+ } else {
+ Icon.createWithResource(context, android.R.drawable.sym_def_app_icon)
+ }
+
+ Column(
+ modifier =
+ modifier.background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+ ),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = rememberDrawablePainter(icon.loadDrawable(context)),
+ contentDescription = stringResource(R.string.icon_description_for_pending_widget),
+ modifier = Modifier.size(Dimensions.IconSize),
+ )
+ }
+}
+
+@Composable
private fun SmartspaceContent(
model: CommunalContentModel.Smartspace,
modifier: Modifier = Modifier,
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 ec9136d50e4c..bca8fde17fce 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
@@ -320,7 +320,6 @@ private fun SceneScope.QuickSettingsScene(
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
- modifier = Modifier.padding(horizontal = 16.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
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 7eaebc21355d..ff9c5a5ee586 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,10 +5,13 @@ 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.GoneToSplitShade
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
import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
@@ -38,6 +41,13 @@ val SceneContainerTransitions = transitions {
from(
Scenes.Gone,
to = Scenes.Shade,
+ key = GoneToSplitShade,
+ ) {
+ goneToSplitShadeTransition()
+ }
+ from(
+ Scenes.Gone,
+ to = Scenes.Shade,
key = SlightlyFasterShadeCollapse,
) {
goneToShadeTransition(durationScale = 0.9)
@@ -68,5 +78,9 @@ val SceneContainerTransitions = transitions {
Notifications.Elements.NotificationScrim,
y = { Shade.Dimensions.ScrimOverscrollLimit }
)
+ translate(
+ Shade.Elements.SplitShadeStartColumn,
+ y = { Shade.Dimensions.ScrimOverscrollLimit }
+ )
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
new file mode 100644
index 000000000000..4dc36d6f1878
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.ui.composable.transitions
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
+import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.ui.composable.QuickSettings
+import com.android.systemui.shade.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.goneToSplitShadeTransition(
+ durationScale: Double = 1.0,
+) {
+ spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+ swipeSpec =
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
+ )
+ distance =
+ object : UserActionDistance {
+ override fun UserActionDistanceScope.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return fromSceneSize.height.toFloat() * 2 / 3f
+ }
+ }
+
+ fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) }
+
+ fractionRange(start = .33f) {
+ fade(ShadeHeader.Elements.Clock)
+ fade(ShadeHeader.Elements.CollapsedContentStart)
+ fade(ShadeHeader.Elements.CollapsedContentEnd)
+ fade(ShadeHeader.Elements.PrivacyChip)
+ fade(QuickSettings.Elements.SplitShadeQuickSettings)
+ fade(QuickSettings.Elements.FooterActions)
+ fade(Notifications.Elements.NotificationScrim)
+ }
+}
+
+private val DefaultDuration = 500.milliseconds
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 0bd38a1daf43..709a416c0366 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
@@ -50,6 +50,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexScenePicker
@@ -63,6 +64,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.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
@@ -77,6 +79,7 @@ import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
import com.android.systemui.statusbar.policy.Clock
+import kotlin.math.max
object ShadeHeader {
object Elements {
@@ -121,7 +124,11 @@ fun SceneScope.CollapsedShadeHeader(
}
val cutoutWidth = LocalDisplayCutout.current.width()
+ val cutoutHeight = LocalDisplayCutout.current.height()
+ val cutoutTop = LocalDisplayCutout.current.top
val cutoutLocation = LocalDisplayCutout.current.location
+ val horizontalPadding =
+ max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
val useExpandedFormat by
remember(cutoutLocation) {
@@ -140,7 +147,7 @@ fun SceneScope.CollapsedShadeHeader(
contents =
listOf(
{
- Row {
+ Row(modifier = Modifier.padding(horizontal = horizontalPadding)) {
Clock(
scale = 1f,
viewModel = viewModel,
@@ -157,7 +164,12 @@ fun SceneScope.CollapsedShadeHeader(
},
{
if (isPrivacyChipVisible) {
- Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
+ Box(
+ modifier =
+ Modifier.height(CollapsedHeight)
+ .fillMaxWidth()
+ .padding(horizontal = horizontalPadding)
+ ) {
PrivacyChip(
viewModel = viewModel,
modifier = Modifier.align(Alignment.CenterEnd),
@@ -166,9 +178,13 @@ fun SceneScope.CollapsedShadeHeader(
} else {
Row(
horizontalArrangement = Arrangement.End,
- modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+ modifier =
+ Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+ .padding(horizontal = horizontalPadding)
) {
- SystemIconContainer {
+ SystemIconContainer(
+ modifier = Modifier.align(Alignment.CenterVertically)
+ ) {
when (LocalWindowSizeClass.current.widthSizeClass) {
WindowWidthSizeClass.Medium,
WindowWidthSizeClass.Expanded ->
@@ -206,7 +222,7 @@ fun SceneScope.CollapsedShadeHeader(
val screenWidth = constraints.maxWidth
val cutoutWidthPx = cutoutWidth.roundToPx()
- val height = CollapsedHeight.roundToPx()
+ val height = max(cutoutHeight + (cutoutTop * 2), CollapsedHeight).roundToPx()
val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height)
val startMeasurable = measurables[0][0]
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 4ad8b9f47f70..36b60d64399a 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
@@ -67,6 +67,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
@@ -97,6 +98,7 @@ object Shade {
val MediaCarousel = ElementKey("ShadeMediaCarousel")
val BackgroundScrim =
ElementKey("ShadeBackgroundScrim", scenePicker = LowestZIndexScenePicker)
+ val SplitShadeStartColumn = ElementKey("SplitShadeStartColumn")
}
object Dimensions {
@@ -250,10 +252,6 @@ private fun SceneScope.SingleShade(
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
- modifier =
- Modifier.padding(
- horizontal = Shade.Dimensions.HorizontalPadding
- )
)
Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
QuickSettings(
@@ -312,6 +310,8 @@ private fun SceneScope.SplitShade(
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
) {
+ val screenCornerRadius = LocalScreenCornerRadius.current
+
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
val customizingAnimationDuration by
@@ -320,7 +320,11 @@ private fun SceneScope.SplitShade(
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
val tileSquishiness by
- animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
+ animateSceneFloatAsState(
+ value = 1f,
+ key = QuickSettings.SharedValues.TilesSquishiness,
+ canOverflow = false,
+ )
val unfoldTranslationXForStartSide by
viewModel
.unfoldTranslationX(
@@ -388,8 +392,7 @@ private fun SceneScope.SplitShade(
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
modifier =
- Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
- .then(brightnessMirrorShowingModifier)
+ Modifier.then(brightnessMirrorShowingModifier)
.padding(
horizontal = { unfoldTranslationXForStartSide.roundToInt() },
)
@@ -398,9 +401,9 @@ private fun SceneScope.SplitShade(
Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
Box(
modifier =
- Modifier.weight(1f).graphicsLayer {
- translationX = unfoldTranslationXForStartSide
- },
+ Modifier.element(Shade.Elements.SplitShadeStartColumn)
+ .weight(1f)
+ .graphicsLayer { translationX = unfoldTranslationXForStartSide },
) {
BrightnessMirror(
viewModel = viewModel.brightnessMirrorViewModel,
@@ -467,7 +470,7 @@ private fun SceneScope.SplitShade(
modifier =
Modifier.weight(1f)
.fillMaxHeight()
- .padding(bottom = navBarBottomHeight)
+ .padding(end = screenCornerRadius / 2f, bottom = navBarBottomHeight)
.then(brightnessMirrorShowingModifier)
)
}
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 0f6d51d3e3ea..79d17efcacc1 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
@@ -16,6 +16,7 @@
package com.android.systemui.volume.panel.component.anc.ui.composable
+import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -35,7 +36,6 @@ import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
import com.android.systemui.volume.panel.component.anc.ui.viewmodel.AncViewModel
@@ -81,11 +81,10 @@ constructor(
onClick = onClick,
)
Text(
- modifier = Modifier.clearAndSetSemantics {},
+ modifier = Modifier.clearAndSetSemantics {}.basicMarquee(),
text = label,
style = MaterialTheme.typography.labelMedium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
+ maxLines = 2,
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
index c51e8b0ead12..a602e25e05c1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
@@ -28,7 +28,11 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
+import com.android.systemui.res.R
import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
@@ -49,6 +53,7 @@ fun VolumePanelRoot(
}
}
+ val accessibilityTitle = stringResource(R.string.accessibility_volume_settings)
val state: VolumePanelState by viewModel.volumePanelState.collectAsState()
val components by viewModel.componentsLayout.collectAsState(null)
@@ -56,12 +61,14 @@ fun VolumePanelRoot(
components?.let { componentsState ->
Components(
componentsState,
- modifier.padding(
- start = padding,
- top = padding,
- end = padding,
- bottom = 20.dp,
- )
+ modifier
+ .semantics { paneTitle = accessibilityTitle }
+ .padding(
+ start = padding,
+ top = padding,
+ end = padding,
+ bottom = 20.dp,
+ )
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 447c28067200..6c3f3c181d45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -17,8 +17,11 @@
package com.android.keyguard
+import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
import android.content.res.Configuration
import android.media.AudioManager
+import android.platform.test.annotations.EnableFlags
import android.telephony.TelephonyManager
import android.testing.TestableLooper.RunWithLooper
import android.testing.TestableResources
@@ -148,6 +151,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
@Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
@Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock private lateinit var postureController: DevicePostureController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Captor
private lateinit var swipeListenerArgumentCaptor:
@@ -273,6 +277,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
mSelectedUserInteractor,
deviceProvisionedController,
faceAuthAccessibilityDelegate,
+ devicePolicyManager,
keyguardTransitionInteractor,
{ primaryBouncerInteractor },
) {
@@ -934,6 +939,45 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any())
}
+ @Test
+ @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+ fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() {
+ val mainUserId = 10
+
+ underTest.showMessageForFailedUnlockAttempt(
+ /* userId = */ mainUserId,
+ /* expiringUserId = */ mainUserId,
+ /* mainUserId = */ mainUserId,
+ /* remainingBeforeWipe = */ 1,
+ /* failedAttempts = */ 1
+ )
+
+ verify(view)
+ .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY))
+ }
+
+ @Test
+ @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+ fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() {
+ val secondaryUserId = 10
+ val mainUserId = 0
+
+ underTest.showMessageForFailedUnlockAttempt(
+ /* userId = */ secondaryUserId,
+ /* expiringUserId = */ secondaryUserId,
+ /* mainUserId = */ mainUserId,
+ /* remainingBeforeWipe = */ 1,
+ /* failedAttempts = */ 1
+ )
+
+ verify(view)
+ .showAlmostAtWipeDialog(
+ any(),
+ any(),
+ eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER)
+ )
+ }
+
private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener
get() {
underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 81878aaf4a18..0c5e726e17aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.authentication.domain.interactor
import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
@@ -32,6 +34,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -410,12 +414,16 @@ class AuthenticationInteractorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
fun upcomingWipe() =
testScope.runTest {
val upcomingWipe by collectLastValue(underTest.upcomingWipe)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+ kosmos.fakeUserRepository.asMainUser()
+ kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe =
+ FakeUserRepository.MAIN_USER_ID
underTest.authenticate(correctPin)
assertThat(upcomingWipe).isNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
index 23869576a7a4..7628debf245a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
@@ -50,6 +50,7 @@ class PackageChangeRepositoryTest : SysuiTestCase() {
@Mock private lateinit var context: Context
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var handler: Handler
+ @Mock private lateinit var packageInstallerMonitor: PackageInstallerMonitor
private lateinit var repository: PackageChangeRepository
private lateinit var updateMonitor: PackageUpdateMonitor
@@ -60,19 +61,20 @@ class PackageChangeRepositoryTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this@PackageChangeRepositoryTest)
whenever(context.packageManager).thenReturn(packageManager)
- repository = PackageChangeRepositoryImpl { user ->
- updateMonitor =
- PackageUpdateMonitor(
- user = user,
- bgDispatcher = testDispatcher,
- scope = applicationCoroutineScope,
- context = context,
- bgHandler = handler,
- logger = PackageUpdateLogger(logcatLogBuffer()),
- systemClock = fakeSystemClock,
- )
- updateMonitor
- }
+ repository =
+ PackageChangeRepositoryImpl(packageInstallerMonitor) { user ->
+ updateMonitor =
+ PackageUpdateMonitor(
+ user = user,
+ bgDispatcher = testDispatcher,
+ scope = applicationCoroutineScope,
+ context = context,
+ bgHandler = handler,
+ logger = PackageUpdateLogger(logcatLogBuffer()),
+ systemClock = fakeSystemClock,
+ )
+ updateMonitor
+ }
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
new file mode 100644
index 000000000000..5556b04c2d20
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.common.data.repository
+
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageInstaller.SessionInfo
+import android.graphics.Bitmap
+import android.os.fakeExecutorHandler
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.PackageInstallSession
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
+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.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+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.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PackageInstallerMonitorTest : SysuiTestCase() {
+ @Mock private lateinit var packageInstaller: PackageInstaller
+ @Mock private lateinit var icon1: Bitmap
+ @Mock private lateinit var icon2: Bitmap
+ @Mock private lateinit var icon3: Bitmap
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val handler = kosmos.fakeExecutorHandler
+
+ private lateinit var session1: SessionInfo
+ private lateinit var session2: SessionInfo
+ private lateinit var session3: SessionInfo
+
+ private lateinit var defaultSessions: List<SessionInfo>
+
+ private lateinit var underTest: PackageInstallerMonitor
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ session1 =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = "pkg_name_1"
+ appIcon = icon1
+ }
+ session2 =
+ SessionInfo().apply {
+ sessionId = 2
+ appPackageName = "pkg_name_2"
+ appIcon = icon2
+ }
+ session3 =
+ SessionInfo().apply {
+ sessionId = 3
+ appPackageName = "pkg_name_3"
+ appIcon = icon3
+ }
+ defaultSessions = listOf(session1, session2)
+
+ whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(session1)
+ whenever(packageInstaller.getSessionInfo(2)).thenReturn(session2)
+
+ underTest =
+ PackageInstallerMonitor(
+ handler,
+ kosmos.applicationCoroutineScope,
+ logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+ packageInstaller,
+ )
+ }
+
+ @Test
+ fun installSessions_callbacksRegisteredOnlyWhenFlowIsCollected() =
+ testScope.runTest {
+ // Verify callback not added before flow is collected
+ verify(packageInstaller, never()).registerSessionCallback(any(), eq(handler))
+
+ // Start collecting the flow
+ val job =
+ backgroundScope.launch {
+ underTest.installSessionsForPrimaryUser.collect {
+ // Do nothing with the value
+ }
+ }
+ runCurrent()
+
+ // Verify callback added only after flow is collected
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // Verify callback not removed
+ verify(packageInstaller, never()).unregisterSessionCallback(any())
+
+ // Stop collecting the flow
+ job.cancel()
+ runCurrent()
+
+ // Verify callback removed once flow stops being collected
+ verify(packageInstaller).unregisterSessionCallback(eq(callback))
+ }
+
+ @Test
+ fun installSessions_newSessionsAreAdded() =
+ testScope.runTest {
+ val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // New session added
+ whenever(packageInstaller.getSessionInfo(3)).thenReturn(session3)
+ callback.onCreated(3)
+ runCurrent()
+
+ // Verify flow updated with the new session
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions + session3)
+ }
+
+ @Test
+ fun installSessions_finishedSessionsAreRemoved() =
+ testScope.runTest {
+ val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // Session 1 finished successfully
+ callback.onFinished(1, /* success = */ true)
+ runCurrent()
+
+ // Verify flow updated with session 1 removed
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions - session1)
+ }
+
+ @Test
+ fun installSessions_sessionsUpdatedOnBadgingChange() =
+ testScope.runTest {
+ val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // App icon for session 1 updated
+ val newSession =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = "pkg_name_1"
+ appIcon = mock()
+ }
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(newSession)
+ callback.onBadgingChanged(1)
+ runCurrent()
+
+ // Verify flow updated with the new session 1
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions - session1 + newSession)
+ }
+
+ private val represents =
+ Correspondence.from<PackageInstallSession, SessionInfo>(
+ { actual, expected ->
+ actual?.sessionId == expected?.sessionId &&
+ actual?.packageName == expected?.appPackageName &&
+ actual?.icon == expected?.getAppIcon()
+ },
+ "represents",
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index fe4d32d88612..6ce6cdb32a12 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -17,16 +17,18 @@
package com.android.systemui.communal.data.repository
import android.app.backup.BackupManager
-import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.ComponentName
import android.content.applicationContext
+import android.graphics.Bitmap
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.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.communal.data.backup.CommunalBackupUtils
import com.android.systemui.communal.data.db.CommunalItemRank
import com.android.systemui.communal.data.db.CommunalWidgetDao
@@ -46,10 +48,10 @@ import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
-import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
@@ -58,7 +60,6 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@@ -68,10 +69,10 @@ import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
- @Mock private lateinit var appWidgetManager: AppWidgetManager
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
- @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo
@Mock private lateinit var providerInfoA: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfoB: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfoC: AppWidgetProviderInfo
@Mock private lateinit var communalWidgetHost: CommunalWidgetHost
@Mock private lateinit var communalWidgetDao: CommunalWidgetDao
@Mock private lateinit var backupManager: BackupManager
@@ -79,9 +80,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
private lateinit var backupUtils: CommunalBackupUtils
private lateinit var logBuffer: LogBuffer
private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>>
+ private lateinit var fakeProviders: MutableStateFlow<Map<Int, AppWidgetProviderInfo?>>
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val packageChangeRepository = kosmos.fakePackageChangeRepository
private val fakeAllowlist =
listOf(
@@ -96,6 +99,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
fakeWidgets = MutableStateFlow(emptyMap())
+ fakeProviders = MutableStateFlow(emptyMap())
logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
backupUtils = CommunalBackupUtils(kosmos.applicationContext)
@@ -103,12 +107,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray())
- whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets)
+ whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders)
underTest =
CommunalWidgetRepositoryImpl(
- Optional.of(appWidgetManager),
appWidgetHost,
testScope.backgroundScope,
kosmos.testDispatcher,
@@ -117,6 +120,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
logBuffer,
backupManager,
backupUtils,
+ packageChangeRepository,
)
}
@@ -126,15 +130,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
- whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)
-
- installedProviders(listOf(stopwatchProviderInfo))
+ fakeProviders.value = mapOf(1 to providerInfoA)
val communalWidgets by collectLastValue(underTest.communalWidgets)
verify(communalWidgetDao).getWidgets()
assertThat(communalWidgets)
.containsExactly(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = communalWidgetItemEntry.widgetId,
providerInfo = providerInfoA,
priority = communalItemRankEntry.rank,
@@ -146,6 +148,102 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun communalWidgets_widgetsWithoutMatchingProvidersAreSkipped() =
+ testScope.runTest {
+ // Set up 4 widgets, but widget 3 and 4 don't have matching providers
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ CommunalItemRank(uid = 2L, rank = 2) to
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+ CommunalItemRank(uid = 3L, rank = 3) to
+ CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L),
+ CommunalItemRank(uid = 4L, rank = 4) to
+ CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L),
+ )
+ fakeProviders.value =
+ mapOf(
+ 1 to providerInfoA,
+ 2 to providerInfoB,
+ )
+
+ // Expect to see only widget 1 and 2
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 2,
+ providerInfo = providerInfoB,
+ priority = 2,
+ ),
+ )
+ }
+
+ @Test
+ fun communalWidgets_updatedWhenProvidersUpdate() =
+ testScope.runTest {
+ // Set up widgets and providers
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ CommunalItemRank(uid = 2L, rank = 2) to
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+ )
+ fakeProviders.value =
+ mapOf(
+ 1 to providerInfoA,
+ 2 to providerInfoB,
+ )
+
+ // Expect two widgets
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets).isNotNull()
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 2,
+ providerInfo = providerInfoB,
+ priority = 2,
+ ),
+ )
+
+ // Provider info updated for widget 1
+ fakeProviders.value =
+ mapOf(
+ 1 to providerInfoC,
+ 2 to providerInfoB,
+ )
+ runCurrent()
+
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ // Verify that provider info updated
+ providerInfo = providerInfoC,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 2,
+ providerInfo = providerInfoB,
+ priority = 2,
+ ),
+ )
+ }
+
+ @Test
fun addWidget_allocateId_bindWidget_andAddToDb() =
testScope.runTest {
val provider = ComponentName("pkg_name", "cls_name")
@@ -434,9 +532,102 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank)
}
- private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
- whenever(appWidgetManager.installedProviders).thenReturn(providers)
- }
+ @Test
+ fun pendingWidgets() =
+ testScope.runTest {
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ CommunalItemRank(uid = 2L, rank = 2) to
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+ )
+
+ // Widget 1 is installed
+ fakeProviders.value = mapOf(1 to providerInfoA)
+
+ // Widget 2 is pending install
+ val fakeIcon = mock<Bitmap>()
+ packageChangeRepository.setInstallSessions(
+ listOf(
+ PackageInstallSession(
+ sessionId = 1,
+ packageName = "pk_2",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ )
+ )
+ )
+
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = 2,
+ priority = 2,
+ packageName = "pk_2",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ ),
+ )
+ }
+
+ @Test
+ fun pendingWidgets_pendingWidgetBecomesAvailableAfterInstall() =
+ testScope.runTest {
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ )
+
+ // Widget 1 is pending install
+ val fakeIcon = mock<Bitmap>()
+ packageChangeRepository.setInstallSessions(
+ listOf(
+ PackageInstallSession(
+ sessionId = 1,
+ packageName = "pk_1",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ )
+ )
+ )
+
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = 1,
+ priority = 1,
+ packageName = "pk_1",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ ),
+ )
+
+ // Package for widget 1 finished installing
+ packageChangeRepository.setInstallSessions(emptyList())
+
+ // Provider info for widget 1 becomes available
+ fakeProviders.value = mapOf(1 to providerInfoA)
+
+ runCurrent()
+
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ )
+ }
private fun setAppWidgetIds(ids: List<Int>) {
whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
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 456fb79d0536..766798c2c2c3 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
@@ -23,6 +23,7 @@ import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.content.pm.UserInfo
+import android.graphics.Bitmap
import android.os.UserHandle
import android.os.UserManager
import android.os.userManager
@@ -871,7 +872,14 @@ class CommunalInteractorTest : SysuiTestCase() {
// One widget is filtered out and the remaining two link to main user id.
assertThat(checkNotNull(widgetContent).size).isEqualTo(2)
widgetContent!!.forEachIndexed { _, model ->
- assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
+ assertThat(model is CommunalContentModel.WidgetContent.Widget).isTrue()
+ assertThat(
+ (model as CommunalContentModel.WidgetContent.Widget)
+ .providerInfo
+ .profile
+ ?.identifier
+ )
+ .isEqualTo(MAIN_USER_INFO.id)
}
}
@@ -1037,9 +1045,9 @@ class CommunalInteractorTest : SysuiTestCase() {
runCurrent()
val widgetContent by collectLastValue(underTest.widgetContent)
- // Given three widgets, and one of them is associated with work profile.
+ // One available work widget, one pending work widget, and one regular available widget.
val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
val widgets = listOf(widget1, widget2, widget3)
widgetRepository.setCommunalWidgets(widgets)
@@ -1049,11 +1057,9 @@ class CommunalInteractorTest : SysuiTestCase() {
DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
)
- // Widget under work profile is filtered out and the remaining two link to main user id.
- assertThat(widgetContent).hasSize(2)
- widgetContent!!.forEach { model ->
- assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
- }
+ // Widgets under work profile are filtered out. Only the regular widget remains.
+ assertThat(widgetContent).hasSize(1)
+ assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(3)
}
@Test
@@ -1076,7 +1082,7 @@ class CommunalInteractorTest : SysuiTestCase() {
val widgetContent by collectLastValue(underTest.widgetContent)
// Given three widgets, and one of them is associated with work profile.
val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
val widgets = listOf(widget1, widget2, widget3)
widgetRepository.setCommunalWidgets(widgets)
@@ -1086,10 +1092,11 @@ class CommunalInteractorTest : SysuiTestCase() {
DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE
)
- // Widget under work profile is available.
+ // Widgets under work profile are available.
assertThat(widgetContent).hasSize(3)
- assertThat(widgetContent!![0].providerInfo.profile?.identifier)
- .isEqualTo(USER_INFO_WORK.id)
+ assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(1)
+ assertThat(widgetContent?.get(1)?.appWidgetId).isEqualTo(2)
+ assertThat(widgetContent?.get(2)?.appWidgetId).isEqualTo(3)
}
@Test
@@ -1182,8 +1189,11 @@ class CommunalInteractorTest : SysuiTestCase() {
)
}
- private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel> {
+ private fun createWidgetForUser(
+ appWidgetId: Int,
+ userId: Int
+ ): CommunalWidgetContentModel.Available =
+ mock<CommunalWidgetContentModel.Available> {
whenever(this.appWidgetId).thenReturn(appWidgetId)
val providerInfo =
mock<AppWidgetProviderInfo>().apply {
@@ -1193,11 +1203,27 @@ class CommunalInteractorTest : SysuiTestCase() {
whenever(this.providerInfo).thenReturn(providerInfo)
}
+ private fun createPendingWidgetForUser(
+ appWidgetId: Int,
+ priority: Int = 0,
+ packageName: String = "",
+ icon: Bitmap? = null,
+ userId: Int = 0,
+ ): CommunalWidgetContentModel.Pending {
+ return CommunalWidgetContentModel.Pending(
+ appWidgetId = appWidgetId,
+ priority = priority,
+ packageName = packageName,
+ icon = icon,
+ user = UserHandle(userId),
+ )
+ }
+
private fun createWidgetWithCategory(
appWidgetId: Int,
category: Int
): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel> {
+ mock<CommunalWidgetContentModel.Available> {
whenever(this.appWidgetId).thenReturn(appWidgetId)
val providerInfo = mock<AppWidgetProviderInfo>().apply { widgetCategory = category }
whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
index 89a4c04015b6..b3a12a69d1af 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
@@ -28,6 +28,8 @@ import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,6 +38,9 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -96,4 +101,137 @@ class CommunalAppWidgetHostTest : SysuiTestCase() {
assertThat(appWidgetIdToRemove).isEqualTo(2)
}
+
+ @Test
+ fun observer_onHostStartListeningTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onHostStartListening()
+ underTest.startListening()
+ runCurrent()
+ verify(observer).onHostStartListening()
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.startListening()
+ runCurrent()
+ verify(observer, never()).onHostStartListening()
+ }
+
+ @Test
+ fun observer_onHostStopListeningTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onHostStopListening()
+ underTest.stopListening()
+ runCurrent()
+ verify(observer).onHostStopListening()
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.stopListening()
+ runCurrent()
+ verify(observer, never()).onHostStopListening()
+ }
+
+ @Test
+ fun observer_onAllocateAppWidgetIdTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onAllocateAppWidgetId(any())
+ val id = underTest.allocateAppWidgetId()
+ runCurrent()
+ verify(observer).onAllocateAppWidgetId(eq(id))
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.allocateAppWidgetId()
+ runCurrent()
+ verify(observer, never()).onAllocateAppWidgetId(any())
+ }
+
+ @Test
+ fun observer_onDeleteAppWidgetIdTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onDeleteAppWidgetId(any())
+ underTest.deleteAppWidgetId(1)
+ runCurrent()
+ verify(observer).onDeleteAppWidgetId(eq(1))
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.deleteAppWidgetId(2)
+ runCurrent()
+ verify(observer, never()).onDeleteAppWidgetId(any())
+ }
+
+ @Test
+ fun observer_multipleObservers() =
+ testScope.runTest {
+ // Set up two observers
+ val observer1 = mock<CommunalAppWidgetHost.Observer>()
+ val observer2 = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer1)
+ underTest.addObserver(observer2)
+ runCurrent()
+
+ // Verify both observers triggered
+ verify(observer1, never()).onHostStartListening()
+ verify(observer2, never()).onHostStartListening()
+ underTest.startListening()
+ runCurrent()
+ verify(observer1).onHostStartListening()
+ verify(observer2).onHostStartListening()
+
+ // Observer 1 removed
+ underTest.removeObserver(observer1)
+ runCurrent()
+
+ // Verify only observer 2 is triggered
+ underTest.stopListening()
+ runCurrent()
+ verify(observer2).onHostStopListening()
+ verify(observer1, never()).onHostStopListening()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 9aebc305fb56..6ca04dfca6a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -123,12 +123,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
// Widgets available.
val widgets =
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 0,
priority = 30,
providerInfo = providerInfo,
),
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 20,
providerInfo = providerInfo,
@@ -177,12 +177,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
// Widgets available.
val widgets =
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 0,
priority = 30,
providerInfo = providerInfo,
),
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 20,
providerInfo = providerInfo,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 569116c6124a..9d6a66de367f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -186,12 +186,12 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
// Widgets available.
val widgets =
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 0,
priority = 30,
providerInfo = providerInfo,
),
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 20,
providerInfo = providerInfo,
@@ -245,7 +245,7 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
widgetRepository.setCommunalWidgets(
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 1,
providerInfo = providerInfo,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 6cae5d352fc2..3d2eabf2a07c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.communal.widgets
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
+import android.graphics.Bitmap
import android.os.UserHandle
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -60,6 +61,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
private val kosmos = testKosmos()
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+ @Mock private lateinit var communalWidgetHost: CommunalWidgetHost
private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>
@@ -78,6 +80,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
underTest =
CommunalAppWidgetHostStartable(
appWidgetHost,
+ communalWidgetHost,
kosmos.communalInteractor,
kosmos.fakeUserTracker,
kosmos.applicationCoroutineScope,
@@ -143,16 +146,44 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
}
@Test
+ fun observeHostWhenCommunalIsAvailable() =
+ with(kosmos) {
+ testScope.runTest {
+ setCommunalAvailable(true)
+ communalInteractor.setEditModeOpen(false)
+ verify(communalWidgetHost, never()).startObservingHost()
+ verify(communalWidgetHost, never()).stopObservingHost()
+
+ underTest.start()
+ runCurrent()
+
+ verify(communalWidgetHost).startObservingHost()
+ verify(communalWidgetHost, never()).stopObservingHost()
+
+ setCommunalAvailable(false)
+ runCurrent()
+
+ verify(communalWidgetHost).stopObservingHost()
+ }
+ }
+
+ @Test
fun removeAppWidgetReportedByHost() =
with(kosmos) {
testScope.runTest {
// Set up communal widgets
val widget1 =
- mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) }
+ mock<CommunalWidgetContentModel.Available> {
+ whenever(this.appWidgetId).thenReturn(1)
+ }
val widget2 =
- mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) }
+ mock<CommunalWidgetContentModel.Available> {
+ whenever(this.appWidgetId).thenReturn(2)
+ }
val widget3 =
- mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) }
+ mock<CommunalWidgetContentModel.Available> {
+ whenever(this.appWidgetId).thenReturn(3)
+ }
fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
underTest.start()
@@ -184,8 +215,9 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK),
selectedUserIndex = 0,
)
+ // One work widget, one pending work widget, and one personal widget.
val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget2 = createPendingWidgetForUser(2, USER_INFO_WORK.id)
val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
val widgets = listOf(widget1, widget2, widget3)
fakeCommunalWidgetRepository.setCommunalWidgets(widgets)
@@ -209,8 +241,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
fakeKeyguardRepository.setKeyguardShowing(true)
runCurrent()
- // Widget created for work profile is removed.
- assertThat(communalWidgets).containsExactly(widget2, widget3)
+ // Both work widgets are removed.
+ assertThat(communalWidgets).containsExactly(widget3)
}
}
@@ -227,14 +259,32 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
)
}
- private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel> {
+ private fun createWidgetForUser(
+ appWidgetId: Int,
+ userId: Int
+ ): CommunalWidgetContentModel.Available =
+ mock<CommunalWidgetContentModel.Available> {
whenever(this.appWidgetId).thenReturn(appWidgetId)
val providerInfo = mock<AppWidgetProviderInfo>()
whenever(providerInfo.profile).thenReturn(UserHandle(userId))
whenever(this.providerInfo).thenReturn(providerInfo)
}
+ private fun createPendingWidgetForUser(
+ appWidgetId: Int,
+ userId: Int,
+ priority: Int = 0,
+ packageName: String = "",
+ icon: Bitmap? = null,
+ ): CommunalWidgetContentModel.Pending =
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = appWidgetId,
+ priority = priority,
+ packageName = packageName,
+ icon = icon,
+ user = UserHandle(userId),
+ )
+
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
index 88f5e1b85840..054e516db943 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.widgets
+import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
@@ -26,6 +27,8 @@ 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.coroutines.collectValues
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
@@ -40,6 +43,7 @@ import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -47,6 +51,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -59,6 +65,10 @@ class CommunalWidgetHostTest : SysuiTestCase() {
@Mock private lateinit var appWidgetManager: AppWidgetManager
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+ @Mock private lateinit var providerInfo1: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfo2: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfo3: AppWidgetProviderInfo
+
private val selectedUserInteractor: SelectedUserInteractor by lazy {
kosmos.selectedUserInteractor
}
@@ -69,8 +79,19 @@ class CommunalWidgetHostTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(
+ appWidgetManager.bindAppWidgetIdIfAllowed(
+ any<Int>(),
+ any<UserHandle>(),
+ any<ComponentName>(),
+ any<Bundle>()
+ )
+ )
+ .thenReturn(true)
+
underTest =
CommunalWidgetHost(
+ kosmos.applicationCoroutineScope,
Optional.of(appWidgetManager),
appWidgetHost,
selectedUserInteractor,
@@ -89,15 +110,6 @@ class CommunalWidgetHostTest : SysuiTestCase() {
val user = UserHandle(checkNotNull(userId))
whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId)
- whenever(
- appWidgetManager.bindAppWidgetIdIfAllowed(
- any<Int>(),
- any<UserHandle>(),
- any<ComponentName>(),
- any<Bundle>(),
- )
- )
- .thenReturn(true)
// bind the widget with the current user when no user is explicitly set
val result = underTest.allocateIdAndBindWidget(provider)
@@ -121,15 +133,6 @@ class CommunalWidgetHostTest : SysuiTestCase() {
val user = UserHandle(0)
whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId)
- whenever(
- appWidgetManager.bindAppWidgetIdIfAllowed(
- any<Int>(),
- any<UserHandle>(),
- any<ComponentName>(),
- any<Bundle>()
- )
- )
- .thenReturn(true)
// provider and user handle are both set
val result = underTest.allocateIdAndBindWidget(provider, user)
@@ -172,6 +175,261 @@ class CommunalWidgetHostTest : SysuiTestCase() {
assertThat(result).isNull()
}
+ @Test
+ fun listener_exactlyOneListenerRegisteredForEachWidgetWhenHostStartListening() =
+ testScope.runTest {
+ // 3 widgets registered with the host
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3))
+
+ underTest.startObservingHost()
+ runCurrent()
+
+ // Make sure no listener is set before host starts listening
+ verify(appWidgetHost, never()).setListener(any(), any())
+
+ // Host starts listening
+ val observer =
+ withArgCaptor<CommunalAppWidgetHost.Observer> {
+ verify(appWidgetHost).addObserver(capture())
+ }
+ observer.onHostStartListening()
+ runCurrent()
+
+ // Verify a listener is set for each widget
+ verify(appWidgetHost, times(3)).setListener(any(), any())
+ verify(appWidgetHost).setListener(eq(1), any())
+ verify(appWidgetHost).setListener(eq(2), any())
+ verify(appWidgetHost).setListener(eq(3), any())
+ }
+
+ @Test
+ fun listener_listenersRemovedWhenHostStopListening() =
+ testScope.runTest {
+ // 3 widgets registered with the host
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3))
+
+ underTest.startObservingHost()
+ runCurrent()
+
+ // Host starts listening
+ val observer =
+ withArgCaptor<CommunalAppWidgetHost.Observer> {
+ verify(appWidgetHost).addObserver(capture())
+ }
+ observer.onHostStartListening()
+ runCurrent()
+
+ // Verify none of the listener is removed before host stop listening
+ verify(appWidgetHost, never()).removeListener(any())
+
+ observer.onHostStopListening()
+
+ // Verify each listener is removed
+ verify(appWidgetHost, times(3)).removeListener(any())
+ verify(appWidgetHost).removeListener(eq(1))
+ verify(appWidgetHost).removeListener(eq(2))
+ verify(appWidgetHost).removeListener(eq(3))
+ }
+
+ @Test
+ fun listener_addNewListenerWhenNewIdAllocated() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf())
+ val observer = start()
+
+ // Verify no listener is set before a new app widget id is allocated
+ verify(appWidgetHost, never()).setListener(any(), any())
+
+ // Allocate an app widget id
+ observer.onAllocateAppWidgetId(1)
+
+ // Verify new listener set for that app widget id
+ verify(appWidgetHost).setListener(eq(1), any())
+ }
+
+ @Test
+ fun listener_removeListenerWhenWidgetDeleted() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1))
+ val observer = start()
+
+ // Verify listener not removed before widget deleted
+ verify(appWidgetHost, never()).removeListener(eq(1))
+
+ // Widget deleted
+ observer.onDeleteAppWidgetId(1)
+
+ // Verify listener removed for that widget
+ verify(appWidgetHost).removeListener(eq(1))
+ }
+
+ @Test
+ fun providerInfo_populatesWhenStartListening() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfoValues by collectValues(underTest.appWidgetProviders)
+
+ // Assert that the map is empty before host starts listening
+ assertThat(providerInfoValues).hasSize(1)
+ assertThat(providerInfoValues[0]).isEmpty()
+
+ start()
+ runCurrent()
+
+ // Assert that the provider info map is populated after host started listening, and that
+ // all providers are emitted at once
+ assertThat(providerInfoValues).hasSize(2)
+ assertThat(providerInfoValues[1])
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+ }
+
+ @Test
+ fun providerInfo_clearsWhenStopListening() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val observer = start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Host stop listening
+ observer.onHostStopListening()
+
+ // Assert that the provider info map is cleared
+ assertThat(providerInfo).isEmpty()
+ }
+
+ @Test
+ fun providerInfo_onUpdate() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+ start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Provider info for widget 1 updated
+ val listener =
+ withArgCaptor<AppWidgetHost.AppWidgetHostListener> {
+ verify(appWidgetHost).setListener(eq(1), capture())
+ }
+ listener.onUpdateProviderInfo(providerInfo3)
+ runCurrent()
+
+ // Assert that the update is reflected in the flow
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo3),
+ Pair(2, providerInfo2),
+ )
+ )
+ }
+
+ @Test
+ fun providerInfo_updateWhenANewWidgetIsBound() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+ start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Bind a new widget
+ whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(3)
+ whenever(appWidgetManager.getAppWidgetInfo(3)).thenReturn(providerInfo3)
+ val newWidgetComponentName = ComponentName.unflattenFromString("pkg_new/cls_new")!!
+ underTest.allocateIdAndBindWidget(newWidgetComponentName)
+ runCurrent()
+
+ // Assert that the new provider is reflected in the flow
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ Pair(3, providerInfo3),
+ )
+ )
+ }
+
+ @Test
+ fun providerInfo_updateWhenWidgetRemoved() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+ val observer = start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Remove widget 1
+ observer.onDeleteAppWidgetId(1)
+ runCurrent()
+
+ // Assert that provider info for widget 1 is removed
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(2, providerInfo2),
+ )
+ )
+ }
+
private fun selectUser() {
kosmos.fakeUserRepository.selectedUser.value =
SelectedUserModel(
@@ -179,4 +437,16 @@ class CommunalWidgetHostTest : SysuiTestCase() {
selectionStatus = SelectionStatus.SELECTION_COMPLETE
)
}
+
+ private fun TestScope.start(): CommunalAppWidgetHost.Observer {
+ underTest.startObservingHost()
+ runCurrent()
+
+ val observer =
+ withArgCaptor<CommunalAppWidgetHost.Observer> {
+ verify(appWidgetHost).addObserver(capture())
+ }
+ observer.onHostStartListening()
+ return observer
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
new file mode 100644
index 000000000000..1e5599bfe1d5
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.panels.data.repository
+
+import android.content.ComponentName
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class IconAndNameCustomRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private val packageManager: PackageManager = kosmos.packageManager
+ private val userTracker: FakeUserTracker =
+ kosmos.fakeUserTracker.apply {
+ whenever(userContext.packageManager).thenReturn(packageManager)
+ }
+
+ private val service1 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component1,
+ tileService1,
+ drawable1,
+ appName1,
+ )
+
+ private val service2 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component2,
+ tileService2,
+ drawable2,
+ appName2,
+ )
+
+ private val underTest =
+ with(kosmos) {
+ IconAndNameCustomRepository(
+ installedTilesRepository,
+ userTracker,
+ mainCoroutineContext,
+ )
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(service1, service2)
+ )
+ }
+
+ @Test
+ fun loadDataForCurrentServices() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTileDataList = underTest.getCustomTileData()
+ val expectedData1 =
+ EditTileData(
+ TileSpec.create(component1),
+ Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
+ Text.Loaded(tileService1),
+ Text.Loaded(appName1),
+ )
+ val expectedData2 =
+ EditTileData(
+ TileSpec.create(component2),
+ Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)),
+ Text.Loaded(tileService2),
+ Text.Loaded(appName2),
+ )
+
+ assertThat(editTileDataList).containsExactly(expectedData1, expectedData2)
+ }
+ }
+
+ @Test
+ fun loadDataForCurrentServices_otherCurrentUser_empty() =
+ with(kosmos) {
+ testScope.runTest {
+ userTracker.set(listOf(UserInfo(11, "", 0)), 0)
+ val editTileDataList = underTest.getCustomTileData()
+
+ assertThat(editTileDataList).isEmpty()
+ }
+ }
+
+ @Test
+ fun loadDataForCurrentServices_serviceInfoWithNullIcon_notInList() =
+ with(kosmos) {
+ testScope.runTest {
+ val serviceNullIcon =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component2,
+ tileService2,
+ )
+ fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(service1, serviceNullIcon)
+ )
+
+ val expectedData1 =
+ EditTileData(
+ TileSpec.create(component1),
+ Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
+ Text.Loaded(tileService1),
+ Text.Loaded(appName1),
+ )
+
+ val editTileDataList = underTest.getCustomTileData()
+ assertThat(editTileDataList).containsExactly(expectedData1)
+ }
+ }
+
+ private companion object {
+ val drawable1 = TestStubDrawable("drawable1")
+ val appName1 = "App1"
+ val tileService1 = "Tile Service 1"
+ val component1 = ComponentName("pkg1", "srv1")
+
+ val drawable2 = TestStubDrawable("drawable2")
+ val appName2 = "App2"
+ val tileService2 = "Tile Service 2"
+ val component2 = ComponentName("pkg2", "srv2")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt
new file mode 100644
index 000000000000..56cead19d1df
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.panels.data.repository
+
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StockTilesRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private val underTest = StockTilesRepository(kosmos.mainResources)
+
+ @Test
+ fun stockTilesMatchesResources() {
+ val expected =
+ kosmos.mainResources
+ .getString(R.string.quick_settings_tiles_stock)
+ .split(",")
+ .map(TileSpec::create)
+ assertThat(underTest.stockTiles).isEqualTo(expected)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
new file mode 100644
index 000000000000..deefbf585ba9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.panels.domain.interactor
+
+import android.content.ComponentName
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EditTilesListInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ // Only have some configurations so we can test the effect of missing configurations.
+ // As the configurations are injected by dagger, we'll have all the existing configurations
+ private val internetTileConfig = kosmos.qsInternetTileConfig
+ private val flashlightTileConfig = kosmos.qsFlashlightTileConfig
+ private val batteryTileConfig = kosmos.qsBatterySaverTileConfig
+
+ private val serviceInfo =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component,
+ tileName,
+ icon,
+ appName,
+ )
+
+ private val underTest =
+ with(kosmos) {
+ EditTilesListInteractor(
+ stockTilesRepository,
+ qSTileConfigProvider,
+ iconAndNameCustomRepository,
+ )
+ }
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(serviceInfo)
+ )
+
+ with(fakeQSTileConfigProvider) {
+ putConfig(internetTileConfig.tileSpec, internetTileConfig)
+ putConfig(flashlightTileConfig.tileSpec, flashlightTileConfig)
+ putConfig(batteryTileConfig.tileSpec, batteryTileConfig)
+ }
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_stockTilesHaveNoAppName() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(editTiles.stockTiles.all { it.appName == null }).isTrue()
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_stockTilesAreAllPlatformSpecs() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(editTiles.stockTiles.all { it.tileSpec is TileSpec.PlatformTileSpec })
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_stockTiles_sameOrderAsRepository() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(editTiles.stockTiles.map { it.tileSpec })
+ .isEqualTo(stockTilesRepository.stockTiles)
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_customTileData_matchesService() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+ val expected =
+ EditTileData(
+ tileSpec = TileSpec.create(component),
+ icon = Icon.Loaded(icon, ContentDescription.Loaded(tileName)),
+ label = Text.Loaded(tileName),
+ appName = Text.Loaded(appName),
+ )
+
+ assertThat(editTiles.customTiles).hasSize(1)
+ assertThat(editTiles.customTiles[0]).isEqualTo(expected)
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_tilesInConfigProvider_correctData() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(
+ editTiles.stockTiles.first { it.tileSpec == internetTileConfig.tileSpec }
+ )
+ .isEqualTo(internetTileConfig.toEditTileData())
+ assertThat(
+ editTiles.stockTiles.first { it.tileSpec == flashlightTileConfig.tileSpec }
+ )
+ .isEqualTo(flashlightTileConfig.toEditTileData())
+ assertThat(editTiles.stockTiles.first { it.tileSpec == batteryTileConfig.tileSpec })
+ .isEqualTo(batteryTileConfig.toEditTileData())
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_tilesNotInConfigProvider_useDefaultData() =
+ with(kosmos) {
+ testScope.runTest {
+ underTest
+ .getTilesToEdit()
+ .stockTiles
+ .filterNot { qSTileConfigProvider.hasConfig(it.tileSpec.spec) }
+ .forEach { assertThat(it).isEqualTo(it.tileSpec.missingConfigEditTileData()) }
+ }
+ }
+
+ private companion object {
+ val component = ComponentName("pkg", "srv")
+ const val tileName = "Tile Service"
+ const val appName = "App"
+ val icon = TestStubDrawable("icon")
+
+ fun TileSpec.missingConfigEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = this,
+ icon = Icon.Resource(android.R.drawable.star_on, ContentDescription.Loaded(spec)),
+ label = Text.Loaded(spec),
+ appName = null
+ )
+ }
+
+ fun QSTileConfig.toEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = tileSpec,
+ icon =
+ Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
+ label = Text.Resource(uiConfig.labelRes),
+ appName = null,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
new file mode 100644
index 000000000000..9fb25a28415b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
@@ -0,0 +1,507 @@
+/*
+ * 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.panels.ui.viewmodel
+
+import android.R
+import android.content.ComponentName
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.qsTileFactory
+import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.impl.sensorprivacy.qsCameraSensorPrivacyToggleTileConfig
+import com.android.systemui.qs.tiles.impl.sensorprivacy.qsMicrophoneSensorPrivacyToggleTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EditModeViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ // Only have some configurations so we can test the effect of missing configurations.
+ // As the configurations are injected by dagger, we'll have all the existing configurations
+ private val configs =
+ with(kosmos) {
+ setOf(
+ qsInternetTileConfig,
+ qsFlashlightTileConfig,
+ qsBatterySaverTileConfig,
+ qsAlarmTileConfig,
+ qsCameraSensorPrivacyToggleTileConfig,
+ qsMicrophoneSensorPrivacyToggleTileConfig,
+ )
+ }
+
+ private val serviceInfo1 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component1,
+ tileService1,
+ drawable1,
+ appName1,
+ )
+
+ private val serviceInfo2 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component2,
+ tileService2,
+ drawable2,
+ appName2,
+ )
+
+ private val underTest: EditModeViewModel by lazy {
+ with(kosmos) {
+ EditModeViewModel(
+ editTilesListInteractor,
+ currentTilesInteractor,
+ minimumTilesInteractor,
+ infiniteGridLayout,
+ applicationCoroutineScope,
+ gridLayoutTypeInteractor,
+ gridLayoutMap,
+ )
+ }
+ }
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ fakeMinimumTilesRepository = MinimumTilesFixedRepository(minNumberOfTiles)
+
+ fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(serviceInfo1, serviceInfo2)
+ )
+
+ with(fakeQSTileConfigProvider) { configs.forEach { putConfig(it.tileSpec, it) } }
+ qsTileFactory = FakeQSFactory { FakeQSTile(userTracker.userId, available = true) }
+ }
+ }
+
+ @Test
+ fun isEditing() =
+ with(kosmos) {
+ testScope.runTest {
+ val isEditing by collectLastValue(underTest.isEditing)
+
+ assertThat(isEditing).isFalse()
+
+ underTest.startEditing()
+ assertThat(isEditing).isTrue()
+
+ underTest.stopEditing()
+ assertThat(isEditing).isFalse()
+ }
+ }
+
+ @Test
+ fun editing_false_emptyFlowOfTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ assertThat(tiles).isNull()
+ }
+ }
+
+ @Test
+ fun editing_true_notEmptyTileData() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ assertThat(tiles).isNotEmpty()
+ }
+ }
+
+ @Test
+ fun tilesData_hasAllStockTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ assertThat(
+ tiles!!
+ .filter { it.tileSpec is TileSpec.PlatformTileSpec }
+ .map { it.tileSpec }
+ )
+ .containsExactlyElementsIn(stockTilesRepository.stockTiles)
+ }
+ }
+
+ @Test
+ fun tilesData_stockTiles_haveCorrectUiValues() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.tileSpec is TileSpec.PlatformTileSpec }
+ .forEach {
+ val data = getEditTileData(it.tileSpec)
+
+ assertThat(it.label).isEqualTo(data.label)
+ assertThat(it.icon).isEqualTo(data.icon)
+ assertThat(it.appName).isNull()
+ }
+ }
+ }
+
+ @Test
+ fun tilesData_hasAllCustomTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ assertThat(
+ tiles!!
+ .filter { it.tileSpec is TileSpec.CustomTileSpec }
+ .map { it.tileSpec }
+ )
+ .containsExactly(TileSpec.create(component1), TileSpec.create(component2))
+ }
+ }
+
+ @Test
+ fun tilesData_customTiles_haveCorrectUiValues() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ // service1
+ val model1 = tiles!!.first { it.tileSpec == TileSpec.create(component1) }
+ assertThat(model1.label).isEqualTo(Text.Loaded(tileService1))
+ assertThat(model1.appName).isEqualTo(Text.Loaded(appName1))
+ assertThat(model1.icon)
+ .isEqualTo(Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)))
+
+ // service2
+ val model2 = tiles!!.first { it.tileSpec == TileSpec.create(component2) }
+ assertThat(model2.label).isEqualTo(Text.Loaded(tileService2))
+ assertThat(model2.appName).isEqualTo(Text.Loaded(appName2))
+ assertThat(model2.icon)
+ .isEqualTo(Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)))
+ }
+ }
+
+ @Test
+ fun currentTiles_inCorrectOrder_markedAsCurrent() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ listOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(currentTiles)
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun notCurrentTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ listOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ val remainingTiles =
+ stockTilesRepository.stockTiles.filterNot { it in currentTiles } +
+ listOf(TileSpec.create(component1))
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(remainingTiles)
+ }
+ }
+
+ @Test
+ fun currentTilesChange_trackingChange() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ val newTile = TileSpec.create("internet")
+ val position = 1
+ currentTilesInteractor.addTile(newTile, position)
+ currentTiles.add(position, newTile)
+
+ assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(currentTiles)
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun nonCurrentTiles_orderPreservedWhenCurrentTilesChange() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ val nonCurrentSpecs = tiles!!.filterNot { it.isCurrent }.map { it.tileSpec }
+ val newTile = TileSpec.create("internet")
+ currentTilesInteractor.addTile(newTile)
+
+ assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(nonCurrentSpecs - listOf(newTile))
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun nonCurrentTiles_haveOnlyAddAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filterNot { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions)
+ .containsExactly(AvailableEditActions.ADD)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_moreThanMinimumTiles_haveRemoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+ assertThat(currentTiles.size).isGreaterThan(minNumberOfTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions).contains(AvailableEditActions.REMOVE)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_minimumTiles_dontHaveRemoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+ assertThat(currentTiles.size).isEqualTo(minNumberOfTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions)
+ .doesNotContain(AvailableEditActions.REMOVE)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_lessThanMinimumTiles_dontHaveRemoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+ assertThat(currentTiles.size).isLessThan(minNumberOfTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions)
+ .doesNotContain(AvailableEditActions.REMOVE)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_haveMoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions).contains(AvailableEditActions.MOVE)
+ }
+ }
+ }
+
+ private companion object {
+ val drawable1 = TestStubDrawable("drawable1")
+ val appName1 = "App1"
+ val tileService1 = "Tile Service 1"
+ val component1 = ComponentName("pkg1", "srv1")
+
+ val drawable2 = TestStubDrawable("drawable2")
+ val appName2 = "App2"
+ val tileService2 = "Tile Service 2"
+ val component2 = ComponentName("pkg2", "srv2")
+
+ fun TileSpec.missingConfigEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = this,
+ icon = Icon.Resource(R.drawable.star_on, ContentDescription.Loaded(spec)),
+ label = Text.Loaded(spec),
+ appName = null
+ )
+ }
+
+ fun QSTileConfig.toEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = tileSpec,
+ icon =
+ Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
+ label = Text.Resource(uiConfig.labelRes),
+ appName = null,
+ )
+ }
+
+ fun Kosmos.getEditTileData(tileSpec: TileSpec): EditTileData {
+ return if (qSTileConfigProvider.hasConfig(tileSpec.spec)) {
+ qSTileConfigProvider.getConfig(tileSpec.spec).toEditTileData()
+ } else {
+ tileSpec.missingConfigEditTileData()
+ }
+ }
+
+ val minNumberOfTiles = 3
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index bc57ce6f95f5..a0dec8cb745d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -90,7 +90,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
}
@Test
- fun componentsLoadedOnStart() =
+ fun servicesLoadedOnStart() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -106,12 +106,14 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
runCurrent()
+ val services = underTest.getInstalledTilesServiceInfos(userId)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
+ assertThat(services).containsExactly(resolveInfo.serviceInfo)
}
@Test
- fun componentAdded_foundAfterPackageChange() =
+ fun serviceAdded_foundAfterPackageChange() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -132,12 +134,14 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
.thenReturn(listOf(resolveInfo))
kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
runCurrent()
+ val services = underTest.getInstalledTilesServiceInfos(userId)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
+ assertThat(services).containsExactly(resolveInfo.serviceInfo)
}
@Test
- fun componentWithoutPermission_notValid() =
+ fun serviceWithoutPermission_notValid() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -152,13 +156,15 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ val services = underTest.getInstalledTilesServiceInfos(userId)
runCurrent()
assertThat(componentNames).isEmpty()
+ assertThat(services).isEmpty()
}
@Test
- fun componentNotEnabled_notValid() =
+ fun serviceNotEnabled_notValid() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -173,9 +179,11 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ val services = underTest.getInstalledTilesServiceInfos(userId)
runCurrent()
assertThat(componentNames).isEmpty()
+ assertThat(services).isEmpty()
}
@Test
@@ -221,30 +229,22 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() {
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
runCurrent()
+ val service = underTest.getInstalledTilesServiceInfos(userId)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
+ assertThat(service).containsExactly(resolveInfo.serviceInfo)
}
@Test
- fun loadComponentsForSameUserTwice_returnsSameFlow() =
+ fun loadServicesForSameUserTwice_returnsSameFlow() =
testScope.runTest {
- val flowForUser1 = underTest.getInstalledTilesComponents(1)
- val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1)
+ val flowForUser1 = underTest.getInstalledTilesServiceInfos(1)
+ val flowForUser1TheSecondTime = underTest.getInstalledTilesServiceInfos(1)
runCurrent()
assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1)
}
- @Test
- fun loadComponentsForDifferentUsers_returnsDifferentFlow() =
- testScope.runTest {
- val flowForUser1 = underTest.getInstalledTilesComponents(1)
- val flowForUser2 = underTest.getInstalledTilesComponents(2)
- runCurrent()
-
- assertThat(flowForUser2).isNotEqualTo(flowForUser1)
- }
-
// Tests that a ServiceInfo that is returned by queryIntentServicesAsUser but shortly
// after uninstalled, doesn't crash SystemUI.
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
index 61e4774d9c92..3faab5048fb4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
@@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.domain.model.TileModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.ColorCorrectionTile
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
index 8ae917264a37..167eff193147 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
@@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 634c5fa74295..1c73fe2b305d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -32,6 +32,7 @@ import com.android.systemui.dump.nano.SystemUIProtoDump
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.external.CustomTile
import com.android.systemui.qs.external.CustomTileStatePersister
import com.android.systemui.qs.external.TileLifecycleManager
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
index 90c83047e72f..260189d401d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.repository.FakeDefaultTilesRepository
import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
index 4207a9c27ad0..dffd0d72969c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository
import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository
@@ -54,9 +55,7 @@ import org.junit.runner.RunWith
@OptIn(ExperimentalCoroutinesApi::class)
class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() {
- private val kosmos by lazy {
- Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) }
- }
+ private val kosmos by lazy { Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } }
// Getter here so it can change when there is a managed profile.
private val workTileAvailable: Boolean
get() = hasManagedProfile()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt
new file mode 100644
index 000000000000..a2ffe7008009
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.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.compose.animation.scene.SwipeDirection
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class GoneSceneViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val shadeRepository by lazy { kosmos.shadeRepository }
+ private lateinit var underTest: GoneSceneViewModel
+
+ @Before
+ fun setUp() {
+ underTest =
+ GoneSceneViewModel(
+ applicationScope = testScope.backgroundScope,
+ shadeInteractor = kosmos.shadeInteractor,
+ )
+ }
+
+ @Test
+ fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Split)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey)
+ .isEqualTo(GoneToSplitShade)
+ }
+
+ @Test
+ fun downTransitionKey_splitShadeDisabled_isNull() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Single)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 5312ad809a72..243921753f22 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -39,6 +39,7 @@ import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -159,6 +160,27 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
}
@Test
+ fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Split)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey)
+ .isEqualTo(GoneToSplitShade)
+ }
+
+ @Test
+ fun upTransitionKey_splitShadeDisabled_isNull() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Single)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull()
+ }
+
+ @Test
fun isClickable_deviceUnlocked_false() =
testScope.runTest {
val isClickable by collectLastValue(underTest.isClickable)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
index 03a39f8f07d8..2d8cd930d8fa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -23,9 +23,9 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,21 +45,21 @@ class BooleanFlowOperatorsTest : SysuiTestCase() {
@Test
fun and_allTrue_returnsTrue() =
testScope.runTest {
- val result by collectLastValue(and(TRUE, TRUE))
+ val result by collectLastValue(allOf(TRUE, TRUE))
assertThat(result).isTrue()
}
@Test
fun and_anyFalse_returnsFalse() =
testScope.runTest {
- val result by collectLastValue(and(TRUE, FALSE, TRUE))
+ val result by collectLastValue(allOf(TRUE, FALSE, TRUE))
assertThat(result).isFalse()
}
@Test
fun and_allFalse_returnsFalse() =
testScope.runTest {
- val result by collectLastValue(and(FALSE, FALSE, FALSE))
+ val result by collectLastValue(allOf(FALSE, FALSE, FALSE))
assertThat(result).isFalse()
}
@@ -68,7 +68,7 @@ class BooleanFlowOperatorsTest : SysuiTestCase() {
testScope.runTest {
val flow1 = MutableStateFlow(false)
val flow2 = MutableStateFlow(false)
- val values by collectValues(and(flow1, flow2))
+ val values by collectValues(allOf(flow1, flow2))
assertThat(values).containsExactly(false)
flow1.value = true
@@ -81,21 +81,21 @@ class BooleanFlowOperatorsTest : SysuiTestCase() {
@Test
fun or_allTrue_returnsTrue() =
testScope.runTest {
- val result by collectLastValue(or(TRUE, TRUE))
+ val result by collectLastValue(anyOf(TRUE, TRUE))
assertThat(result).isTrue()
}
@Test
fun or_anyTrue_returnsTrue() =
testScope.runTest {
- val result by collectLastValue(or(FALSE, TRUE, FALSE))
+ val result by collectLastValue(anyOf(FALSE, TRUE, FALSE))
assertThat(result).isTrue()
}
@Test
fun or_allFalse_returnsFalse() =
testScope.runTest {
- val result by collectLastValue(or(FALSE, FALSE, FALSE))
+ val result by collectLastValue(anyOf(FALSE, FALSE, FALSE))
assertThat(result).isFalse()
}
@@ -104,7 +104,7 @@ class BooleanFlowOperatorsTest : SysuiTestCase() {
testScope.runTest {
val flow1 = MutableStateFlow(false)
val flow2 = MutableStateFlow(false)
- val values by collectValues(or(flow1, flow2))
+ val values by collectValues(anyOf(flow1, flow2))
assertThat(values).containsExactly(false)
flow1.value = true
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index c43e394cb97a..da12dd731c23 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -36,7 +36,6 @@
</style>
<style name="Keyguard.Bouncer.SecondaryMessage" parent="Theme.SystemUI">
<item name="android:textSize">14sp</item>
- <item name="android:lineHeight">20dp</item>
<item name="android:maxLines">@integer/bouncer_secondary_message_lines</item>
<item name="android:lines">@integer/bouncer_secondary_message_lines</item>
<item name="android:textAlignment">center</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 45bcd829336f..b1ba0882d9ba 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1158,6 +1158,8 @@
<string name="button_to_configure_widgets_text">Customize widgets</string>
<!-- Description for the App icon of disabled widget. [CHAR LIMIT=NONE] -->
<string name="icon_description_for_disabled_widget">App icon for disabled widget</string>
+ <!-- Description for the App icon of a package that is currently being installed. [CHAR LIMIT=NONE] -->
+ <string name="icon_description_for_pending_widget">App icon for a widget being installed</string>
<!-- Label for the button which configures widgets [CHAR LIMIT=NONE] -->
<string name="edit_widget">Edit widget</string>
<!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 91fb6888bf06..905a98c2e181 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -35,6 +35,7 @@ import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -134,6 +135,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
private final BouncerMessageInteractor mBouncerMessageInteractor;
private int mTranslationY;
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private final DevicePolicyManager mDevicePolicyManager;
// Whether the volume keys should be handled by keyguard. If true, then
// they will be handled here for specific media types such as music, otherwise
// the audio service will bring up the volume dialog.
@@ -460,6 +462,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
SelectedUserInteractor selectedUserInteractor,
DeviceProvisionedController deviceProvisionedController,
FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
+ DevicePolicyManager devicePolicyManager,
KeyguardTransitionInteractor keyguardTransitionInteractor,
Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
Provider<DeviceEntryInteractor> deviceEntryInteractor
@@ -495,6 +498,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mDeviceProvisionedController = deviceProvisionedController;
mPrimaryBouncerInteractor = primaryBouncerInteractor;
+ mDevicePolicyManager = devicePolicyManager;
}
@Override
@@ -1105,35 +1109,23 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts);
- final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager();
final int failedAttemptsBeforeWipe =
- dpm.getMaximumFailedPasswordsForWipe(null, userId);
+ mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId);
final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0
? (failedAttemptsBeforeWipe - failedAttempts)
: Integer.MAX_VALUE; // because DPM returns 0 if no restriction
if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) {
- // The user has installed a DevicePolicyManager that requests a user/profile to be wiped
- // N attempts. Once we get below the grace period, we post this dialog every time as a
- // clear warning until the deletion fires.
- // Check which profile has the strictest policy for failed password attempts
- final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId);
- int userType = USER_TYPE_PRIMARY;
- if (expiringUser == userId) {
- // TODO: http://b/23522538
- if (expiringUser != UserHandle.USER_SYSTEM) {
- userType = USER_TYPE_SECONDARY_USER;
- }
- } else if (expiringUser != UserHandle.USER_NULL) {
- userType = USER_TYPE_WORK_PROFILE;
- } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
- if (remainingBeforeWipe > 0) {
- mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType);
- } else {
- // Too many attempts. The device will be wiped shortly.
- Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!");
- mView.showWipeDialog(failedAttempts, userType);
- }
+ // The user has installed a DevicePolicyManager that requests a
+ // user/profile to be wiped N attempts. Once we get below the grace period,
+ // we post this dialog every time as a clear warning until the deletion
+ // fires. Check which profile has the strictest policy for failed password
+ // attempts.
+ final int expiringUser =
+ mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId);
+ Integer mainUser = mSelectedUserInteractor.getMainUserId();
+ showMessageForFailedUnlockAttempt(
+ userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts);
}
mLockPatternUtils.reportFailedPasswordAttempt(userId);
if (timeoutMs > 0) {
@@ -1145,6 +1137,35 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
}
}
+ @VisibleForTesting
+ void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId,
+ int remainingBeforeWipe, int failedAttempts) {
+ int userType = USER_TYPE_PRIMARY;
+ if (expiringUserId == userId) {
+ int primaryUser = UserHandle.USER_SYSTEM;
+ if (Flags.headlessSingleUserFixes()) {
+ if (mainUserId != null) {
+ primaryUser = mainUserId;
+ }
+ }
+ // TODO: http://b/23522538
+ if (expiringUserId != primaryUser) {
+ userType = USER_TYPE_SECONDARY_USER;
+ }
+ } else if (expiringUserId != UserHandle.USER_NULL) {
+ userType = USER_TYPE_WORK_PROFILE;
+ } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
+ if (remainingBeforeWipe > 0) {
+ mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe,
+ userType);
+ } else {
+ // Too many attempts. The device will be wiped shortly.
+ Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId
+ + " will be wiped!");
+ mView.showWipeDialog(failedAttempts, userType);
+ }
+ }
+
private void getCurrentSecurityController(
KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) {
mSecurityViewFlipperController
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5df7fc9865ff..fcba425f0956 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.authentication.domain.interactor
+import android.app.admin.flags.Flags
import android.os.UserHandle
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternView
@@ -288,9 +289,15 @@ constructor(
private suspend fun getWipeTarget(): WipeTarget {
// Check which profile has the strictest policy for failed authentication attempts.
val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe()
+ val primaryUser =
+ if (Flags.headlessSingleUserFixes()) {
+ selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM
+ } else {
+ UserHandle.USER_SYSTEM
+ }
return when (userToBeWiped) {
selectedUserInteractor.getSelectedUserId() ->
- if (userToBeWiped == UserHandle.USER_SYSTEM) {
+ if (userToBeWiped == primaryUser) {
WipeTarget.WholeDevice
} else {
WipeTarget.User
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt
new file mode 100644
index 000000000000..2e9169e03d80
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.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.bluetooth.qsdialog
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface BluetoothTileDialogModule {
+ @Binds
+ @SysUISingleton
+ fun bindDeviceItemActionInteractor(
+ impl: DeviceItemActionInteractorImpl
+ ): DeviceItemActionInteractor
+}
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 4369f3f64613..94f465d3c1c3 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -62,6 +62,7 @@ internal class BluetoothTileDialogViewModel
@Inject
constructor(
private val deviceItemInteractor: DeviceItemInteractor,
+ private val deviceItemActionInteractor: DeviceItemActionInteractor,
private val bluetoothStateInteractor: BluetoothStateInteractor,
private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
private val audioSharingInteractor: AudioSharingInteractor,
@@ -192,7 +193,7 @@ constructor(
// deviceItemClick is emitted when user clicked on a device item.
dialogDelegate.deviceItemClick
- .onEach { deviceItemInteractor.updateDeviceItemOnClick(it) }
+ .onEach { deviceItemActionInteractor.onClick(it, dialog) }
.launchIn(this)
// contentHeight is emitted when the dialog is dismissed.
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
new file mode 100644
index 000000000000..931176003b1b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Defines interface for click handling of a DeviceItem. */
+interface DeviceItemActionInteractor {
+ suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+}
+
+@SysUISingleton
+open class DeviceItemActionInteractorImpl
+@Inject
+constructor(
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val logger: BluetoothTileDialogLogger,
+ private val uiEventLogger: UiEventLogger,
+) : DeviceItemActionInteractor {
+
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ withContext(backgroundDispatcher) {
+ logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
+
+ deviceItem.cachedBluetoothDevice.apply {
+ when (deviceItem.type) {
+ DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
+ disconnect()
+ uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT)
+ }
+ DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
+ }
+ DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
+ setActive()
+ uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
+ }
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {
+ disconnect()
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT
+ )
+ }
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+ connect()
+ uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 66e593b94b21..1526cd9675c7 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -20,7 +20,6 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.media.AudioManager
-import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -52,7 +51,6 @@ constructor(
private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
private val localBluetoothManager: LocalBluetoothManager?,
private val systemClock: SystemClock,
- private val uiEventLogger: UiEventLogger,
private val logger: BluetoothTileDialogLogger,
@Application private val coroutineScope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
@@ -169,38 +167,6 @@ constructor(
)
}
- internal suspend fun updateDeviceItemOnClick(deviceItem: DeviceItem) {
- withContext(backgroundDispatcher) {
- logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
-
- deviceItem.cachedBluetoothDevice.apply {
- when (deviceItem.type) {
- DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
- disconnect()
- uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT)
- }
- DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
- uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
- }
- DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
- setActive()
- uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
- }
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {
- disconnect()
- uiEventLogger.log(
- BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT
- )
- }
- DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
- connect()
- uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
- }
- }
- }
- }
- }
-
internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
deviceItemFactoryList = list
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index fa19bf478453..e0334a060ee2 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -29,7 +29,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.time.SystemClock
import dagger.Lazy
import javax.inject.Inject
@@ -78,7 +78,7 @@ constructor(
bouncerRepository.alternateBouncerUIAvailable
}
private val isDozingOrAod: Flow<Boolean> =
- or(
+ anyOf(
keyguardTransitionInteractor.get().transitionValue(KeyguardState.DOZING).map {
it > 0f
},
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index beaa170943fd..b42a903878a7 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -206,7 +206,7 @@ class FalsingCollectorImpl implements FalsingCollector {
);
final CommunalInteractor communalInteractor = mCommunalInteractorLazy.get();
mJavaAdapter.alwaysCollectFlow(
- BooleanFlowOperators.INSTANCE.and(
+ BooleanFlowOperators.INSTANCE.allOf(
communalInteractor.isCommunalEnabled(),
communalInteractor.isCommunalShowing()),
this::onShowingCommunalHubChanged
@@ -292,6 +292,7 @@ class FalsingCollectorImpl implements FalsingCollector {
@Override
public void onKeyEvent(KeyEvent ev) {
+ logDebug("REAL: onKeyEvent(" + KeyEvent.actionToString(ev.getAction()) + ")");
// Only collect if it is an ACTION_UP action and is allow-listed
if (ev.getAction() == KeyEvent.ACTION_UP && mAcceptedKeycodes.contains(ev.getKeyCode())) {
mFalsingDataProvider.onKeyEvent(ev);
@@ -300,7 +301,7 @@ class FalsingCollectorImpl implements FalsingCollector {
@Override
public void onTouchEvent(MotionEvent ev) {
- logDebug("REAL: onTouchEvent(" + ev.getActionMasked() + ")");
+ logDebug("REAL: onTouchEvent(" + MotionEvent.actionToString(ev.getActionMasked()) + ")");
if (!mKeyguardStateController.isShowing()) {
avoidGesture();
return;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt
index b289fa49d06c..6b22137e455e 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt
@@ -61,11 +61,11 @@ class FalsingCollectorNoOp @Inject constructor() : FalsingCollector {
}
override fun onKeyEvent(ev: KeyEvent) {
- logDebug("NOOP: onKeyEvent(${ev.action}")
+ logDebug("NOOP: onKeyEvent(${KeyEvent.actionToString(ev.action)}")
}
override fun onTouchEvent(ev: MotionEvent) {
- logDebug("NOOP: onTouchEvent(${ev.actionMasked})")
+ logDebug("NOOP: onTouchEvent(${MotionEvent.actionToString(ev.actionMasked)})")
}
override fun onMotionEventComplete() {
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
index 5c64dc645283..1c1642905d7d 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
@@ -18,6 +18,7 @@ package com.android.systemui.common.data.repository
import android.os.UserHandle
import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
import kotlinx.coroutines.flow.Flow
interface PackageChangeRepository {
@@ -28,4 +29,7 @@ interface PackageChangeRepository {
* [UserHandle.USER_ALL] may be used to listen to all users.
*/
fun packageChanged(user: UserHandle): Flow<PackageChangeModel>
+
+ /** Emits a list of all known install sessions associated with the primary user. */
+ val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>>
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
index 712a3527b7c9..41b03f1f3de6 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
@@ -18,6 +18,7 @@ package com.android.systemui.common.data.repository
import android.os.UserHandle
import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.filter
class PackageChangeRepositoryImpl
@Inject
constructor(
+ packageInstallerMonitor: PackageInstallerMonitor,
private val monitorFactory: PackageUpdateMonitor.Factory,
) : PackageChangeRepository {
/**
@@ -37,4 +39,7 @@ constructor(
override fun packageChanged(user: UserHandle): Flow<PackageChangeModel> =
monitor.packageChanged.filter { user == UserHandle.ALL || user == it.user }
+
+ override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+ packageInstallerMonitor.installSessionsForPrimaryUser
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
new file mode 100644
index 000000000000..46db34618c70
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.common.data.repository
+
+import android.content.pm.PackageInstaller
+import android.os.Handler
+import com.android.internal.annotations.GuardedBy
+import com.android.systemui.common.shared.model.PackageInstallSession
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.PackageChangeRepoLog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/** Monitors package install sessions for all users. */
+@SysUISingleton
+class PackageInstallerMonitor
+@Inject
+constructor(
+ @Background private val bgHandler: Handler,
+ @Background private val bgScope: CoroutineScope,
+ @PackageChangeRepoLog logBuffer: LogBuffer,
+ private val packageInstaller: PackageInstaller,
+) : PackageInstaller.SessionCallback() {
+
+ private val logger = Logger(logBuffer, TAG)
+
+ @GuardedBy("sessions") private val sessions = mutableMapOf<Int, PackageInstallSession>()
+
+ private val _installSessions =
+ MutableStateFlow<List<PackageInstallSession>>(emptyList()).apply {
+ subscriptionCount
+ .map { count -> count > 0 }
+ .distinctUntilChanged()
+ // Drop initial false value
+ .dropWhile { !it }
+ .onEach { isActive ->
+ if (isActive) {
+ synchronized(sessions) {
+ sessions.putAll(
+ packageInstaller.allSessions
+ .map { session -> session.toModel() }
+ .associateBy { it.sessionId }
+ )
+ updateInstallerSessionsFlow()
+ }
+ packageInstaller.registerSessionCallback(
+ this@PackageInstallerMonitor,
+ bgHandler
+ )
+ } else {
+ synchronized(sessions) {
+ sessions.clear()
+ updateInstallerSessionsFlow()
+ }
+ packageInstaller.unregisterSessionCallback(this@PackageInstallerMonitor)
+ }
+ }
+ .launchIn(bgScope)
+ }
+
+ val installSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+ _installSessions.asStateFlow()
+
+ /** Called when a new installer session is created. */
+ override fun onCreated(sessionId: Int) {
+ logger.i({ "session created $int1" }) { int1 = sessionId }
+ updateSession(sessionId)
+ }
+
+ /** Called when new installer session has finished. */
+ override fun onFinished(sessionId: Int, success: Boolean) {
+ logger.i({ "session finished $int1" }) { int1 = sessionId }
+ synchronized(sessions) {
+ sessions.remove(sessionId)
+ updateInstallerSessionsFlow()
+ }
+ }
+
+ /**
+ * Badging details for the session changed. For example, the app icon or label has been updated.
+ */
+ override fun onBadgingChanged(sessionId: Int) {
+ logger.i({ "session badging changed $int1" }) { int1 = sessionId }
+ updateSession(sessionId)
+ }
+
+ /**
+ * A session is considered active when there is ongoing forward progress being made. For
+ * example, a package started downloading.
+ */
+ override fun onActiveChanged(sessionId: Int, active: Boolean) {
+ // Active status updates are not tracked for now
+ }
+
+ override fun onProgressChanged(sessionId: Int, progress: Float) {
+ // Progress updates are not tracked for now
+ }
+
+ private fun updateSession(sessionId: Int) {
+ val session = packageInstaller.getSessionInfo(sessionId)
+
+ synchronized(sessions) {
+ if (session == null) {
+ sessions.remove(sessionId)
+ } else {
+ sessions[sessionId] = session.toModel()
+ }
+ updateInstallerSessionsFlow()
+ }
+ }
+
+ @GuardedBy("sessions")
+ private fun updateInstallerSessionsFlow() {
+ _installSessions.value = sessions.values.toList()
+ }
+
+ companion object {
+ const val TAG = "PackageInstallerMonitor"
+
+ private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+ return PackageInstallSession(
+ sessionId = this.sessionId,
+ packageName = this.appPackageName,
+ icon = this.getAppIcon(),
+ user = this.user,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt
new file mode 100644
index 000000000000..7025229743b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.common.shared.model
+
+import android.graphics.Bitmap
+import android.os.UserHandle
+
+/** Represents a session of a package being installed on device. */
+data class PackageInstallSession(
+ val sessionId: Int,
+ val packageName: String,
+ val icon: Bitmap?,
+ val user: UserHandle,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 1f54e70fa21b..fdb797d5ba06 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -17,14 +17,13 @@
package com.android.systemui.communal.data.repository
import android.app.backup.BackupManager
-import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.os.UserHandle
-import androidx.annotation.WorkerThread
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.communal.data.backup.CommunalBackupUtils
-import com.android.systemui.communal.data.db.CommunalItemRank
import com.android.systemui.communal.data.db.CommunalWidgetDao
-import com.android.systemui.communal.data.db.CommunalWidgetItem
import com.android.systemui.communal.nano.CommunalHubState
import com.android.systemui.communal.proto.toCommunalHubState
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -36,13 +35,15 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.util.kotlin.getValue
-import java.util.Optional
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -88,7 +89,6 @@ interface CommunalWidgetRepository {
class CommunalWidgetRepositoryImpl
@Inject
constructor(
- appWidgetManagerOptional: Optional<AppWidgetManager>,
private val appWidgetHost: CommunalAppWidgetHost,
@Background private val bgScope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
@@ -97,6 +97,7 @@ constructor(
@CommunalLog logBuffer: LogBuffer,
private val backupManager: BackupManager,
private val backupUtils: CommunalBackupUtils,
+ packageChangeRepository: PackageChangeRepository,
) : CommunalWidgetRepository {
companion object {
const val TAG = "CommunalWidgetRepository"
@@ -104,12 +105,39 @@ constructor(
private val logger = Logger(logBuffer, TAG)
- private val appWidgetManager by appWidgetManagerOptional
+ /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */
+ private val widgetEntries: Flow<List<CommunalWidgetEntry>> =
+ combine(
+ communalWidgetDao.getWidgets(),
+ communalWidgetHost.appWidgetProviders,
+ ) { entries, providers ->
+ entries.mapNotNull { (rank, widget) ->
+ CommunalWidgetEntry(
+ appWidgetId = widget.widgetId,
+ componentName = widget.componentName,
+ priority = rank.rank,
+ providerInfo = providers[widget.widgetId]
+ )
+ }
+ }
+ @OptIn(ExperimentalCoroutinesApi::class)
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
- communalWidgetDao
- .getWidgets()
- .map { it.mapNotNull(::mapToContentModel) }
+ widgetEntries
+ .flatMapLatest { widgetEntries ->
+ // If and only if any widget is missing provider info, combine with the package
+ // installer sessions flow to check whether they are pending installation. This can
+ // happen after widgets are freshly restored from a backup. In most cases, provider
+ // info is available to all widgets, and is unnecessary to involve an API call to
+ // the package installer.
+ if (widgetEntries.any { it.providerInfo == null }) {
+ packageChangeRepository.packageInstallSessionsForPrimaryUser.map { sessions ->
+ widgetEntries.mapNotNull { entry -> mapToContentModel(entry, sessions) }
+ }
+ } else {
+ flowOf(widgetEntries.map(::mapToContentModel))
+ }
+ }
// As this reads from a database and triggers IPCs to AppWidgetManager,
// it should be executed in the background.
.flowOn(bgDispatcher)
@@ -245,6 +273,9 @@ constructor(
}
appWidgetHost.deleteAppWidgetId(widgetId)
}
+
+ // Providers may have changed
+ communalWidgetHost.refreshProviders()
}
}
@@ -255,16 +286,57 @@ constructor(
}
}
- @WorkerThread
+ /**
+ * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with the assumption that the
+ * [AppWidgetProviderInfo] of the entry is available.
+ */
+ private fun mapToContentModel(entry: CommunalWidgetEntry): CommunalWidgetContentModel {
+ return CommunalWidgetContentModel.Available(
+ appWidgetId = entry.appWidgetId,
+ providerInfo = entry.providerInfo!!,
+ priority = entry.priority,
+ )
+ }
+
+ /**
+ * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with a list of install
+ * sessions. If the [AppWidgetProviderInfo] of the entry is absent, and its package is in the
+ * install sessions, the entry is mapped to a pending widget.
+ */
private fun mapToContentModel(
- entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
+ entry: CommunalWidgetEntry,
+ installSessions: List<PackageInstallSession>,
): CommunalWidgetContentModel? {
- val (_, widgetId) = entry.value
- val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null
- return CommunalWidgetContentModel(
- appWidgetId = widgetId,
- providerInfo = providerInfo,
- priority = entry.key.rank,
- )
+ if (entry.providerInfo != null) {
+ return CommunalWidgetContentModel.Available(
+ appWidgetId = entry.appWidgetId,
+ providerInfo = entry.providerInfo!!,
+ priority = entry.priority,
+ )
+ }
+
+ val session =
+ installSessions.firstOrNull {
+ it.packageName ==
+ ComponentName.unflattenFromString(entry.componentName)?.packageName
+ }
+ return if (session != null) {
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = entry.appWidgetId,
+ priority = entry.priority,
+ packageName = session.packageName,
+ icon = session.icon,
+ user = session.user,
+ )
+ } else {
+ null
+ }
}
+
+ private data class CommunalWidgetEntry(
+ val appWidgetId: Int,
+ val componentName: String,
+ val priority: Int,
+ var providerInfo: AppWidgetProviderInfo? = null,
+ )
}
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 0042915d0cd9..6b4cf79d59b1 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
@@ -60,9 +60,9 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.UserTracker
import com.android.systemui.smartspace.data.repository.SmartspaceRepository
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
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
@@ -127,10 +127,10 @@ constructor(
/** Whether communal features are enabled and available. */
val isCommunalAvailable: Flow<Boolean> =
- and(
+ allOf(
communalSettingsInteractor.isCommunalEnabled,
not(keyguardInteractor.isEncryptedOrLockdown),
- or(keyguardInteractor.isKeyguardShowing, keyguardInteractor.isDreaming)
+ anyOf(keyguardInteractor.isKeyguardShowing, keyguardInteractor.isDreaming)
)
.distinctUntilChanged()
.onEach { available ->
@@ -403,19 +403,30 @@ constructor(
updateOnWorkProfileBroadcastReceived,
) { widgets, allowedCategories, _ ->
widgets.map { widget ->
- if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
- // At least one category this widget specified is allowed, so show it
- WidgetContent.Widget(
- appWidgetId = widget.appWidgetId,
- providerInfo = widget.providerInfo,
- appWidgetHost = appWidgetHost,
- inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
- )
- } else {
- WidgetContent.DisabledWidget(
- appWidgetId = widget.appWidgetId,
- providerInfo = widget.providerInfo,
- )
+ when (widget) {
+ is CommunalWidgetContentModel.Available -> {
+ if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
+ // At least one category this widget specified is allowed, so show it
+ WidgetContent.Widget(
+ appWidgetId = widget.appWidgetId,
+ providerInfo = widget.providerInfo,
+ appWidgetHost = appWidgetHost,
+ inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
+ )
+ } else {
+ WidgetContent.DisabledWidget(
+ appWidgetId = widget.appWidgetId,
+ providerInfo = widget.providerInfo,
+ )
+ }
+ }
+ is CommunalWidgetContentModel.Pending -> {
+ WidgetContent.PendingWidget(
+ appWidgetId = widget.appWidgetId,
+ packageName = widget.packageName,
+ icon = widget.icon,
+ )
+ }
}
}
}
@@ -430,7 +441,15 @@ constructor(
} else {
// Get associated work profile for the currently selected user.
val workProfile = userTracker.userProfiles.find { it.isManagedProfile }
- list.filter { it.providerInfo.profile.identifier != workProfile?.id }
+ list.filter { model ->
+ val uid =
+ when (model) {
+ is CommunalWidgetContentModel.Available ->
+ model.providerInfo.profile.identifier
+ is CommunalWidgetContentModel.Pending -> model.user.identifier
+ }
+ uid != workProfile?.id
+ }
}
/** A flow of available smartspace targets. Currently only showing timers. */
@@ -513,7 +532,11 @@ constructor(
): List<CommunalWidgetContentModel> {
val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
return list.filter { widget ->
- currentUserIds.contains(widget.providerInfo.profile?.identifier)
+ when (widget) {
+ is CommunalWidgetContentModel.Available ->
+ currentUserIds.contains(widget.providerInfo.profile?.identifier)
+ is CommunalWidgetContentModel.Pending -> true
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index 706122789563..122240daed52 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -19,6 +19,7 @@ package com.android.systemui.communal.domain.model
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.pm.ApplicationInfo
+import android.graphics.Bitmap
import android.widget.RemoteViews
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
@@ -45,11 +46,10 @@ sealed interface CommunalContentModel {
sealed interface WidgetContent : CommunalContentModel {
val appWidgetId: Int
- val providerInfo: AppWidgetProviderInfo
data class Widget(
override val appWidgetId: Int,
- override val providerInfo: AppWidgetProviderInfo,
+ val providerInfo: AppWidgetProviderInfo,
val appWidgetHost: CommunalAppWidgetHost,
val inQuietMode: Boolean,
) : WidgetContent {
@@ -66,7 +66,7 @@ sealed interface CommunalContentModel {
data class DisabledWidget(
override val appWidgetId: Int,
- override val providerInfo: AppWidgetProviderInfo
+ val providerInfo: AppWidgetProviderInfo
) : WidgetContent {
override val key = KEY.disabledWidget(appWidgetId)
// Widget size is always half.
@@ -75,6 +75,16 @@ sealed interface CommunalContentModel {
val appInfo: ApplicationInfo?
get() = providerInfo.providerInfo?.applicationInfo
}
+
+ data class PendingWidget(
+ override val appWidgetId: Int,
+ val packageName: String,
+ val icon: Bitmap? = null,
+ ) : WidgetContent {
+ override val key = KEY.pendingWidget(appWidgetId)
+ // Widget size is always half.
+ override val size = CommunalContentSize.HALF
+ }
}
/** A placeholder item representing a new widget being added */
@@ -127,6 +137,10 @@ sealed interface CommunalContentModel {
return "disabled_widget_$id"
}
+ fun pendingWidget(id: Int): String {
+ return "pending_widget_$id"
+ }
+
fun widgetPlaceholder(): String {
return "widget_placeholder_${UUID.randomUUID()}"
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index e141dc40477c..53aecc199c4b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -17,10 +17,27 @@
package com.android.systemui.communal.shared.model
import android.appwidget.AppWidgetProviderInfo
+import android.graphics.Bitmap
+import android.os.UserHandle
/** Encapsulates data for a communal widget. */
-data class CommunalWidgetContentModel(
- val appWidgetId: Int,
- val providerInfo: AppWidgetProviderInfo,
- val priority: Int,
-)
+sealed interface CommunalWidgetContentModel {
+ val appWidgetId: Int
+ val priority: Int
+
+ /** Widget is ready to display */
+ data class Available(
+ override val appWidgetId: Int,
+ val providerInfo: AppWidgetProviderInfo,
+ override val priority: Int,
+ ) : CommunalWidgetContentModel
+
+ /** Widget is pending installation */
+ data class Pending(
+ override val appWidgetId: Int,
+ override val priority: Int,
+ val packageName: String,
+ val icon: Bitmap?,
+ val user: UserHandle,
+ ) : CommunalWidgetContentModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 3f92223fb57b..f6122ad48300 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -104,7 +104,12 @@ constructor(
): Boolean =
withContext(backgroundDispatcher) {
val widgets = communalInteractor.widgetContent.first()
- val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo }
+ val excludeList =
+ widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo(
+ ArrayList()
+ ) {
+ it.providerInfo
+ }
getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
try {
activityLauncher.launch(it)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 5f1d89e079a7..b7e8205e6582 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -24,6 +24,7 @@ import android.os.Looper
import android.widget.RemoteViews
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
+import javax.annotation.concurrent.GuardedBy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -47,6 +48,8 @@ class CommunalAppWidgetHost(
/** App widget ids that have been removed and no longer available. */
val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow()
+ @GuardedBy("observers") private val observers = mutableSetOf<Observer>()
+
override fun onCreateView(
context: Context,
appWidgetId: Int,
@@ -77,6 +80,61 @@ class CommunalAppWidgetHost(
}
}
+ override fun allocateAppWidgetId(): Int {
+ return super.allocateAppWidgetId().also { appWidgetId ->
+ backgroundScope.launch {
+ observers.forEach { observer -> observer.onAllocateAppWidgetId(appWidgetId) }
+ }
+ }
+ }
+
+ override fun deleteAppWidgetId(appWidgetId: Int) {
+ super.deleteAppWidgetId(appWidgetId)
+ backgroundScope.launch {
+ observers.forEach { observer -> observer.onDeleteAppWidgetId(appWidgetId) }
+ }
+ }
+
+ override fun startListening() {
+ super.startListening()
+ backgroundScope.launch { observers.forEach { observer -> observer.onHostStartListening() } }
+ }
+
+ override fun stopListening() {
+ super.stopListening()
+ backgroundScope.launch { observers.forEach { observer -> observer.onHostStopListening() } }
+ }
+
+ fun addObserver(observer: Observer) {
+ synchronized(observers) { observers.add(observer) }
+ }
+
+ fun removeObserver(observer: Observer) {
+ synchronized(observers) { observers.remove(observer) }
+ }
+
+ /**
+ * Allows another class to observe the [CommunalAppWidgetHost] and handle any logic there.
+ *
+ * This is mainly for testability as it is difficult to test a real instance of [AppWidgetHost]
+ * which communicates with framework services.
+ *
+ * Note: all the callbacks are launched from the background scope.
+ */
+ interface Observer {
+ /** Called immediately after the host has started listening for widget updates. */
+ fun onHostStartListening() {}
+
+ /** Called immediately after the host has stopped listening for widget updates. */
+ fun onHostStopListening() {}
+
+ /** Called immediately after a new app widget id has been allocated. */
+ fun onAllocateAppWidgetId(appWidgetId: Int) {}
+
+ /** Called immediately after an app widget id is to be deleted. */
+ fun onDeleteAppWidgetId(appWidgetId: Int) {}
+ }
+
companion object {
private const val TAG = "CommunalAppWidgetHost"
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 8390d62b23db..301da51c8082 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
@@ -39,6 +39,7 @@ class CommunalAppWidgetHostStartable
@Inject
constructor(
private val appWidgetHost: CommunalAppWidgetHost,
+ private val communalWidgetHost: CommunalWidgetHost,
private val communalInteractor: CommunalInteractor,
private val userTracker: UserTracker,
@Background private val bgScope: CoroutineScope,
@@ -46,7 +47,7 @@ constructor(
) : CoreStartable {
override fun start() {
- or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+ anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
// Only trigger updates on state changes, ignoring the initial false value.
.pairwise(false)
.filter { (previous, new) -> previous != new }
@@ -70,9 +71,11 @@ constructor(
// Always ensure this is called on the main/ui thread.
withContext(uiDispatcher) {
if (active) {
+ communalWidgetHost.startObservingHost()
appWidgetHost.startListening()
} else {
appWidgetHost.stopListening()
+ communalWidgetHost.stopObservingHost()
}
}
@@ -83,7 +86,15 @@ constructor(
private fun validateWidgetsAndDeleteOrphaned(widgets: List<CommunalWidgetContentModel>) {
val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
widgets
- .filter { widget -> !currentUserIds.contains(widget.providerInfo.profile?.identifier) }
+ .filter { widget ->
+ val uid =
+ when (widget) {
+ is CommunalWidgetContentModel.Available ->
+ widget.providerInfo.profile?.identifier
+ is CommunalWidgetContentModel.Pending -> widget.user.identifier
+ }
+ !currentUserIds.contains(uid)
+ }
.onEach { widget -> communalInteractor.deleteWidget(id = widget.appWidgetId) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
index 93e2b37cfe87..42107c1e9769 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.widgets
+import android.appwidget.AppWidgetHost.AppWidgetHostListener
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
@@ -23,6 +24,9 @@ import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.ComponentName
import android.os.Bundle
import android.os.UserHandle
+import android.widget.RemoteViews
+import androidx.annotation.WorkerThread
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
@@ -30,6 +34,11 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.kotlin.getOrNull
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
/**
* Widget host that interacts with AppWidget service and host to bind and provide info for widgets
@@ -38,11 +47,12 @@ import javax.inject.Inject
class CommunalWidgetHost
@Inject
constructor(
+ @Background private val bgScope: CoroutineScope,
private val appWidgetManager: Optional<AppWidgetManager>,
private val appWidgetHost: CommunalAppWidgetHost,
private val selectedUserInteractor: SelectedUserInteractor,
@CommunalLog logBuffer: LogBuffer,
-) {
+) : CommunalAppWidgetHost.Observer {
companion object {
private const val TAG = "CommunalWidgetHost"
@@ -60,6 +70,19 @@ constructor(
private val logger = Logger(logBuffer, TAG)
+ private val _appWidgetProviders = MutableStateFlow(emptyMap<Int, AppWidgetProviderInfo?>())
+
+ /**
+ * A flow of mappings between an appWidgetId and its corresponding [AppWidgetProviderInfo].
+ * These [AppWidgetProviderInfo]s represent app widgets that are actively bound to the
+ * [CommunalAppWidgetHost].
+ *
+ * The [AppWidgetProviderInfo] may be null in the case that the widget is bound but its provider
+ * is unavailable. For example, its package is not installed.
+ */
+ val appWidgetProviders: StateFlow<Map<Int, AppWidgetProviderInfo?>> =
+ _appWidgetProviders.asStateFlow()
+
/**
* Allocate an app widget id and binds the widget with the provider and associated user.
*
@@ -77,6 +100,7 @@ constructor(
)
) {
logger.d("Successfully bound the widget $provider")
+ onProviderInfoUpdated(id, getAppWidgetInfo(id))
return id
}
appWidgetHost.deleteAppWidgetId(id)
@@ -100,7 +124,83 @@ constructor(
return false
}
+ @WorkerThread
fun getAppWidgetInfo(widgetId: Int): AppWidgetProviderInfo? {
return appWidgetManager.getOrNull()?.getAppWidgetInfo(widgetId)
}
+
+ fun startObservingHost() {
+ appWidgetHost.addObserver(this@CommunalWidgetHost)
+ }
+
+ fun stopObservingHost() {
+ appWidgetHost.removeObserver(this@CommunalWidgetHost)
+ }
+
+ fun refreshProviders() {
+ bgScope.launch {
+ val newProviders = mutableMapOf<Int, AppWidgetProviderInfo?>()
+ appWidgetHost.appWidgetIds.forEach { appWidgetId ->
+ // Listen for updates from each bound widget
+ addListener(appWidgetId)
+
+ // Fetch provider info of the widget
+ newProviders[appWidgetId] = getAppWidgetInfo(appWidgetId)
+ }
+
+ _appWidgetProviders.value = newProviders.toMap()
+ }
+ }
+
+ override fun onHostStartListening() {
+ refreshProviders()
+ }
+
+ override fun onHostStopListening() {
+ // Remove listeners
+ _appWidgetProviders.value.keys.forEach { appWidgetId ->
+ appWidgetHost.removeListener(appWidgetId)
+ }
+
+ // Clear providers
+ _appWidgetProviders.value = emptyMap()
+ }
+
+ override fun onAllocateAppWidgetId(appWidgetId: Int) {
+ addListener(appWidgetId)
+ }
+
+ override fun onDeleteAppWidgetId(appWidgetId: Int) {
+ appWidgetHost.removeListener(appWidgetId)
+ _appWidgetProviders.value =
+ _appWidgetProviders.value.toMutableMap().also { it.remove(appWidgetId) }
+ }
+
+ private fun addListener(appWidgetId: Int) {
+ appWidgetHost.setListener(
+ appWidgetId,
+ CommunalAppWidgetHostListener(appWidgetId, this::onProviderInfoUpdated),
+ )
+ }
+
+ private fun onProviderInfoUpdated(appWidgetId: Int, providerInfo: AppWidgetProviderInfo?) {
+ bgScope.launch {
+ _appWidgetProviders.value =
+ _appWidgetProviders.value.toMutableMap().also { it[appWidgetId] = providerInfo }
+ }
+ }
+
+ /** A [AppWidgetHostListener] for [appWidgetId]. */
+ private class CommunalAppWidgetHostListener(
+ private val appWidgetId: Int,
+ private val onUpdateProviderInfo: (Int, AppWidgetProviderInfo?) -> Unit,
+ ) : AppWidgetHostListener {
+ override fun onUpdateProviderInfo(providerInfo: AppWidgetProviderInfo?) {
+ onUpdateProviderInfo(appWidgetId, providerInfo)
+ }
+
+ override fun onViewDataChanged(viewId: Int) {}
+
+ override fun updateAppWidget(remoteViews: RemoteViews?) {}
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
index aa6516d54563..2000f96bcdb0 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
@@ -69,16 +69,18 @@ interface CommunalWidgetModule {
@SysUISingleton
@Provides
fun provideCommunalWidgetHost(
+ @Application applicationScope: CoroutineScope,
appWidgetManager: Optional<AppWidgetManager>,
appWidgetHost: CommunalAppWidgetHost,
selectedUserInteractor: SelectedUserInteractor,
@CommunalLog logBuffer: LogBuffer,
): CommunalWidgetHost {
return CommunalWidgetHost(
+ applicationScope,
appWidgetManager,
appWidgetHost,
selectedUserInteractor,
- logBuffer
+ logBuffer,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index ef3f10fa3853..e00137e3045e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -49,6 +49,7 @@ import android.content.SharedPreferences;
import android.content.om.OverlayManager;
import android.content.pm.IPackageManager;
import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutManager;
import android.content.res.AssetManager;
@@ -483,6 +484,12 @@ public class FrameworkServicesModule {
@Provides
@Singleton
+ static PackageInstaller providePackageInstaller(PackageManager packageManager) {
+ return packageManager.getPackageInstaller();
+ }
+
+ @Provides
+ @Singleton
static PackageManagerWrapper providePackageManagerWrapper() {
return PackageManagerWrapper.getInstance();
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
index c6fb4f9d6956..fc9406bd27d8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
@@ -19,12 +19,13 @@ package com.android.systemui.keyboard
import com.android.systemui.keyboard.data.repository.KeyboardRepository
import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl
+import com.android.systemui.keyboard.shortcut.ShortcutHelperModule
import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository
import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl
import dagger.Binds
import dagger.Module
-@Module
+@Module(includes = [ShortcutHelperModule::class])
abstract class KeyboardModule {
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
new file mode 100644
index 000000000000..5635f8056b9c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.keyboard.shortcut
+
+import android.app.Activity
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.keyboardShortcutHelperRewrite
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import dagger.Binds
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface ShortcutHelperModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(ShortcutHelperActivity::class)
+ fun activity(impl: ShortcutHelperActivity): Activity
+
+ companion object {
+ @Provides
+ @IntoMap
+ @ClassKey(ShortcutHelperActivityStarter::class)
+ fun starter(implLazy: Lazy<ShortcutHelperActivityStarter>): CoreStartable {
+ return if (keyboardShortcutHelperRewrite()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ NoOpStartable
+ }
+ }
+
+ @Provides
+ @IntoMap
+ @ClassKey(ShortcutHelperRepository::class)
+ fun repo(implLazy: Lazy<ShortcutHelperRepository>): CoreStartable {
+ return if (keyboardShortcutHelperRewrite()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ NoOpStartable
+ }
+ }
+ }
+}
+
+private object NoOpStartable : CoreStartable {
+ override fun start() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
new file mode 100644
index 000000000000..9450af4c804e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Inactive
+import com.android.systemui.statusbar.CommandQueue
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class ShortcutHelperRepository
+@Inject
+constructor(
+ private val commandQueue: CommandQueue,
+ private val broadcastDispatcher: BroadcastDispatcher,
+) : CoreStartable {
+
+ val state = MutableStateFlow<ShortcutHelperState>(Inactive)
+
+ override fun start() {
+ registerBroadcastReceiver(
+ action = Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS,
+ onReceive = { state.value = Active() }
+ )
+ registerBroadcastReceiver(
+ action = Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS,
+ onReceive = { state.value = Inactive }
+ )
+ commandQueue.addCallback(
+ object : CommandQueue.Callbacks {
+ override fun dismissKeyboardShortcutsMenu() {
+ state.value = Inactive
+ }
+
+ override fun toggleKeyboardShortcutsMenu(deviceId: Int) {
+ state.value =
+ if (state.value is Inactive) {
+ Active(deviceId)
+ } else {
+ Inactive
+ }
+ }
+ }
+ )
+ }
+
+ fun hide() {
+ state.value = Inactive
+ }
+
+ private fun registerBroadcastReceiver(action: String, onReceive: () -> Unit) {
+ broadcastDispatcher.registerReceiver(
+ receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ onReceive()
+ }
+ },
+ filter = IntentFilter(action),
+ flags = Context.RECEIVER_EXPORTED or Context.RECEIVER_VISIBLE_TO_INSTANT_APPS
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt
new file mode 100644
index 000000000000..d3f7e24bb87f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.keyboard.shortcut.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class ShortcutHelperInteractor
+@Inject
+constructor(private val repository: ShortcutHelperRepository) {
+
+ val state: Flow<ShortcutHelperState> = repository.state
+
+ fun onUserLeave() {
+ repository.hide()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
new file mode 100644
index 000000000000..d22d6c88ccc8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.keyboard.shortcut.shared.model
+
+sealed interface ShortcutHelperState {
+ data object Inactive : ShortcutHelperState
+
+ data class Active(val deviceId: Int? = null) : ShortcutHelperState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
new file mode 100644
index 000000000000..fbf52e773599
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.keyboard.shortcut.ui
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class ShortcutHelperActivityStarter(
+ private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val viewModel: ShortcutHelperViewModel,
+ private val startActivity: (Intent) -> Unit,
+) : CoreStartable {
+
+ @Inject
+ constructor(
+ context: Context,
+ @Application applicationScope: CoroutineScope,
+ viewModel: ShortcutHelperViewModel,
+ ) : this(
+ context,
+ applicationScope,
+ viewModel,
+ startActivity = { intent -> context.startActivity(intent) }
+ )
+
+ override fun start() {
+ applicationScope.launch {
+ viewModel.shouldShow.collect { shouldShow ->
+ if (shouldShow) {
+ startShortcutHelperActivity()
+ }
+ }
+ }
+ }
+
+ private fun startShortcutHelperActivity() {
+ startActivity(
+ Intent(context, ShortcutHelperActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 692fbb06e88c..934f9ee9e90d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.keyboard.shortcut
+package com.android.systemui.keyboard.shortcut.ui.view
import android.graphics.Insets
import android.os.Bundle
@@ -24,16 +24,25 @@ import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.core.view.updatePadding
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.res.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import javax.inject.Inject
+import kotlinx.coroutines.launch
/**
* Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
* small and large screen devices.
*/
-class ShortcutHelperActivity : ComponentActivity() {
+class ShortcutHelperActivity
+@Inject
+constructor(
+ private val viewModel: ShortcutHelperViewModel,
+) : ComponentActivity() {
private val bottomSheetContainer
get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
@@ -53,6 +62,24 @@ class ShortcutHelperActivity : ComponentActivity() {
setUpPredictiveBack()
setUpSheetDismissListener()
setUpDismissOnTouchOutside()
+ observeFinishRequired()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isFinishing) {
+ viewModel.onUserLeave()
+ }
+ }
+
+ private fun observeFinishRequired() {
+ lifecycleScope.launch {
+ viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow ->
+ if (!shouldShow) {
+ finish()
+ }
+ }
+ }
}
private fun setupEdgeToEdge() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
new file mode 100644
index 000000000000..7e48c6523122
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.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.keyboard.shortcut.ui.viewmodel
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+class ShortcutHelperViewModel
+@Inject
+constructor(
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val interactor: ShortcutHelperInteractor
+) {
+
+ val shouldShow =
+ interactor.state
+ .map { it is ShortcutHelperState.Active }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ fun onUserLeave() {
+ interactor.onUserLeave()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index a49b3ae7b7e3..c11c49c7a8a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository
import android.os.Handler
import android.util.Log
+import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
@@ -57,21 +58,7 @@ constructor(
TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) }
val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!)
val refreshTransition = MutableSharedFlow<Config>(extraBufferCapacity = 1)
- private var targetTransitionConfig: Config? = null
-
- /**
- * Emits the blueprint value to the collectors.
- *
- * @param blueprintId
- * @return whether the transition has succeeded.
- */
- fun applyBlueprint(index: Int): Boolean {
- ArrayList(blueprintIdMap.values)[index]?.let {
- applyBlueprint(it)
- return true
- }
- return false
- }
+ @VisibleForTesting var targetTransitionConfig: Config? = null
/**
* Emits the blueprint value to the collectors.
@@ -81,27 +68,21 @@ constructor(
*/
fun applyBlueprint(blueprintId: String?): Boolean {
val blueprint = blueprintIdMap[blueprintId]
- return if (blueprint != null) {
- applyBlueprint(blueprint)
- true
- } else {
+ if (blueprint == null) {
Log.e(
TAG,
"Could not find blueprint with id: $blueprintId. " +
"Perhaps it was not added to KeyguardBlueprintModule?"
)
- false
+ return false
}
- }
- /** Emits the blueprint value to the collectors. */
- fun applyBlueprint(blueprint: KeyguardBlueprint?) {
if (blueprint == this.blueprint.value) {
- refreshBlueprint()
- return
+ return true
}
- blueprint?.let { this.blueprint.value = it }
+ this.blueprint.value = blueprint
+ return true
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 54d9a78620e5..faab033441c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -28,7 +28,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@@ -148,7 +148,7 @@ constructor(
}
} else {
scope.launch {
- and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
+ allOf(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
.filterRelevantKeyguardStateAnd { isOccludedAndNotDreaming ->
isOccludedAndNotDreaming
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index da4f85e0dd2f..857096e1c03b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -36,9 +36,12 @@ import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
@SysUISingleton
@@ -64,12 +67,7 @@ constructor(
/** Current BlueprintId */
val blueprintId =
- combine(
- configurationInteractor.onAnyConfigurationChange,
- fingerprintPropertyInteractor.propertiesInitialized.filter { it },
- clockInteractor.currentClock,
- shadeInteractor.shadeMode,
- ) { _, _, _, shadeMode ->
+ shadeInteractor.shadeMode.map { shadeMode ->
val useSplitShade = shadeMode == ShadeMode.Split && !ComposeLockscreen.isEnabled
when {
useSplitShade -> SplitShadeKeyguardBlueprint.ID
@@ -77,17 +75,29 @@ constructor(
}
}
+ private val refreshEvents: Flow<Unit> =
+ merge(
+ configurationInteractor.onAnyConfigurationChange,
+ fingerprintPropertyInteractor.propertiesInitialized.filter { it }.map { Unit },
+ )
+
init {
applicationScope.launch { blueprintId.collect { transitionToBlueprint(it) } }
+ applicationScope.launch { refreshEvents.collect { refreshBlueprint() } }
}
/**
- * Transitions to a blueprint.
+ * Transitions to a blueprint, or refreshes it if already applied.
*
* @param blueprintId
* @return whether the transition has succeeded.
*/
- fun transitionToBlueprint(blueprintId: String): Boolean {
+ fun transitionOrRefreshBlueprint(blueprintId: String): Boolean {
+ if (blueprintId == blueprint.value.id) {
+ refreshBlueprint()
+ return true
+ }
+
return keyguardBlueprintRepository.applyBlueprint(blueprintId)
}
@@ -97,7 +107,7 @@ constructor(
* @param blueprintId
* @return whether the transition has succeeded.
*/
- fun transitionToBlueprint(blueprintId: Int): Boolean {
+ fun transitionToBlueprint(blueprintId: String): Boolean {
return keyguardBlueprintRepository.applyBlueprint(blueprintId)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index ccc48b5ecb12..bda6438c308f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -36,7 +36,6 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
-import com.android.app.tracing.coroutines.launch
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
import com.android.systemui.Flags.newAodTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index ce7ec0e22f1c..962cdf10cf86 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -46,15 +46,14 @@ constructor(
return
}
- if (
- arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt())
- ) {
- pw.println("Transition succeeded!")
- } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
- pw.println("Transition succeeded!")
- } else {
- pw.println("Invalid argument! To see available blueprint ids, run:")
- pw.println("$ adb shell cmd statusbar blueprint help")
+ when {
+ arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.")
+ keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) ->
+ pw.println("Transition succeeded!")
+ else -> {
+ pw.println("Invalid argument! To see available blueprint ids, run:")
+ pw.println("$ adb shell cmd statusbar blueprint help")
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 24a7c512a6a9..bbcea56799ea 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -42,7 +42,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.ui.AnimatableEvent
@@ -64,7 +64,6 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
@@ -134,7 +133,7 @@ constructor(
private val isOnLockscreen: Flow<Boolean> =
combine(
keyguardTransitionInteractor.isFinishedInState(LOCKSCREEN).onStart { emit(false) },
- or(
+ anyOf(
keyguardTransitionInteractor.isInTransitionToState(LOCKSCREEN),
keyguardTransitionInteractor.isInTransitionFromState(LOCKSCREEN),
),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index b705a0389300..ea89be61d773 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -21,6 +21,7 @@ import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE;
import android.content.Context;
import android.os.Handler;
+import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogModule;
import com.android.systemui.dagger.NightDisplayListenerModule;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -60,6 +61,7 @@ import javax.inject.Named;
*/
@Module(subcomponents = {QSFragmentComponent.class, QSSceneComponent.class},
includes = {
+ BluetoothTileDialogModule.class,
MediaModule.class,
PanelsModule.class,
QSExternalModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
new file mode 100644
index 000000000000..28c1fbf2007d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.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.panels.data.repository
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class IconAndNameCustomRepository
+@Inject
+constructor(
+ private val installedTilesComponentRepository: InstalledTilesComponentRepository,
+ private val userTracker: UserTracker,
+ @Background private val backgroundContext: CoroutineContext,
+) {
+ /**
+ * Returns a list of the icon/labels for all available (installed and enabled) tile services.
+ *
+ * No order is guaranteed.
+ */
+ suspend fun getCustomTileData(): List<EditTileData> {
+ return withContext(backgroundContext) {
+ val installedTiles =
+ installedTilesComponentRepository.getInstalledTilesServiceInfos(userTracker.userId)
+ val packageManager = userTracker.userContext.packageManager
+ installedTiles
+ .map {
+ val tileSpec = TileSpec.create(it.componentName)
+ val label = it.loadLabel(packageManager)
+ val icon = it.loadIcon(packageManager)
+ val appName = it.applicationInfo.loadLabel(packageManager)
+ if (icon != null) {
+ EditTileData(
+ tileSpec,
+ Icon.Loaded(icon, ContentDescription.Loaded(label.toString())),
+ Text.Loaded(label.toString()),
+ Text.Loaded(appName.toString()),
+ )
+ } else {
+ null
+ }
+ }
+ .filterNotNull()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
new file mode 100644
index 000000000000..ec9d151a26d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+@SysUISingleton
+class StockTilesRepository
+@Inject
+constructor(
+ @Main private val resources: Resources,
+) {
+ /**
+ * List of stock platform tiles. All of the specs will be of type [TileSpec.PlatformTileSpec].
+ */
+ val stockTiles =
+ resources
+ .getString(R.string.quick_settings_tiles_stock)
+ .split(",")
+ .map(TileSpec::create)
+ .filterNot { it is TileSpec.Invalid }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
new file mode 100644
index 000000000000..3b29422ccfc3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.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.panels.domain.interactor
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.data.repository.IconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.StockTilesRepository
+import com.android.systemui.qs.panels.domain.model.EditTilesModel
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import javax.inject.Inject
+
+@SysUISingleton
+class EditTilesListInteractor
+@Inject
+constructor(
+ private val stockTilesRepository: StockTilesRepository,
+ private val qsTileConfigProvider: QSTileConfigProvider,
+ private val iconAndNameCustomRepository: IconAndNameCustomRepository,
+) {
+ /**
+ * Provides a list of the tiles to edit, with their UI information (icon, labels).
+ *
+ * The icons have the label as their content description.
+ */
+ suspend fun getTilesToEdit(): EditTilesModel {
+ val stockTiles =
+ stockTilesRepository.stockTiles.map {
+ if (qsTileConfigProvider.hasConfig(it.spec)) {
+ val config = qsTileConfigProvider.getConfig(it.spec)
+ EditTileData(
+ it,
+ Icon.Resource(
+ config.uiConfig.iconRes,
+ ContentDescription.Resource(config.uiConfig.labelRes)
+ ),
+ Text.Resource(config.uiConfig.labelRes),
+ null,
+ )
+ } else {
+ EditTileData(
+ it,
+ Icon.Resource(
+ android.R.drawable.star_on,
+ ContentDescription.Loaded(it.spec)
+ ),
+ Text.Loaded(it.spec),
+ null
+ )
+ }
+ }
+ return EditTilesModel(stockTiles, iconAndNameCustomRepository.getCustomTileData())
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt
new file mode 100644
index 000000000000..b573b9a8770f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.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.panels.domain.model
+
+import com.android.systemui.qs.panels.shared.model.EditTileData
+
+data class EditTilesModel(
+ val stockTiles: List<EditTileData>,
+ val customTiles: List<EditTileData>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
new file mode 100644
index 000000000000..8b70bb9f9e23
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.panels.shared.model
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+data class EditTileData(
+ val tileSpec: TileSpec,
+ val icon: Icon,
+ val label: Text,
+ val appName: Text?,
+) {
+ init {
+ check(
+ (tileSpec is TileSpec.PlatformTileSpec && appName == null) ||
+ (tileSpec is TileSpec.CustomTileSpec && appName != null)
+ ) {
+ "tileSpec: $tileSpec - appName: $appName. " +
+ "appName must be non-null for custom tiles and only for custom tiles."
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
new file mode 100644
index 000000000000..5c17fd1bf94e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.panels.ui.compose
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
+
+@Composable
+fun EditMode(
+ viewModel: EditModeViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val gridLayout by viewModel.gridLayout.collectAsState()
+ val tiles by viewModel.tiles.collectAsState(emptyList())
+
+ BackHandler { viewModel.stopEditing() }
+
+ DisposableEffect(Unit) { onDispose { viewModel.stopEditing() } }
+
+ Column(modifier) {
+ gridLayout.EditTileGrid(
+ tiles,
+ Modifier,
+ viewModel::addTile,
+ viewModel::removeTile,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 68ce5d830570..8806931a888a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,7 +18,9 @@ package com.android.systemui.qs.panels.ui.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
interface GridLayout {
@Composable
@@ -26,4 +28,12 @@ interface GridLayout {
tiles: List<TileViewModel>,
modifier: Modifier,
)
+
+ @Composable
+ fun EditTileGrid(
+ tiles: List<EditTileViewModel>,
+ modifier: Modifier,
+ onAddTile: (TileSpec, Int) -> Unit,
+ onRemoveTile: (TileSpec) -> Unit,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index e2143e090b21..6539cf35b073 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -28,6 +28,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
@@ -37,8 +39,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -47,6 +55,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -56,14 +65,28 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.integerResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.background
import com.android.compose.theme.colorAttr
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.load
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.ActiveTileColorAttributes
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileColorAttributes
import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.res.R
import javax.inject.Inject
@@ -75,6 +98,8 @@ import kotlinx.coroutines.flow.mapLatest
class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: IconTilesInteractor) :
GridLayout {
+ private object TileType
+
@Composable
override fun TileGrid(
tiles: List<TileViewModel>,
@@ -88,17 +113,7 @@ class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: Ic
val iconTilesSpecs by
iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet())
- LazyVerticalGrid(
- columns =
- GridCells.Fixed(
- integerResource(R.integer.quick_settings_infinite_grid_num_columns)
- ),
- verticalArrangement =
- Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
- horizontalArrangement =
- Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
- modifier = modifier
- ) {
+ TileLazyGrid(modifier) {
items(
tiles.size,
span = { index ->
@@ -131,29 +146,11 @@ class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: Ic
.mapLatest { it.toUiState() }
.collectAsState(initial = tile.currentState.toUiState())
val context = LocalContext.current
- val horizontalAlignment =
- if (iconOnly) {
- Alignment.CenterHorizontally
- } else {
- Alignment.Start
- }
Row(
- modifier =
- modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
- .clickable { tile.onClick(null) }
- .background(colorAttr(state.colors.background))
- .padding(
- horizontal = dimensionResource(id = R.dimen.qs_label_container_margin)
- ),
+ modifier = modifier.clickable { tile.onClick(null) }.tileModifier(state.colors),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement =
- Arrangement.spacedBy(
- space = dimensionResource(id = R.dimen.qs_label_container_margin),
- alignment = horizontalAlignment
- )
+ horizontalArrangement = tileHorizontalArrangement(iconOnly)
) {
val icon =
remember(state.icon) {
@@ -165,62 +162,275 @@ class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: Ic
}
}
}
- TileIcon(icon, colorAttr(state.colors.icon))
-
- if (!iconOnly) {
- Column(
- verticalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxHeight()
- ) {
- Text(
- state.label.toString(),
- color = colorAttr(state.colors.label),
- modifier = Modifier.basicMarquee(),
- )
- if (!TextUtils.isEmpty(state.secondaryLabel)) {
- Text(
- state.secondaryLabel.toString(),
- color = colorAttr(state.colors.secondaryLabel),
- modifier = Modifier.basicMarquee(),
- )
- }
- }
- }
+ TileContent(
+ label = state.label.toString(),
+ secondaryLabel = state.secondaryLabel.toString(),
+ icon = icon,
+ colors = state.colors,
+ iconOnly = iconOnly
+ )
}
}
- @OptIn(ExperimentalAnimationGraphicsApi::class)
@Composable
- private fun TileIcon(icon: Icon, color: Color) {
- val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
- val context = LocalContext.current
- val loadedDrawable =
- remember(icon, context) {
- when (icon) {
- is Icon.Loaded -> icon.drawable
- is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
+ override fun EditTileGrid(
+ tiles: List<EditTileViewModel>,
+ modifier: Modifier,
+ onAddTile: (TileSpec, Int) -> Unit,
+ onRemoveTile: (TileSpec) -> Unit,
+ ) {
+ val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
+ val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
+ val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
+ onAddTile(it, POSITION_AT_END)
+ }
+ val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet())
+ val isIconOnly: (TileSpec) -> Boolean =
+ remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
+
+ TileLazyGrid(modifier = modifier) {
+ // These Text are just placeholders to see the different sections. Not final UI.
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Text("Current tiles", color = Color.White)
+ }
+
+ editTiles(
+ currentTiles,
+ ClickAction.REMOVE,
+ onRemoveTile,
+ isIconOnly,
+ indicatePosition = true,
+ )
+
+ item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
+
+ editTiles(
+ otherTilesStock,
+ ClickAction.ADD,
+ addTileToEnd,
+ isIconOnly,
+ )
+
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Text("Custom tiles to add", color = Color.White)
+ }
+
+ editTiles(
+ otherTilesCustom,
+ ClickAction.ADD,
+ addTileToEnd,
+ isIconOnly,
+ )
+ }
+ }
+
+ private fun LazyGridScope.editTiles(
+ tiles: List<EditTileViewModel>,
+ clickAction: ClickAction,
+ onClick: (TileSpec) -> Unit,
+ isIconOnly: (TileSpec) -> Boolean,
+ indicatePosition: Boolean = false,
+ ) {
+ items(
+ count = tiles.size,
+ key = { tiles[it].tileSpec.spec },
+ span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
+ contentType = { TileType }
+ ) {
+ val viewModel = tiles[it]
+ val canClick =
+ when (clickAction) {
+ ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
+ ClickAction.REMOVE ->
+ AvailableEditActions.REMOVE in viewModel.availableEditActions
+ }
+ val onClickActionName =
+ when (clickAction) {
+ ClickAction.ADD ->
+ stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+ ClickAction.REMOVE ->
+ stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+ }
+ val stateDescription =
+ if (indicatePosition) {
+ stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
+ } else {
+ ""
+ }
+
+ Box(
+ modifier =
+ Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) }
+ .animateItem()
+ .semantics {
+ onClick(onClickActionName) { false }
+ this.stateDescription = stateDescription
+ }
+ ) {
+ EditTile(
+ tileViewModel = viewModel,
+ isIconOnly(viewModel.tileSpec),
+ modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+ )
+ if (canClick) {
+ Badge(clickAction, Modifier.align(Alignment.TopEnd))
}
}
- if (loadedDrawable !is Animatable) {
+ }
+ }
+
+ @Composable
+ private fun Badge(action: ClickAction, modifier: Modifier = Modifier) {
+ Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) {
Icon(
- icon = icon,
- tint = color,
- modifier = modifier,
+ imageVector =
+ when (action) {
+ ClickAction.ADD -> Icons.Filled.Add
+ ClickAction.REMOVE -> Icons.Filled.Remove
+ },
+ "",
+ tint = Color.Black,
)
- } else if (icon is Icon.Resource) {
- val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
- var atEnd by remember(icon.res) { mutableStateOf(false) }
- LaunchedEffect(key1 = icon.res) {
- delay(350)
- atEnd = true
+ }
+ }
+
+ @Composable
+ private fun EditTile(
+ tileViewModel: EditTileViewModel,
+ iconOnly: Boolean,
+ modifier: Modifier = Modifier,
+ ) {
+ val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
+ val colors = ActiveTileColorAttributes
+
+ Row(
+ modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = tileHorizontalArrangement(iconOnly)
+ ) {
+ TileContent(
+ label = label,
+ secondaryLabel = tileViewModel.appName?.load(),
+ colors = colors,
+ icon = tileViewModel.icon,
+ iconOnly = iconOnly,
+ animateIconToEnd = true,
+ )
+ }
+ }
+
+ private enum class ClickAction {
+ ADD,
+ REMOVE,
+ }
+}
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+private fun TileIcon(
+ icon: Icon,
+ color: Color,
+ animateToEnd: Boolean = false,
+) {
+ val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
+ val context = LocalContext.current
+ val loadedDrawable =
+ remember(icon, context) {
+ when (icon) {
+ is Icon.Loaded -> icon.drawable
+ is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
+ }
+ }
+ if (loadedDrawable !is Animatable) {
+ Icon(
+ icon = icon,
+ tint = color,
+ modifier = modifier,
+ )
+ } else if (icon is Icon.Resource) {
+ val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+ val painter =
+ if (animateToEnd) {
+ rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+ } else {
+ var atEnd by remember(icon.res) { mutableStateOf(false) }
+ LaunchedEffect(key1 = icon.res) {
+ delay(350)
+ atEnd = true
+ }
+ rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
}
- val painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
- Image(
- painter = painter,
- contentDescription = null,
- colorFilter = ColorFilter.tint(color = color),
- modifier = modifier
+ Image(
+ painter = painter,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(color = color),
+ modifier = modifier
+ )
+ }
+}
+
+@Composable
+private fun TileLazyGrid(
+ modifier: Modifier = Modifier,
+ content: LazyGridScope.() -> Unit,
+) {
+ LazyVerticalGrid(
+ columns =
+ GridCells.Fixed(integerResource(R.integer.quick_settings_infinite_grid_num_columns)),
+ verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
+ horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
+ modifier = modifier,
+ content = content,
+ )
+}
+
+@Composable
+private fun Modifier.tileModifier(colors: TileColorAttributes): Modifier {
+ return fillMaxWidth()
+ .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
+ .background(colorAttr(colors.background))
+ .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin))
+}
+
+@Composable
+private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal {
+ val horizontalAlignment =
+ if (iconOnly) {
+ Alignment.CenterHorizontally
+ } else {
+ Alignment.Start
+ }
+ return spacedBy(
+ space = dimensionResource(id = R.dimen.qs_label_container_margin),
+ alignment = horizontalAlignment
+ )
+}
+
+@Composable
+private fun TileContent(
+ label: String,
+ secondaryLabel: String?,
+ icon: Icon,
+ colors: TileColorAttributes,
+ iconOnly: Boolean,
+ animateIconToEnd: Boolean = false,
+) {
+ TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
+
+ if (!iconOnly) {
+ Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+ Text(
+ label,
+ color = colorAttr(colors.label),
+ modifier = Modifier.basicMarquee(),
)
+ if (!TextUtils.isEmpty(secondaryLabel)) {
+ Text(
+ secondaryLabel ?: "",
+ color = colorAttr(colors.secondaryLabel),
+ modifier = Modifier.basicMarquee(),
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
new file mode 100644
index 000000000000..69f50a7986d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.GridLayoutTypeInteractor
+import com.android.systemui.qs.panels.shared.model.GridLayoutType
+import com.android.systemui.qs.panels.ui.compose.GridLayout
+import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class EditModeViewModel
+@Inject
+constructor(
+ private val editTilesListInteractor: EditTilesListInteractor,
+ private val currentTilesInteractor: CurrentTilesInteractor,
+ private val minTilesInteractor: MinimumTilesInteractor,
+ private val defaultGridLayout: InfiniteGridLayout,
+ @Application private val applicationScope: CoroutineScope,
+ gridLayoutTypeInteractor: GridLayoutTypeInteractor,
+ gridLayoutMap: Map<GridLayoutType, @JvmSuppressWildcards GridLayout>,
+) {
+ private val _isEditing = MutableStateFlow(false)
+
+ /**
+ * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this
+ */
+ val isEditing = _isEditing.asStateFlow()
+ private val minimumTiles: Int
+ get() = minTilesInteractor.minNumberOfTiles
+
+ val gridLayout: StateFlow<GridLayout> =
+ gridLayoutTypeInteractor.layout
+ .map { gridLayoutMap[it] ?: defaultGridLayout }
+ .stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ defaultGridLayout,
+ )
+
+ /**
+ * Flow of view models for each tile that should be visible in edit mode (or empty flow when not
+ * editing).
+ *
+ * Guarantees of the data:
+ * * The data for the tiles is fetched once whenever [isEditing] goes from `false` to `true`.
+ * This prevents icons/labels changing while in edit mode.
+ * * It tracks the current tiles as they are added/removed/moved by the user.
+ * * The tiles that are current will be in the same relative order as the user sees them in
+ * Quick Settings.
+ * * The tiles that are not current will preserve their relative order even when the current
+ * tiles change.
+ */
+ val tiles =
+ isEditing.flatMapLatest {
+ if (it) {
+ val editTilesData = editTilesListInteractor.getTilesToEdit()
+ currentTilesInteractor.currentTiles.map { tiles ->
+ val currentSpecs = tiles.map { it.spec }
+ val canRemoveTiles = currentSpecs.size > minimumTiles
+ val allTiles = editTilesData.stockTiles + editTilesData.customTiles
+ val allTilesMap = allTiles.associate { it.tileSpec to it }
+ val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
+ val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
+
+ (currentTiles + nonCurrentTiles).map {
+ val current = it.tileSpec in currentSpecs
+ val availableActions = buildSet {
+ if (current) {
+ add(AvailableEditActions.MOVE)
+ if (canRemoveTiles) {
+ add(AvailableEditActions.REMOVE)
+ }
+ } else {
+ add(AvailableEditActions.ADD)
+ }
+ }
+ EditTileViewModel(
+ it.tileSpec,
+ it.icon,
+ it.label,
+ it.appName,
+ current,
+ availableActions
+ )
+ }
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+
+ /** @see isEditing */
+ fun startEditing() {
+ _isEditing.value = true
+ }
+
+ /** @see isEditing */
+ fun stopEditing() {
+ _isEditing.value = false
+ }
+
+ /** Immediately moves [tileSpec] to [position]. */
+ fun moveTile(tileSpec: TileSpec, position: Int) {
+ throw NotImplementedError("This is not supported yet")
+ }
+
+ /** Immediately adds [tileSpec] to the current tiles at [position]. */
+ fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
+ currentTilesInteractor.addTile(tileSpec, position)
+ }
+
+ /** Immediately removes [tileSpec] from the current tiles. */
+ fun removeTile(tileSpec: TileSpec) {
+ currentTilesInteractor.removeTiles(listOf(tileSpec))
+ }
+
+ /** Immediately resets the current tiles to the default list. */
+ fun resetCurrentTilesToDefault() {
+ throw NotImplementedError("This is not supported yet")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
new file mode 100644
index 000000000000..ba9a0442503d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.panels.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/**
+ * View model for each tile that is available to be added/removed/moved in Edit mode.
+ *
+ * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in
+ * Quick Settings.
+ */
+class EditTileViewModel(
+ val tileSpec: TileSpec,
+ val icon: Icon,
+ val label: Text,
+ val appName: Text?,
+ val isCurrent: Boolean,
+ val availableEditActions: Set<AvailableEditActions>,
+)
+
+enum class AvailableEditActions {
+ ADD,
+ REMOVE,
+ MOVE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index cfcea9829610..c5b27376a82a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -23,6 +23,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.service.quicksettings.TileService
import androidx.annotation.GuardedBy
@@ -36,14 +37,17 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
interface InstalledTilesComponentRepository {
fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>>
+
+ fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo>
}
@SysUISingleton
@@ -55,38 +59,45 @@ constructor(
private val packageChangeRepository: PackageChangeRepository
) : InstalledTilesComponentRepository {
- @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>()
+ @GuardedBy("userMap") private val userMap = mutableMapOf<Int, StateFlow<List<ServiceInfo>>>()
override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> =
- synchronized(userMap) {
- userMap.getOrPut(userId) {
- /*
- * In order to query [PackageManager] for different users, this implementation will
- * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
- * context.
- */
- val packageManager =
- if (applicationContext.userId == userId) {
- applicationContext.packageManager
- } else {
- applicationContext
- .createContextAsUser(
- UserHandle.of(userId),
- /* flags */ 0,
- )
- .packageManager
- }
- packageChangeRepository
- .packageChanged(UserHandle.of(userId))
- .onStart { emit(PackageChangeModel.Empty) }
- .map { reloadComponents(userId, packageManager) }
- .distinctUntilChanged()
- .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1)
- }
+ synchronized(userMap) { getForUserLocked(userId) }
+ .map { it.mapTo(mutableSetOf()) { it.componentName } }
+
+ override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> {
+ return synchronized(userMap) { getForUserLocked(userId).value }
+ }
+
+ private fun getForUserLocked(userId: Int): StateFlow<List<ServiceInfo>> {
+ return userMap.getOrPut(userId) {
+ /*
+ * In order to query [PackageManager] for different users, this implementation will
+ * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
+ * context.
+ */
+ val packageManager =
+ if (applicationContext.userId == userId) {
+ applicationContext.packageManager
+ } else {
+ applicationContext
+ .createContextAsUser(
+ UserHandle.of(userId),
+ /* flags */ 0,
+ )
+ .packageManager
+ }
+ packageChangeRepository
+ .packageChanged(UserHandle.of(userId))
+ .onStart { emit(PackageChangeModel.Empty) }
+ .map { reloadComponents(userId, packageManager) }
+ .distinctUntilChanged()
+ .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), emptyList())
}
+ }
@WorkerThread
- private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> {
+ private fun reloadComponents(userId: Int, packageManager: PackageManager): List<ServiceInfo> {
return packageManager
.queryIntentServicesAsUser(INTENT, FLAGS, userId)
.mapNotNull { it.serviceInfo }
@@ -100,7 +111,6 @@ constructor(
false
}
}
- .mapTo(mutableSetOf()) { it.componentName }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 61896f0a3816..b7fcef4376ea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -115,6 +115,10 @@ interface CurrentTilesInteractor : ProtoDumpable {
* @see TileSpecRepository.setTiles
*/
fun setTiles(specs: List<TileSpec>)
+
+ companion object {
+ val POSITION_AT_END: Int = TileSpecRepository.POSITION_AT_END
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt
new file mode 100644
index 000000000000..2ae3f07d6b67
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.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.qs.pipeline.domain.interactor
+
+import com.android.systemui.qs.pipeline.data.repository.MinimumTilesRepository
+import javax.inject.Inject
+
+class MinimumTilesInteractor
+@Inject
+constructor(
+ private val minimumTilesRepository: MinimumTilesRepository,
+) {
+ val minNumberOfTiles: Int
+ get() = minimumTilesRepository.minNumberOfTiles
+}
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 c24113f14f00..56588ff75a5a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -55,6 +55,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.graph.SignalDrawable;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.Expandable;
@@ -632,12 +633,23 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
}
public static class DrawableIcon extends Icon {
+
protected final Drawable mDrawable;
protected final Drawable mInvisibleDrawable;
+ private static final String TAG = "QSTileImpl";
public DrawableIcon(Drawable drawable) {
mDrawable = drawable;
- mInvisibleDrawable = drawable.getConstantState().newDrawable();
+ Drawable.ConstantState nullableConstantState = drawable.getConstantState();
+ if (nullableConstantState == null) {
+ if (!(drawable instanceof SignalDrawable)) {
+ Log.w(TAG, "DrawableIcon: drawable has null ConstantState"
+ + " and is not a SignalDrawable");
+ }
+ mInvisibleDrawable = drawable;
+ } else {
+ mInvisibleDrawable = nullableConstantState.newDrawable();
+ }
}
@Override
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 f3852a275209..4fd0df4d3f8f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -186,7 +186,8 @@ open class QSTileViewImpl @JvmOverloads constructor(
private val locInScreen = IntArray(2)
/** Visuo-haptic long-press effects */
- private var haveLongPressPropertiesBeenReset = true
+ var haveLongPressPropertiesBeenReset = true
+ private set
private var paddingForLaunch = Rect()
private var initialLongPressProperties: QSLongPressProperties? = null
private var finalLongPressProperties: QSLongPressProperties? = null
@@ -772,7 +773,11 @@ open class QSTileViewImpl @JvmOverloads constructor(
}
}
- override fun onActivityLaunchAnimationEnd() = resetLongPressEffectProperties()
+ override fun onActivityLaunchAnimationEnd() {
+ if (longPressEffect != null && !haveLongPressPropertiesBeenReset) {
+ resetLongPressEffectProperties()
+ }
+ }
fun prepareForLaunch() {
val startingHeight = initialLongPressProperties?.height?.toInt() ?: 0
@@ -877,8 +882,8 @@ open class QSTileViewImpl @JvmOverloads constructor(
background.updateBounds(
left = 0,
top = 0,
- right = initialLongPressProperties?.width?.toInt() ?: 0,
- bottom = initialLongPressProperties?.height?.toInt() ?: 0,
+ right = initialLongPressProperties?.width?.toInt() ?: measuredWidth,
+ bottom = initialLongPressProperties?.height?.toInt() ?: measuredHeight,
)
changeCornerRadius(resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat())
setAllColors(
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 b88c1e566c4a..5346b237111f 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
@@ -201,6 +201,7 @@ constructor(
qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
+
override fun getTileLabel(): CharSequence =
with(qsTileViewModel.config.uiConfig) {
when (this) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
index d6325c049823..a04fa3817493 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.ui.viewmodel
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
import javax.inject.Inject
@@ -27,4 +28,5 @@ class QuickSettingsContainerViewModel
constructor(
val brightnessSliderViewModel: BrightnessSliderViewModel,
val tileGridViewModel: TileGridViewModel,
+ val editModeViewModel: EditModeViewModel,
)
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 76bd80ff6f79..faf2bbc1ba3b 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -707,7 +707,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
public void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop) {
if (mOverviewProxy != null) {
try {
- if (DesktopModeStatus.isEnabled() && (sysUiState.getFlags()
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)
+ && (sysUiState.getFlags()
& SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
index b91dd0451808..0603d21fa2a6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
@@ -24,6 +24,8 @@ import com.android.compose.animation.scene.TransitionKey
* These are the subset of transitions that can be referenced by key when asking for a scene change.
*/
object TransitionKeys {
+ /** Reference to the gone to shade transition with split shade enabled. */
+ val GoneToSplitShade = TransitionKey("GoneToSplitShade")
/** Reference to a scene transition that can collapse the shade scene instantly. */
val CollapseShadeInstantly = TransitionKey("CollapseShadeInstantly")
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 78704e16b586..c20d577d66a1 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -198,7 +198,7 @@ object SceneWindowRootViewBinder {
private fun getDisplayWidth(context: Context): Dp {
val point = Point()
checkNotNull(context.display).getRealSize(point)
- return point.x.dp
+ return point.x.toDp(context)
}
// TODO(b/298525212): remove once Compose exposes window inset bounds.
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 b0af7f9ce072..016fe572894c 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
@@ -24,6 +24,7 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
@@ -70,10 +71,11 @@ constructor(
)] = UserActionResult(Scenes.QuickSettingsShade)
}
+ val downSceneKey =
+ if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+ val downTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split }
this[Swipe(direction = SwipeDirection.Down)] =
- UserActionResult(
- if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
- )
+ UserActionResult(downSceneKey, downTransitionKey)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 494fc9b8c683..bd90de230d5f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -47,7 +47,6 @@ import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Rect;
-import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
@@ -208,8 +207,7 @@ public class ScreenshotController {
@Nullable
private final ScreenshotSoundController mScreenshotSoundController;
private final PhoneWindow mWindow;
- private final DisplayManager mDisplayManager;
- private final int mDisplayId;
+ private final Display mDisplay;
private final ScrollCaptureExecutor mScrollCaptureExecutor;
private final ScreenshotNotificationSmartActionsProvider
mScreenshotNotificationSmartActionsProvider;
@@ -249,7 +247,6 @@ public class ScreenshotController {
@AssistedInject
ScreenshotController(
Context context,
- DisplayManager displayManager,
WindowManager windowManager,
FeatureFlags flags,
ScreenshotViewProxy.Factory viewProxyFactory,
@@ -271,12 +268,13 @@ public class ScreenshotController {
AssistContentRequester assistContentRequester,
MessageContainerController messageContainerController,
Provider<ScreenshotSoundController> screenshotSoundController,
- @Assisted int displayId,
+ @Assisted Display display,
@Assisted boolean showUIOnExternalDisplay
) {
mScreenshotSmartActions = screenshotSmartActions;
mActionsProviderFactory = actionsProviderFactory;
- mNotificationsController = screenshotNotificationsControllerFactory.create(displayId);
+ mNotificationsController = screenshotNotificationsControllerFactory.create(
+ display.getDisplayId());
mUiEventLogger = uiEventLogger;
mImageExporter = imageExporter;
mImageCapture = imageCapture;
@@ -290,11 +288,9 @@ public class ScreenshotController {
mScreenshotHandler = timeoutHandler;
mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
-
- mDisplayId = displayId;
- mDisplayManager = displayManager;
+ mDisplay = display;
mWindowManager = windowManager;
- final Context displayContext = context.createDisplayContext(getDisplay());
+ final Context displayContext = context.createDisplayContext(display);
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mFlags = flags;
mActionIntentExecutor = actionIntentExecutor;
@@ -302,7 +298,7 @@ public class ScreenshotController {
mMessageContainerController = messageContainerController;
mAssistContentRequester = assistContentRequester;
- mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId);
+ mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId());
mScreenshotHandler.setOnTimeoutRunnable(() -> {
if (DEBUG_UI) {
@@ -328,7 +324,7 @@ public class ScreenshotController {
});
// Sound is only reproduced from the controller of the default display.
- if (displayId == Display.DEFAULT_DISPLAY) {
+ if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) {
mScreenshotSoundController = screenshotSoundController.get();
} else {
mScreenshotSoundController = null;
@@ -356,7 +352,7 @@ public class ScreenshotController {
if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN
&& screenshot.getBitmap() == null) {
Rect bounds = getFullScreenRect();
- screenshot.setBitmap(mImageCapture.captureDisplay(mDisplayId, bounds));
+ screenshot.setBitmap(mImageCapture.captureDisplay(mDisplay.getDisplayId(), bounds));
screenshot.setScreenBounds(bounds);
}
@@ -459,7 +455,7 @@ public class ScreenshotController {
}
private boolean shouldShowUi() {
- return mDisplayId == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay;
+ return mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay;
}
void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
@@ -618,7 +614,7 @@ public class ScreenshotController {
private void requestScrollCapture(UserHandle owner) {
mScrollCaptureExecutor.requestScrollCapture(
- mDisplayId,
+ mDisplay.getDisplayId(),
mWindow.getDecorView().getWindowToken(),
(response) -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
@@ -641,7 +637,8 @@ public class ScreenshotController {
}
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0,
response.getPackageName());
- Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, getFullScreenRect());
+ Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplay.getDisplayId(),
+ getFullScreenRect());
if (newScreenshot == null) {
Log.e(TAG, "Failed to capture current screenshot for scroll transition!");
return;
@@ -819,7 +816,8 @@ public class ScreenshotController {
private void saveScreenshotInBackground(
ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) {
ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
- requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), mDisplayId);
+ requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(),
+ mDisplay.getDisplayId());
future.addListener(() -> {
try {
ImageExporter.Result result = future.get();
@@ -861,7 +859,7 @@ public class ScreenshotController {
data.mActionsReadyListener = actionsReadyListener;
data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
data.owner = owner;
- data.displayId = mDisplayId;
+ data.displayId = mDisplay.getDisplayId();
if (mSaveInBgTask != null) {
// just log success/failure for the pre-existing screenshot
@@ -986,13 +984,9 @@ public class ScreenshotController {
}
}
- private Display getDisplay() {
- return mDisplayManager.getDisplay(mDisplayId);
- }
-
private Rect getFullScreenRect() {
DisplayMetrics displayMetrics = new DisplayMetrics();
- getDisplay().getRealMetrics(displayMetrics);
+ mDisplay.getRealMetrics(displayMetrics);
return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
}
@@ -1026,12 +1020,12 @@ public class ScreenshotController {
@AssistedFactory
public interface Factory {
/**
- * Creates an instance of the controller for that specific displayId.
+ * Creates an instance of the controller for that specific display.
*
- * @param displayId: display to capture
- * @param showUIOnExternalDisplay: Whether the UI should be shown if this is an external
- * display.
+ * @param display display to capture
+ * @param showUIOnExternalDisplay Whether the UI should be shown if this is an external
+ * display.
*/
- ScreenshotController create(int displayId, boolean showUIOnExternalDisplay);
+ ScreenshotController create(Display display, boolean showUIOnExternalDisplay);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index e56a4f45d7aa..40d709d0ab25 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -68,11 +68,13 @@ constructor(
onSaved: (Uri?) -> Unit,
requestCallback: RequestCallback
) {
- val displayIds = getDisplaysToScreenshot(screenshotRequest.type)
+ val displays = getDisplaysToScreenshot(screenshotRequest.type)
val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
- displayIds.forEach { displayId: Int ->
+ displays.forEach { display ->
+ val displayId = display.displayId
Log.d(TAG, "Executing screenshot for display $displayId")
dispatchToController(
+ display = display,
rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
onSaved =
if (displayId == Display.DEFAULT_DISPLAY) {
@@ -85,6 +87,7 @@ constructor(
/** All logging should be triggered only by this method. */
private suspend fun dispatchToController(
+ display: Display,
rawScreenshotData: ScreenshotData,
onSaved: (Uri?) -> Unit,
callback: RequestCallback
@@ -104,8 +107,7 @@ constructor(
logScreenshotRequested(screenshotData)
Log.d(TAG, "Screenshot request: $screenshotData")
try {
- getScreenshotController(screenshotData.displayId)
- .handleScreenshot(screenshotData, onSaved, callback)
+ getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback)
} catch (e: IllegalStateException) {
Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e)
onFailedScreenshotRequest(screenshotData, callback)
@@ -135,12 +137,13 @@ constructor(
callback.reportError()
}
- private suspend fun getDisplaysToScreenshot(requestType: Int): List<Int> {
+ private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> {
+ val allDisplays = displays.first()
return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
// If this is a provided image, let's show the UI on the default display only.
- listOf(Display.DEFAULT_DISPLAY)
+ allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY }
} else {
- displays.first().filter { it.type in ALLOWED_DISPLAY_TYPES }.map { it.displayId }
+ allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES }
}
}
@@ -170,9 +173,9 @@ constructor(
screenshotControllers.clear()
}
- private fun getScreenshotController(id: Int): ScreenshotController {
- return screenshotControllers.computeIfAbsent(id) {
- screenshotControllerFactory.create(id, /* showUIOnExternalDisplay= */ false)
+ private fun getScreenshotController(display: Display): ScreenshotController {
+ return screenshotControllers.computeIfAbsent(display.displayId) {
+ screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 281857fb0658..6367d44bd439 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -51,9 +51,9 @@ 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
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -145,7 +145,7 @@ constructor(
/** Returns a flow that tracks whether communal hub is available. */
fun communalAvailable(): Flow<Boolean> =
- or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+ anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
/**
* Creates the container view containing the glanceable hub UI.
@@ -248,7 +248,7 @@ constructor(
// transition to the bouncer would be incorrectly intercepted by the hub.
collectFlow(
containerView,
- or(
+ anyOf(
keyguardInteractor.primaryBouncerShowing,
keyguardInteractor.alternateBouncerShowing
),
@@ -267,7 +267,7 @@ constructor(
)
collectFlow(
containerView,
- and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
+ allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
{
shadeShowing = it
updateTouchHandlingState()
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 ac76becd1797..d15a488cb582 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
@@ -33,6 +33,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
@@ -152,11 +153,13 @@ constructor(
else -> Scenes.Lockscreen
}
+ val upTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split }
+
val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
return buildMap {
if (!isCustomizing) {
- this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+ this[Swipe(SwipeDirection.Up)] = UserActionResult(up, upTransitionKey)
} // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 222b070d151d..14e14f4bd47f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -14,7 +14,6 @@ import androidx.annotation.FloatRange
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.ExpandHelper
-import com.android.systemui.Flags.nsslFalsingFix
import com.android.systemui.Gefingerpoken
import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy
import com.android.systemui.classifier.Classifier
@@ -884,9 +883,7 @@ class DragDownHelper(
isDraggingDown = false
isTrackpadReverseScroll = false
shadeRepository.setLegacyLockscreenShadeTracking(false)
- if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) {
- return true
- }
+ return true
} else {
stopDragging()
return false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
index 0c341cc92f83..ec3c7d0d6de4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
@@ -27,6 +27,9 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
* (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
*/
public interface NotificationActivityStarter {
+ /** Called when the user clicks on the notification bubble icon. */
+ void onNotificationBubbleIconClicked(NotificationEntry entry);
+
/** Called when the user clicks on the surface of a notification. */
void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index d10fac6ea3fc..6487d55197e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -117,11 +117,14 @@ public final class NotificationClicker implements View.OnClickListener {
Notification notification = sbn.getNotification();
if (notification.contentIntent != null || notification.fullScreenIntent != null
|| row.getEntry().isBubble()) {
+ row.setBubbleClickListener(v ->
+ mNotificationActivityStarter.onNotificationBubbleIconClicked(row.getEntry()));
row.setOnClickListener(this);
row.setOnDragSuccessListener(mOnDragSuccessListener);
} else {
row.setOnClickListener(null);
row.setOnDragSuccessListener(null);
+ row.setBubbleClickListener(null);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 61cdea190a43..6a38f8df4715 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -38,6 +38,7 @@ import com.android.internal.jank.InteractionJankMonitor.Configuration;
import com.android.settingslib.Utils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.res.R;
+import com.android.systemui.shade.TouchLogger;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -745,6 +746,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
}
}
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ return TouchLogger.logDispatchTouch(
+ getClass().getSimpleName(), ev, super.dispatchTouchEvent(ev));
+ }
+
/**
* SourceType which should be reset when this View is detached
* @param sourceType will be reset on View detached
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 5e3df7b5e60f..23c0a0db04d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -375,6 +375,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
};
private OnClickListener mOnClickListener;
+ @Nullable
+ private OnClickListener mBubbleClickListener;
private OnDragSuccessListener mOnDragSuccessListener;
private boolean mHeadsupDisappearRunning;
private View mChildAfterViewWhenDismissed;
@@ -1234,14 +1236,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
/**
* The click listener for the bubble button.
*/
+ @Nullable
public View.OnClickListener getBubbleClickListener() {
- return v -> {
- if (mBubblesManagerOptional.isPresent()) {
- mBubblesManagerOptional.get()
- .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
- }
- mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
- };
+ return mBubbleClickListener;
+ }
+
+ /**
+ * Sets the click listener for the bubble button.
+ */
+ public void setBubbleClickListener(@Nullable OnClickListener l) {
+ mBubbleClickListener = l;
+ // ensure listener is passed to the content views
+ mPrivateLayout.updateBubbleButton(mEntry);
+ mPublicLayout.updateBubbleButton(mEntry);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt
index 9a54de1481a0..2527af87728e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.row.ui.viewbinder
+import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
@@ -72,7 +73,6 @@ private class TouchHandler(
var isTouchEnabled = false
override fun onTouch(v: View, ev: MotionEvent): Boolean {
- val result = false
if (ev.action == MotionEvent.ACTION_UP) {
view.setLastActionUpTime(ev.eventTime)
}
@@ -82,13 +82,22 @@ private class TouchHandler(
}
if (ev.action == MotionEvent.ACTION_UP) {
// If this is a false tap, capture the even so it doesn't result in a click.
- return falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
+ return falsingManager.isFalseTap(FalsingManager.LOW_PENALTY).also {
+ if (it) {
+ Log.d(v::class.simpleName ?: TAG, "capturing false tap")
+ }
+ }
}
- return result
+ return false
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false
/** Use [onTouch] instead. */
override fun onTouchEvent(ev: MotionEvent): Boolean = false
+
+ companion object {
+ private const val TAG = "ActivatableNotificationViewBinder"
+ }
}
+
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 773a6bf752a6..232b4e993f06 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
@@ -1469,9 +1469,10 @@ public class NotificationStackScrollLayout
public void setExpandedHeight(float height) {
final boolean skipHeightUpdate = shouldSkipHeightUpdate();
- // when scene framework is enabled, updateStackPosition is already called by
- // updateTopPadding every time the stack moves, so skip it here to avoid flickering.
- if (!SceneContainerFlag.isEnabled()) {
+ // when scene framework is enabled and in single shade, updateStackPosition is already
+ // called by updateTopPadding every time the stack moves, so skip it here to avoid
+ // flickering.
+ if (!SceneContainerFlag.isEnabled() || mShouldUseSplitNotificationShade) {
updateStackPosition();
}
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 3011bc284961..c1c63cdec448 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
@@ -23,7 +23,6 @@ import static com.android.app.animation.Interpolators.STANDARD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.server.notification.Flags.screenshareNotificationHiding;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
-import static com.android.systemui.Flags.nsslFalsingFix;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener;
@@ -2062,32 +2061,18 @@ public class NotificationStackScrollLayoutController implements Dumpable {
}
boolean horizontalSwipeWantsIt = false;
boolean scrollerWantsIt = false;
- if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) {
- // Reverse the order relative to the else statement. onScrollTouch will reset on an
- // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
- if (mLongPressedView == null && !mView.isBeingDragged()
- && !expandingNotification
- && !mView.getExpandedInThisMotion()
- && !onlyScrollingInThisMotion
- && !mView.getDisallowDismissInThisMotion()) {
- horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
- }
- if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
- && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
- scrollerWantsIt = mView.onScrollTouch(ev);
- }
- } else {
- if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
- && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
- scrollerWantsIt = mView.onScrollTouch(ev);
- }
- if (mLongPressedView == null && !mView.isBeingDragged()
- && !expandingNotification
- && !mView.getExpandedInThisMotion()
- && !onlyScrollingInThisMotion
- && !mView.getDisallowDismissInThisMotion()) {
- horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
- }
+ // NOTE: the order of these is important. If reversed, onScrollTouch will reset on an
+ // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
+ if (mLongPressedView == null && !mView.isBeingDragged()
+ && !expandingNotification
+ && !mView.getExpandedInThisMotion()
+ && !onlyScrollingInThisMotion
+ && !mView.getDisallowDismissInThisMotion()) {
+ horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+ }
+ if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
+ && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
+ scrollerWantsIt = mView.onScrollTouch(ev);
}
// Check if we need to clear any snooze leavebehinds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 1f1251a37f58..0ba7b3c214c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -68,8 +68,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import javax.inject.Inject
@@ -246,7 +246,7 @@ constructor(
keyguardTransitionInteractor.finishedKeyguardState.map { state ->
state == GLANCEABLE_HUB
},
- or(
+ anyOf(
keyguardTransitionInteractor.isInTransitionToState(GLANCEABLE_HUB),
keyguardTransitionInteractor.isInTransitionFromState(GLANCEABLE_HUB),
),
@@ -424,14 +424,14 @@ constructor(
while (currentCoroutineContext().isActive) {
emit(false)
// Ensure states are inactive to start
- and(
+ allOf(
*toFlowArray(statesForHiddenKeyguard) { state ->
keyguardTransitionInteractor.transitionValue(state).map { it == 0f }
}
)
.first { it }
// Wait for a qualifying transition to begin
- or(
+ anyOf(
*toFlowArray(statesForHiddenKeyguard) { state ->
keyguardTransitionInteractor
.transitionStepsToState(state)
@@ -446,7 +446,7 @@ constructor(
// it is considered safe to reset alpha to 1f for HUNs.
combine(
keyguardInteractor.statusBarState,
- and(
+ allOf(
*toFlowArray(statesForHiddenKeyguard) { state ->
keyguardTransitionInteractor.transitionValue(state).map {
it == 0f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index e1a7f22a913a..e92058bf034a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -96,6 +96,20 @@ import javax.inject.Inject;
@SysUISingleton
public class StatusBarNotificationActivityStarter implements NotificationActivityStarter {
+ /**
+ * Helps to avoid recalculation of values provided to
+ * {@link #onDismiss(PendingIntent, boolean, boolean, boolean)}} method
+ */
+ private interface OnKeyguardDismissedAction {
+ /**
+ * Invoked when keyguard is dismissed
+ *
+ * @return is used as return value for {@link ActivityStarter.OnDismissAction#onDismiss()}
+ */
+ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animate,
+ boolean showOverTheLockScreen);
+ }
+
private final Context mContext;
private final int mDisplayId;
@@ -207,6 +221,30 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
}
/**
+ * Called when the user clicks on the notification bubble icon.
+ *
+ * @param entry notification that bubble icon was clicked
+ */
+ @Override
+ public void onNotificationBubbleIconClicked(NotificationEntry entry) {
+ Runnable action = () -> {
+ mBubblesManagerOptional.ifPresent(bubblesManager ->
+ bubblesManager.onUserChangedBubble(entry, !entry.isBubble()));
+ mHeadsUpManager.removeNotification(entry.getKey(), /* releaseImmediately= */ true);
+ };
+ if (entry.isBubble()) {
+ // entry is being un-bubbled, no need to unlock
+ action.run();
+ } else {
+ performActionAfterKeyguardDismissed(entry,
+ (intent, isActivityIntent, animate, showOverTheLockScreen) -> {
+ action.run();
+ return false;
+ });
+ }
+ }
+
+ /**
* Called when a notification is clicked.
*
* @param entry notification that was clicked
@@ -217,7 +255,15 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(),
mKeyguardStateController.isVisible(),
mNotificationShadeWindowController.getPanelExpanded());
+ OnKeyguardDismissedAction action =
+ (intent, isActivityIntent, animate, showOverTheLockScreen) ->
+ performActionOnKeyguardDismissed(entry, row, intent, isActivityIntent,
+ animate, showOverTheLockScreen);
+ performActionAfterKeyguardDismissed(entry, action);
+ }
+ private void performActionAfterKeyguardDismissed(NotificationEntry entry,
+ OnKeyguardDismissedAction action) {
if (mRemoteInputManager.isRemoteInputActive(entry)) {
// We have an active remote input typed and the user clicked on the notification.
// this was probably unintentional, so we're closing the edit text instead.
@@ -251,8 +297,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
ActivityStarter.OnDismissAction postKeyguardAction = new ActivityStarter.OnDismissAction() {
@Override
public boolean onDismiss() {
- return handleNotificationClickAfterKeyguardDismissed(
- entry, row, intent, isActivityIntent, animate, showOverLockscreen);
+ return action.onDismiss(intent, isActivityIntent, animate, showOverLockscreen);
}
@Override
@@ -271,7 +316,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
}
}
- private boolean handleNotificationClickAfterKeyguardDismissed(
+ private boolean performActionOnKeyguardDismissed(
NotificationEntry entry,
ExpandableNotificationRow row,
PendingIntent intent,
@@ -282,7 +327,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
entry, row, intent, isActivityIntent, animate);
-
if (showOverLockscreen) {
mShadeController.addPostCollapseAction(runnable);
mShadeController.collapseShade(true /* animate */);
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 226a84a78116..88ca9e5f1744 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
@@ -229,7 +229,7 @@ abstract class StatusBarPipelineModule {
@SysUISingleton
@OemSatelliteInputLog
fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer {
- return factory.create("DeviceBasedSatelliteInputLog", 32)
+ return factory.create("DeviceBasedSatelliteInputLog", 150)
}
const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
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 a7c4187afdbd..12f252d215a9 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
@@ -249,11 +249,17 @@ constructor(
try {
sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
registered = true
+ logBuffer.i { "Registered for signal strength successfully" }
} catch (e: Exception) {
logBuffer.e("error registering for signal strength", e)
}
- awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) }
+ awaitClose {
+ if (registered) {
+ sm.unregisterForNtnSignalStrengthChanged(cb)
+ logBuffer.i { "Unregistered for signal strength successfully" }
+ }
+ }
}
.flowOn(bgDispatcher)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 37be1c6aa73d..a817b31070a1 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,6 +18,7 @@
package com.android.systemui.user.data.repository
import android.annotation.SuppressLint
+import android.annotation.UserIdInt
import android.content.Context
import android.content.pm.UserInfo
import android.os.UserHandle
@@ -107,6 +108,22 @@ interface UserRepository {
fun isSimpleUserSwitcher(): Boolean
fun isUserSwitcherEnabled(): Boolean
+
+ /**
+ * Returns the user ID of the "main user" of the device. This user may have access to certain
+ * features which are limited to at most one user. There will never be more than one main user
+ * on a device.
+ *
+ * <p>Currently, on most form factors the first human user on the device will be the main user;
+ * in the future, the concept may be transferable, so a different user (or even no user at all)
+ * may be designated the main user instead. On other form factors there might not be a main
+ * user.
+ *
+ * <p> When the device doesn't have a main user, this will return {@code null}.
+ *
+ * @see [UserManager.getMainUser]
+ */
+ @UserIdInt suspend fun getMainUserId(): Int?
}
@SysUISingleton
@@ -239,6 +256,10 @@ constructor(
return _userSwitcherSettings.value.isUserSwitcherEnabled
}
+ override suspend fun getMainUserId(): Int? {
+ return withContext(backgroundDispatcher) { manager.mainUser?.identifier }
+ }
+
private suspend fun getSettings(): UserSwitcherSettingsModel {
return withContext(backgroundDispatcher) {
val isSimpleUserSwitcher =
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
index 38b381ac543e..59c819d41493 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
@@ -2,6 +2,7 @@ package com.android.systemui.user.domain.interactor
import android.annotation.UserIdInt
import android.content.pm.UserInfo
+import android.os.UserManager
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags.refactorGetCurrentUser
import com.android.systemui.dagger.SysUISingleton
@@ -38,4 +39,23 @@ class SelectedUserInteractor @Inject constructor(private val repository: UserRep
KeyguardUpdateMonitor.getCurrentUser()
}
}
+
+ /**
+ * Returns the user ID of the "main user" of the device. This user may have access to certain
+ * features which are limited to at most one user. There will never be more than one main user
+ * on a device.
+ *
+ * <p>Currently, on most form factors the first human user on the device will be the main user;
+ * in the future, the concept may be transferable, so a different user (or even no user at all)
+ * may be designated the main user instead. On other form factors there might not be a main
+ * user.
+ *
+ * <p> When the device doesn't have a main user, this will return {@code null}.
+ *
+ * @see [UserManager.getMainUser]
+ */
+ @UserIdInt
+ fun getMainUserId(): Int? {
+ return repository.mainUserId
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
index b3008856d370..a2759c6bf470 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
@@ -28,11 +28,23 @@ object BooleanFlowOperators {
*
* Usage:
* ```
- * val result = and(flow1, flow2)
+ * val result = allOf(flow1, flow2)
* ```
*/
- fun and(vararg flows: Flow<Boolean>): Flow<Boolean> =
- combine(flows.asIterable()) { values -> values.all { it } }.distinctUntilChanged()
+ fun allOf(vararg flows: Flow<Boolean>): Flow<Boolean> = flows.asIterable().all()
+
+ /**
+ * Logical AND operator for boolean flows. Will collect all flows and [combine] them to
+ * determine the result.
+ */
+ fun Array<Flow<Boolean>>.all(): Flow<Boolean> = allOf(*this)
+
+ /**
+ * Logical AND operator for boolean flows. Will collect all flows and [combine] them to
+ * determine the result.
+ */
+ fun Iterable<Flow<Boolean>>.all(): Flow<Boolean> =
+ combine(this) { values -> values.all { it } }.distinctUntilChanged()
/**
* Logical NOT operator for a boolean flow.
@@ -48,6 +60,36 @@ object BooleanFlowOperators {
* Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
* determine the result.
*/
- fun or(vararg flows: Flow<Boolean>): Flow<Boolean> =
- combine(flows.asIterable()) { values -> values.any { it } }.distinctUntilChanged()
+ fun anyOf(vararg flows: Flow<Boolean>): Flow<Boolean> = flows.asIterable().any()
+
+ /**
+ * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
+ * determine the result.
+ */
+ fun Array<Flow<Boolean>>.any(): Flow<Boolean> = anyOf(*this)
+
+ /**
+ * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
+ * determine the result.
+ */
+ fun Iterable<Flow<Boolean>>.any(): Flow<Boolean> =
+ combine(this) { values -> values.any { it } }.distinctUntilChanged()
+
+ /**
+ * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise
+ * produces `false`.
+ */
+ fun noneOf(vararg flows: Flow<Boolean>): Flow<Boolean> = not(anyOf(*flows))
+
+ /**
+ * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise
+ * produces `false`.
+ */
+ fun Array<Flow<Boolean>>.none(): Flow<Boolean> = noneOf(*this)
+
+ /**
+ * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise
+ * produces `false`.
+ */
+ fun Iterable<Flow<Boolean>>.none(): Flow<Boolean> = not(any())
}
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 af1d31528675..f62a55d02f61 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
@@ -77,6 +77,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
+ @Mock private lateinit var deviceItemActionInteractor: DeviceItemActionInteractor
+
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
@@ -118,6 +120,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
bluetoothTileDialogViewModel =
BluetoothTileDialogViewModel(
deviceItemInteractor,
+ deviceItemActionInteractor,
BluetoothStateInteractor(
localBluetoothManager,
bluetoothTileDialogLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt
new file mode 100644
index 000000000000..762137bede27
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceItemActionInteractorImplTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ private lateinit var actionInteractorImpl: DeviceItemActionInteractor
+
+ @Mock private lateinit var dialog: SystemUIDialog
+ @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+ @Mock private lateinit var device: BluetoothDevice
+ @Mock private lateinit var deviceItem: DeviceItem
+
+ @Before
+ fun setUp() {
+ actionInteractorImpl = kosmos.deviceItemActionInteractor
+ whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedDevice)
+ whenever(cachedDevice.address).thenReturn("ADDRESS")
+ whenever(cachedDevice.device).thenReturn(device)
+ }
+
+ @Test
+ fun testOnClick_connectedMedia_setActive() {
+ with(kosmos) {
+ testScope.runTest {
+ whenever(deviceItem.type)
+ .thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+ actionInteractorImpl.onClick(deviceItem, dialog)
+ verify(cachedDevice).setActive()
+ verify(bluetoothTileDialogLogger)
+ .logDeviceClick(
+ cachedDevice.address,
+ DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_activeMedia_disconnect() {
+ with(kosmos) {
+ testScope.runTest {
+ whenever(deviceItem.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+ actionInteractorImpl.onClick(deviceItem, dialog)
+ verify(cachedDevice).disconnect()
+ verify(bluetoothTileDialogLogger)
+ .logDeviceClick(
+ cachedDevice.address,
+ DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_connectedOtherDevice_disconnect() {
+ with(kosmos) {
+ testScope.runTest {
+ whenever(deviceItem.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ actionInteractorImpl.onClick(deviceItem, dialog)
+ verify(cachedDevice).disconnect()
+ verify(bluetoothTileDialogLogger)
+ .logDeviceClick(cachedDevice.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_saved_connect() {
+ with(kosmos) {
+ testScope.runTest {
+ whenever(deviceItem.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+ actionInteractorImpl.onClick(deviceItem, dialog)
+ verify(cachedDevice).connect()
+ verify(bluetoothTileDialogLogger)
+ .logDeviceClick(cachedDevice.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
new file mode 100644
index 000000000000..e8e37bc81866
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.internal.logging.uiEventLogger
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} }
+
+val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by
+ Kosmos.Fixture {
+ DeviceItemActionInteractorImpl(
+ testDispatcher,
+ bluetoothTileDialogLogger,
+ uiEventLogger,
+ )
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index daf4a3cbb9de..2b4f9503f371 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -23,7 +23,6 @@ import android.media.AudioManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
-import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.SysuiTestCase
@@ -39,7 +38,6 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -71,8 +69,6 @@ class DeviceItemInteractorTest : SysuiTestCase() {
@Mock private lateinit var localBluetoothManager: LocalBluetoothManager
- @Mock private lateinit var uiEventLogger: UiEventLogger
-
@Mock private lateinit var logger: BluetoothTileDialogLogger
private val fakeSystemClock = FakeSystemClock()
@@ -94,7 +90,6 @@ class DeviceItemInteractorTest : SysuiTestCase() {
adapter,
localBluetoothManager,
fakeSystemClock,
- uiEventLogger,
logger,
testScope.backgroundScope,
dispatcher
@@ -218,61 +213,6 @@ class DeviceItemInteractorTest : SysuiTestCase() {
}
}
- @Test
- fun testUpdateDeviceItemOnClick_connectedMedia_setActive() {
- testScope.runTest {
- `when`(deviceItem1.type).thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
-
- interactor.updateDeviceItemOnClick(deviceItem1)
-
- verify(cachedDevice1).setActive()
- verify(logger)
- .logDeviceClick(
- cachedDevice1.address,
- DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
- )
- }
- }
-
- @Test
- fun testUpdateDeviceItemOnClick_activeMedia_disconnect() {
- testScope.runTest {
- `when`(deviceItem1.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
-
- interactor.updateDeviceItemOnClick(deviceItem1)
-
- verify(cachedDevice1).disconnect()
- verify(logger)
- .logDeviceClick(cachedDevice1.address, DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
- }
- }
-
- @Test
- fun testUpdateDeviceItemOnClick_connectedOtherDevice_disconnect() {
- testScope.runTest {
- `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-
- interactor.updateDeviceItemOnClick(deviceItem1)
-
- verify(cachedDevice1).disconnect()
- verify(logger)
- .logDeviceClick(cachedDevice1.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
- }
- }
-
- @Test
- fun testUpdateDeviceItemOnClick_saved_connect() {
- testScope.runTest {
- `when`(deviceItem1.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
-
- interactor.updateDeviceItemOnClick(deviceItem1)
-
- verify(cachedDevice1).connect()
- verify(logger)
- .logDeviceClick(cachedDevice1.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
- }
- }
-
private fun createFactory(
isFilterMatchFunc: (CachedBluetoothDevice) -> Boolean,
deviceItem: DeviceItem
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
new file mode 100644
index 000000000000..05a2ca20037b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.keyboard.shortcut.ui
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
+import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShortcutHelperActivityStarterTest : SysuiTestCase() {
+
+ private val kosmos =
+ Kosmos().also {
+ it.testCase = this
+ it.testDispatcher = UnconfinedTestDispatcher()
+ }
+
+ private val testScope = kosmos.testScope
+ private val testHelper = kosmos.shortcutHelperTestHelper
+ private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
+ private val starter = kosmos.shortcutHelperActivityStarter
+
+ @Test
+ fun start_doesNotStartByDefault() =
+ testScope.runTest {
+ starter.start()
+
+ assertThat(fakeStartActivity.startIntents).isEmpty()
+ }
+
+ @Test
+ fun start_onToggle_startsActivity() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.toggle(deviceId = 456)
+
+ verifyShortcutHelperActivityStarted()
+ }
+
+ @Test
+ fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.toggle(deviceId = 456)
+ testHelper.toggle(deviceId = 456)
+ testHelper.toggle(deviceId = 456)
+ testHelper.toggle(deviceId = 456)
+
+ verifyShortcutHelperActivityStarted(numTimes = 2)
+ }
+
+ @Test
+ fun start_onRequestShowShortcuts_startsActivity() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.showFromActivity()
+
+ verifyShortcutHelperActivityStarted()
+ }
+
+ @Test
+ fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.showFromActivity()
+ testHelper.showFromActivity()
+ testHelper.showFromActivity()
+
+ verifyShortcutHelperActivityStarted(numTimes = 1)
+ }
+
+ @Test
+ fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyWhenNotStarted() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.hideFromActivity()
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 987)
+ testHelper.showFromActivity()
+ testHelper.hideFromActivity()
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 456)
+ testHelper.showFromActivity()
+
+ verifyShortcutHelperActivityStarted(numTimes = 2)
+ }
+
+ private fun verifyShortcutHelperActivityStarted(numTimes: Int = 1) {
+ assertThat(fakeStartActivity.startIntents).hasSize(numTimes)
+ fakeStartActivity.startIntents.forEach { intent ->
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.filterEquals(Intent(context, ShortcutHelperActivity::class.java)))
+ .isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
new file mode 100644
index 000000000000..86533086f67a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.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.systemui.keyboard.shortcut.ui.viewmodel
+
+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.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShortcutHelperViewModelTest : SysuiTestCase() {
+
+ private val kosmos =
+ Kosmos().also {
+ it.testCase = this
+ it.testDispatcher = UnconfinedTestDispatcher()
+ }
+
+ private val testScope = kosmos.testScope
+ private val testHelper = kosmos.shortcutHelperTestHelper
+
+ private val viewModel = kosmos.shortcutHelperViewModel
+
+ @Test
+ fun shouldShow_falseByDefault() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun shouldShow_trueAfterShowRequested() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.showFromActivity()
+
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun shouldShow_trueAfterToggleRequested() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.toggle(deviceId = 123)
+
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun shouldShow_falseAfterToggleTwice() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.toggle(deviceId = 123)
+ testHelper.toggle(deviceId = 123)
+
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun shouldShow_falseAfterViewDestroyed() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.toggle(deviceId = 567)
+ viewModel.onUserLeave()
+
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun shouldShow_doesNotEmitDuplicateValues() =
+ testScope.runTest {
+ val shouldShowValues by collectValues(viewModel.shouldShow)
+
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 987)
+ testHelper.showFromActivity()
+ viewModel.onUserLeave()
+ testHelper.hideFromActivity()
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 456)
+ testHelper.showFromActivity()
+
+ assertThat(shouldShowValues).containsExactly(false, true, false, true).inOrder()
+ }
+
+ @Test
+ fun shouldShow_emitsLatestValueToNewSubscribers() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.showFromActivity()
+
+ val shouldShowNew by collectLastValue(viewModel.shouldShow)
+ assertThat(shouldShowNew).isEqualTo(shouldShow)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index bcaad01f1a24..f5b5261de139 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,24 +19,20 @@
package com.android.systemui.keyguard.data.repository
-import android.os.fakeExecutorHandler
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.util.ThreadAssert
-import com.android.systemui.util.mockito.whenever
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.Before
import org.junit.Test
@@ -50,31 +46,32 @@ import org.mockito.MockitoAnnotations
class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
private lateinit var underTest: KeyguardBlueprintRepository
@Mock lateinit var configurationRepository: ConfigurationRepository
- @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint
@Mock lateinit var threadAssert: ThreadAssert
+
private val testScope = TestScope(StandardTestDispatcher())
private val kosmos: Kosmos = testKosmos()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- with(kosmos) {
- whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
- underTest =
- KeyguardBlueprintRepository(
- setOf(defaultLockscreenBlueprint),
- fakeExecutorHandler,
- threadAssert,
- )
- }
+ underTest = kosmos.keyguardBlueprintRepository
}
@Test
fun testApplyBlueprint_DefaultLayout() {
testScope.runTest {
val blueprint by collectLastValue(underTest.blueprint)
- underTest.applyBlueprint(defaultLockscreenBlueprint)
- assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+ underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)
+ assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
+ }
+ }
+
+ @Test
+ fun testApplyBlueprint_SplitShadeLayout() {
+ testScope.runTest {
+ val blueprint by collectLastValue(underTest.blueprint)
+ underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)
+ assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint)
}
}
@@ -83,33 +80,22 @@ class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
testScope.runTest {
val blueprint by collectLastValue(underTest.blueprint)
underTest.refreshBlueprint()
- assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+ assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
}
}
@Test
- fun testTransitionToLayout_validId() {
- assertThat(underTest.applyBlueprint(DEFAULT)).isTrue()
+ fun testTransitionToDefaultLayout_validId() {
+ assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue()
}
@Test
- fun testTransitionToLayout_invalidId() {
- assertThat(underTest.applyBlueprint("abc")).isFalse()
+ fun testTransitionToSplitShadeLayout_validId() {
+ assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue()
}
@Test
- fun testTransitionToSameBlueprint_refreshesBlueprint() =
- with(kosmos) {
- testScope.runTest {
- val transition by collectLastValue(underTest.refreshTransition)
- fakeExecutor.runAllReady()
- runCurrent()
-
- underTest.applyBlueprint(defaultLockscreenBlueprint)
- fakeExecutor.runAllReady()
- runCurrent()
-
- assertThat(transition).isNotNull()
- }
- }
+ fun testTransitionToLayout_invalidId() {
+ assertThat(underTest.applyBlueprint("abc")).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index ac5823e9365b..0bdf47a51670 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -29,6 +29,7 @@ import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
import com.android.systemui.kosmos.testScope
@@ -40,6 +41,7 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -54,7 +56,7 @@ import org.mockito.MockitoAnnotations
class KeyguardBlueprintInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val underTest by lazy { kosmos.keyguardBlueprintInteractor }
+ private val underTest = kosmos.keyguardBlueprintInteractor
private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository }
private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository }
@@ -79,8 +81,9 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() {
val blueprintId by collectLastValue(underTest.blueprintId)
kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
configurationRepository.onConfigurationChange()
- runCurrent()
+ runCurrent()
+ advanceUntilIdle()
assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.Companion.DEFAULT)
}
}
@@ -92,8 +95,9 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() {
val blueprintId by collectLastValue(underTest.blueprintId)
kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
configurationRepository.onConfigurationChange()
- runCurrent()
+ runCurrent()
+ advanceUntilIdle()
assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID)
}
}
@@ -102,12 +106,13 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() {
@DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
fun fingerprintPropertyInitialized_updatesBlueprint() {
testScope.runTest {
- val blueprintId by collectLastValue(underTest.blueprintId)
- kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
+
fingerprintPropertyRepository.supportsUdfps() // initialize properties
- runCurrent()
- assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID)
+ runCurrent()
+ advanceUntilIdle()
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
}
}
@@ -119,9 +124,23 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() {
kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
clockRepository.setCurrentClock(clockController)
configurationRepository.onConfigurationChange()
- runCurrent()
+ runCurrent()
+ advanceUntilIdle()
assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.DEFAULT)
}
}
+
+ @Test
+ fun testRefreshFromConfigChange() {
+ testScope.runTest {
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
+
+ configurationRepository.onConfigurationChange()
+
+ runCurrent()
+ advanceUntilIdle()
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index dbf6a29073a6..8a0613f9b010 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -66,25 +66,19 @@ class KeyguardBlueprintCommandListenerTest : SysuiTestCase() {
fun testHelp() {
command().execute(pw, listOf("help"))
verify(pw, atLeastOnce()).println(anyString())
- verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+ verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
}
@Test
fun testBlank() {
command().execute(pw, listOf())
verify(pw, atLeastOnce()).println(anyString())
- verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+ verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
}
@Test
fun testValidArg() {
command().execute(pw, listOf("fake"))
- verify(keyguardBlueprintInteractor).transitionToBlueprint("fake")
- }
-
- @Test
- fun testValidArg_Int() {
- command().execute(pw, listOf("1"))
- verify(keyguardBlueprintInteractor).transitionToBlueprint(1)
+ verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
deleted file mode 100644
index 8cc3a85ef6c8..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.panels.domain.repository
-
-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.qs.panels.data.repository.IconTilesRepositoryImpl
-import com.android.systemui.qs.pipeline.shared.TileSpec
-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 IconTilesRepositoryImplTest : SysuiTestCase() {
-
- private val underTest = IconTilesRepositoryImpl()
-
- @Test
- fun iconTilesSpecsIsValid() = runTest {
- val tilesSpecs by collectLastValue(underTest.iconTilesSpecs)
- assertThat(tilesSpecs).isEqualTo(ICON_ONLY_TILES_SPECS)
- }
-
- companion object {
- private val ICON_ONLY_TILES_SPECS =
- setOf(
- TileSpec.create("airplane"),
- TileSpec.create("battery"),
- TileSpec.create("cameratoggle"),
- TileSpec.create("cast"),
- TileSpec.create("color_correction"),
- TileSpec.create("inversion"),
- TileSpec.create("saver"),
- TileSpec.create("dnd"),
- TileSpec.create("flashlight"),
- TileSpec.create("location"),
- TileSpec.create("mictoggle"),
- TileSpec.create("nfc"),
- TileSpec.create("night"),
- TileSpec.create("rotation")
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index b5ef8c26a7ce..db11c3e89160 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -500,6 +500,42 @@ class QSTileViewImplTest : SysuiTestCase() {
)
}
+ @Test
+ fun onActivityLaunchAnimationEnd_onFreshTile_longPressPropertiesAreReset() {
+ // WHEN an activity launch animation ends on a fresh tile
+ tileView.onActivityLaunchAnimationEnd()
+
+ // THEN the tile's long-press effect properties are reset by default
+ assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
+ }
+
+ @Test
+ fun onUpdateLongPressEffectProperties_duringLongPressEffect_propertiesAreNotReset() {
+ // GIVEN a state that supports long-press
+ val state = QSTile.State()
+ tileView.changeState(state)
+
+ // WHEN the long-press effect is updating the properties
+ tileView.updateLongPressEffectProperties(1f)
+
+ // THEN the tile's long-press effect properties haven't reset
+ assertThat(tileView.haveLongPressPropertiesBeenReset).isFalse()
+ }
+
+ @Test
+ fun onActivityLaunchAnimationEnd_afterLongPressEffect_longPressPropertiesAreReset() {
+ // GIVEN a state that supports long-press and the long-press effect updating
+ val state = QSTile.State()
+ tileView.changeState(state)
+ tileView.updateLongPressEffectProperties(1f)
+
+ // WHEN an activity launch animation ends on a fresh tile
+ tileView.onActivityLaunchAnimationEnd()
+
+ // THEN the tile's long-press effect properties are reset
+ assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
+ }
+
class FakeTileView(
context: Context,
collapsed: Boolean,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 0f3714385725..bf7d909380df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -69,8 +69,9 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
@Before
fun setUp() {
- whenever(controllerFactory.create(eq(0), any())).thenReturn(controller0)
- whenever(controllerFactory.create(eq(1), any())).thenReturn(controller1)
+ whenever(controllerFactory.create(any(), any())).thenAnswer {
+ if (it.getArgument<Display>(0).displayId == 0) controller0 else controller1
+ }
whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0)
whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1)
}
@@ -78,12 +79,14 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
@Test
fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
testScope.runTest {
- setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val internalDisplay = display(TYPE_INTERNAL, id = 0)
+ val externalDisplay = display(TYPE_EXTERNAL, id = 1)
+ setDisplays(internalDisplay, externalDisplay)
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
- verify(controllerFactory).create(eq(0), any())
- verify(controllerFactory).create(eq(1), any())
+ verify(controllerFactory).create(eq(internalDisplay), any())
+ verify(controllerFactory).create(eq(externalDisplay), any())
val capturer = ArgumentCaptor<ScreenshotData>()
@@ -107,7 +110,9 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
@Test
fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
testScope.runTest {
- setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val internalDisplay = display(TYPE_INTERNAL, id = 0)
+ val externalDisplay = display(TYPE_EXTERNAL, id = 1)
+ setDisplays(internalDisplay, externalDisplay)
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(
createScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE),
@@ -115,8 +120,8 @@ class TakeScreenshotExecutorTest : SysuiTestCase() {
callback
)
- verify(controllerFactory).create(eq(0), any())
- verify(controllerFactory, never()).create(eq(1), any())
+ verify(controllerFactory).create(eq(internalDisplay), any())
+ verify(controllerFactory, never()).create(eq(externalDisplay), any())
val capturer = ArgumentCaptor<ScreenshotData>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 127a3d7b208b..269510e0b4b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -168,6 +168,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
private FakePowerRepository mPowerRepository;
@Mock
private UserTracker mUserTracker;
+ @Mock
+ private HeadsUpManager mHeadsUpManager;
private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mBubbleNotificationRow;
@@ -222,13 +224,12 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
mScreenOffAnimationController,
mStatusBarStateController).getPowerInteractor();
- HeadsUpManager headsUpManager = mock(HeadsUpManager.class);
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider =
new NotificationLaunchAnimatorControllerProvider(
new NotificationLaunchAnimationInteractor(
new NotificationLaunchAnimationRepository()),
mock(NotificationListContainer.class),
- headsUpManager,
+ mHeadsUpManager,
mJankMonitor);
mNotificationActivityStarter =
new StatusBarNotificationActivityStarter(
@@ -237,7 +238,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
mHandler,
mUiBgExecutor,
mVisibilityProvider,
- headsUpManager,
+ mHeadsUpManager,
mActivityStarter,
mCommandQueue,
mClickNotifier,
@@ -417,6 +418,51 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
}
@Test
+ public void testOnNotificationBubbleIconClicked_unbubble_keyGuardShowing()
+ throws RemoteException {
+ NotificationEntry entry = mBubbleNotificationRow.getEntry();
+ StatusBarNotification sbn = entry.getSbn();
+
+ // Given
+ sbn.getNotification().contentIntent = mContentIntent;
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+ // When
+ mNotificationActivityStarter.onNotificationBubbleIconClicked(entry);
+
+ // Then
+ verify(mBubblesManager).onUserChangedBubble(entry, false);
+
+ verify(mHeadsUpManager).removeNotification(entry.getKey(), true);
+
+ verifyNoMoreInteractions(mContentIntent);
+ verifyNoMoreInteractions(mShadeController);
+ }
+
+ @Test
+ public void testOnNotificationBubbleIconClicked_bubble_keyGuardShowing() {
+ NotificationEntry entry = mNotificationRow.getEntry();
+ StatusBarNotification sbn = entry.getSbn();
+
+ // Given
+ sbn.getNotification().contentIntent = mContentIntent;
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+ // When
+ mNotificationActivityStarter.onNotificationBubbleIconClicked(entry);
+
+ // Then
+ verify(mBubblesManager).onUserChangedBubble(entry, true);
+
+ verify(mHeadsUpManager).removeNotification(entry.getKey(), true);
+
+ verify(mContentIntent, atLeastOnce()).isActivity();
+ verifyNoMoreInteractions(mContentIntent);
+ }
+
+ @Test
public void testOnFullScreenIntentWhenDozing_wakeUpDevice() {
// GIVEN entry that can has a full screen intent that can show
PendingIntent fullScreenIntent = PendingIntent.getActivity(mContext, 1,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
index 3a61bf625804..9b3482b8ce42 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
@@ -18,8 +18,12 @@ package com.android.systemui.common.data.repository
import android.os.UserHandle
import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
class FakePackageChangeRepository(private val systemClock: SystemClock) : PackageChangeRepository {
@@ -31,6 +35,15 @@ class FakePackageChangeRepository(private val systemClock: SystemClock) : Packag
user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid)
}
+ private val _packageInstallSessions = MutableStateFlow<List<PackageInstallSession>>(emptyList())
+
+ override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+ _packageInstallSessions.asStateFlow()
+
+ fun setInstallSessions(sessions: List<PackageInstallSession>) {
+ _packageInstallSessions.value = sessions
+ }
+
suspend fun notifyChange(model: PackageChangeModel) {
_packageChanged.emit(model)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 329c0f1ab5b4..f7ce367ebb18 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -51,6 +51,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) :
override fun abortRestoreWidgets() {}
private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
- _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
+ _communalWidgets.value +=
+ listOf(CommunalWidgetContentModel.Available(id, providerInfo, priority))
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
new file mode 100644
index 000000000000..3190171b180d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.keyboard.shortcut
+
+import android.content.Intent
+
+class FakeShortcutHelperStartActivity : (Intent) -> Unit {
+
+ val startIntents = mutableListOf<Intent>()
+
+ override fun invoke(intent: Intent) {
+ startIntents += intent
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
new file mode 100644
index 000000000000..38f2a56d317f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.keyboard.shortcut
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.shortcutHelperRepository by
+ Kosmos.Fixture { ShortcutHelperRepository(fakeCommandQueue, broadcastDispatcher) }
+
+val Kosmos.shortcutHelperTestHelper by
+ Kosmos.Fixture {
+ ShortcutHelperTestHelper(
+ shortcutHelperRepository,
+ applicationContext,
+ broadcastDispatcher,
+ fakeCommandQueue
+ )
+ }
+
+val Kosmos.shortcutHelperInteractor by
+ Kosmos.Fixture { ShortcutHelperInteractor(shortcutHelperRepository) }
+
+val Kosmos.shortcutHelperViewModel by
+ Kosmos.Fixture { ShortcutHelperViewModel(testDispatcher, shortcutHelperInteractor) }
+
+val Kosmos.fakeShortcutHelperStartActivity by Kosmos.Fixture { FakeShortcutHelperStartActivity() }
+
+val Kosmos.shortcutHelperActivityStarter by
+ Kosmos.Fixture {
+ ShortcutHelperActivityStarter(
+ applicationContext,
+ applicationCoroutineScope,
+ shortcutHelperViewModel,
+ fakeShortcutHelperStartActivity,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
new file mode 100644
index 000000000000..772ce415a6e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.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.keyboard.shortcut.data.repository
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.broadcast.FakeBroadcastDispatcher
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+
+class ShortcutHelperTestHelper(
+ repo: ShortcutHelperRepository,
+ private val context: Context,
+ private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
+ private val fakeCommandQueue: FakeCommandQueue,
+) {
+
+ init {
+ repo.start()
+ }
+
+ fun hideFromActivity() {
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS)
+ )
+ }
+
+ fun showFromActivity() {
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS)
+ )
+ }
+
+ fun toggle(deviceId: Int) {
+ fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) }
+ }
+
+ fun hideForSystem() {
+ fakeCommandQueue.doForEachCallback { it.dismissKeyboardShortcutsMenu() }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
index 302ac35f1a8a..093ebd6c6b63 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.qs.pipeline.domain.interactor
+package com.android.systemui.qs
import com.android.internal.logging.InstanceId
import com.android.systemui.animation.Expandable
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt
new file mode 100644
index 000000000000..d686699f795b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.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.qs.panels.data.repository
+
+import android.content.packageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
+import com.android.systemui.settings.userTracker
+import com.android.systemui.util.mockito.whenever
+
+val Kosmos.iconAndNameCustomRepository by
+ Kosmos.Fixture {
+ whenever(userTracker.userContext.packageManager).thenReturn(packageManager)
+ IconAndNameCustomRepository(
+ installedTilesRepository,
+ userTracker,
+ backgroundCoroutineContext,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt
new file mode 100644
index 000000000000..ff33650d9f02
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.panels.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+
+var Kosmos.stockTilesRepository by
+ Kosmos.Fixture {
+ testCase.context.orCreateTestableResources
+ StockTilesRepository(testCase.context.resources)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt
new file mode 100644
index 000000000000..bd54fd471807
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.panels.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+
+val Kosmos.editTilesListInteractor by
+ Kosmos.Fixture {
+ EditTilesListInteractor(
+ stockTilesRepository,
+ qSTileConfigProvider,
+ iconAndNameCustomRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
new file mode 100644
index 000000000000..612a5d9917ff
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.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.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor
+
+val Kosmos.editModeViewModel by
+ Kosmos.Fixture {
+ EditModeViewModel(
+ editTilesListInteractor,
+ currentTilesInteractor,
+ minimumTilesInteractor,
+ infiniteGridLayout,
+ applicationCoroutineScope,
+ gridLayoutTypeInteractor,
+ gridLayoutMap,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
index ff6b7d083df7..ed4c67ee3511 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
@@ -17,23 +17,78 @@
package com.android.systemui.qs.pipeline.data.repository
import android.content.ComponentName
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
class FakeInstalledTilesComponentRepository : InstalledTilesComponentRepository {
- private val installedComponentsPerUser =
- mutableMapOf<Int, MutableStateFlow<Set<ComponentName>>>()
+ private val installedServicesPerUser = mutableMapOf<Int, MutableStateFlow<List<ServiceInfo>>>()
override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
- return getFlow(userId).asStateFlow()
+ return getFlow(userId).map { it.map { it.componentName }.toSet() }
+ }
+
+ override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> {
+ return getFlow(userId).value
}
fun setInstalledPackagesForUser(userId: Int, components: Set<ComponentName>) {
- getFlow(userId).value = components
+ getFlow(userId).value =
+ components.map {
+ ServiceInfo().apply {
+ packageName = it.packageName
+ name = it.className
+ applicationInfo = ApplicationInfo()
+ }
+ }
+ }
+
+ fun setInstalledServicesForUser(userId: Int, services: List<ServiceInfo>) {
+ getFlow(userId).value = services.toList()
}
- private fun getFlow(userId: Int): MutableStateFlow<Set<ComponentName>> =
- installedComponentsPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
+ private fun getFlow(userId: Int): MutableStateFlow<List<ServiceInfo>> =
+ installedServicesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) }
+
+ companion object {
+ fun ServiceInfo(
+ componentName: ComponentName,
+ serviceName: String,
+ serviceIcon: Drawable? = null,
+ appName: String = "",
+ appIcon: Drawable? = null
+ ): ServiceInfo {
+ val appInfo =
+ object : ApplicationInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return appName
+ }
+
+ override fun loadIcon(pm: PackageManager?): Drawable? {
+ return appIcon
+ }
+ }
+ val serviceInfo =
+ object : ServiceInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return serviceName
+ }
+
+ override fun loadIcon(pm: PackageManager?): Drawable? {
+ return serviceIcon ?: getApplicationInfo().loadIcon(pm)
+ }
+ }
+ .apply {
+ packageName = componentName.packageName
+ name = componentName.className
+ applicationInfo = appInfo
+ }
+ return serviceInfo
+ }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt
new file mode 100644
index 000000000000..ef1189f25cff
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.pipeline.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository
+
+var Kosmos.minimumTilesInteractor by
+ Kosmos.Fixture { MinimumTilesInteractor(minimumTilesRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 3e9ae4d2e354..1f2ecb7d172d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -37,7 +37,7 @@ import kotlinx.coroutines.yield
class FakeUserRepository @Inject constructor() : UserRepository {
companion object {
// User id to represent a non system (human) user id. We presume this is the main user.
- private const val MAIN_USER_ID = 10
+ const val MAIN_USER_ID = 10
private const val DEFAULT_SELECTED_USER = 0
private val DEFAULT_SELECTED_USER_INFO =
@@ -84,6 +84,10 @@ class FakeUserRepository @Inject constructor() : UserRepository {
override var isRefreshUsersPaused: Boolean = false
+ override suspend fun getMainUserId(): Int? {
+ return MAIN_USER_ID
+ }
+
var refreshUsersCallCount: Int = 0
private set
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 2f54f8c8bdb5..2a7458fca249 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -1469,10 +1469,7 @@ public class TouchExplorer extends BaseEventStreamTransformation
int policyFlags = mState.getLastReceivedPolicyFlags();
if (mState.isDragging()) {
// Send an event to the end of the drag gesture.
- int pointerIdBits = ALL_POINTER_ID_BITS;
- if (Flags.fixDragPointerWhenEndingDrag()) {
- pointerIdBits = 1 << mDraggingPointerId;
- }
+ int pointerIdBits = 1 << mDraggingPointerId;
mDispatcher.sendMotionEvent(event, ACTION_UP, rawEvent, pointerIdBits, policyFlags);
}
mState.startDelegating();
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 9b72288955a7..d7c65c748d8f 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -41,7 +41,6 @@ import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
-import android.view.Display;
import android.view.InputDevice;
import android.view.WindowManager;
@@ -169,7 +168,6 @@ class InputController {
createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
- setVirtualMousePointerDisplayId(displayId);
}
void createTouchscreen(@NonNull String deviceName, int vendorId, int productId,
@@ -236,15 +234,6 @@ class InputController {
if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_KEYBOARD) {
mInputManagerInternal.removeKeyboardLayoutAssociation(phys);
}
-
- // Reset values to the default if all virtual mice are unregistered, or set display
- // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be
- // removed from the mInputDeviceDescriptors instance variable prior to this point.
- if (inputDeviceDescriptor.isMouse()) {
- if (getVirtualMousePointerDisplayId() == inputDeviceDescriptor.getDisplayId()) {
- updateActivePointerDisplayIdLocked();
- }
- }
}
/**
@@ -276,29 +265,6 @@ class InputController {
mWindowManager.setDisplayImePolicy(displayId, policy);
}
- // TODO(b/293587049): Remove after pointer icon refactor is complete.
- @GuardedBy("mLock")
- private void updateActivePointerDisplayIdLocked() {
- InputDeviceDescriptor mostRecentlyCreatedMouse = null;
- for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
- InputDeviceDescriptor otherInputDeviceDescriptor = mInputDeviceDescriptors.valueAt(i);
- if (otherInputDeviceDescriptor.isMouse()) {
- if (mostRecentlyCreatedMouse == null
- || (otherInputDeviceDescriptor.getCreationOrderNumber()
- > mostRecentlyCreatedMouse.getCreationOrderNumber())) {
- mostRecentlyCreatedMouse = otherInputDeviceDescriptor;
- }
- }
- }
- if (mostRecentlyCreatedMouse != null) {
- setVirtualMousePointerDisplayId(
- mostRecentlyCreatedMouse.getDisplayId());
- } else {
- // All mice have been unregistered
- setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY);
- }
- }
-
/**
* Validates a device name by checking whether a device with the same name already exists.
* @param deviceName The name of the device to be validated
@@ -355,9 +321,6 @@ class InputController {
if (inputDeviceDescriptor == null) {
return false;
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(),
event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
}
@@ -384,9 +347,6 @@ class InputController {
if (inputDeviceDescriptor == null) {
return false;
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(),
event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos());
}
@@ -399,9 +359,6 @@ class InputController {
if (inputDeviceDescriptor == null) {
return false;
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(),
event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos());
}
@@ -415,9 +372,6 @@ class InputController {
throw new IllegalArgumentException(
"Could not get cursor position for input device for given token");
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return LocalServices.getService(InputManagerInternal.class).getCursorPosition(
inputDeviceDescriptor.getDisplayId());
}
@@ -878,22 +832,4 @@ class InputController {
/** Returns true if the calling thread is a valid thread for device creation. */
boolean isValidThread();
}
-
- // TODO(b/293587049): Remove after pointer icon refactor is complete.
- private void setVirtualMousePointerDisplayId(int displayId) {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- // We no longer need to set the pointer display when pointer choreographer is enabled.
- return;
- }
- mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
- }
-
- // TODO(b/293587049): Remove after pointer icon refactor is complete.
- private int getVirtualMousePointerDisplayId() {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- // We no longer need to get the pointer display when pointer choreographer is enabled.
- return Display.INVALID_DISPLAY;
- }
- return mInputManagerInternal.getVirtualMousePointerDisplayId();
- }
}
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 7acca19f9d79..e2b6bd6ae360 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -36,6 +36,7 @@ import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.AppOpsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -47,6 +48,7 @@ import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
+import android.net.vcn.Flags;
import android.net.vcn.IVcnManagementService;
import android.net.vcn.IVcnStatusCallback;
import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
@@ -68,6 +70,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -431,6 +434,8 @@ public class VcnManagementService extends IVcnManagementService.Stub {
mTelephonySubscriptionTracker.register();
}
+ // The system server automatically has the required permissions for #getMainUser()
+ @SuppressLint("AndroidFrameworkRequiresPermission")
private void enforcePrimaryUser() {
final int uid = mDeps.getBinderCallingUid();
if (uid == Process.SYSTEM_UID) {
@@ -438,7 +443,20 @@ public class VcnManagementService extends IVcnManagementService.Stub {
"Calling identity was System Server. Was Binder calling identity cleared?");
}
- if (!UserHandle.getUserHandleForUid(uid).isSystem()) {
+ final UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+
+ if (Flags.enforceMainUser()) {
+ final UserManager userManager = mContext.getSystemService(UserManager.class);
+
+ Binder.withCleanCallingIdentity(
+ () -> {
+ if (!Objects.equals(userManager.getMainUser(), userHandle)) {
+ throw new SecurityException(
+ "VcnManagementService can only be used by callers running as"
+ + " the main user");
+ }
+ });
+ } else if (!userHandle.isSystem()) {
throw new SecurityException(
"VcnManagementService can only be used by callers running as the primary user");
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 3cea01464c10..a182a106ed8c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -115,6 +115,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DebugUtils;
import android.util.DisplayMetrics;
+import android.util.SparseArray;
import android.util.TeeWriter;
import android.util.proto.ProtoOutputStream;
import android.view.Choreographer;
@@ -275,9 +276,9 @@ final class ActivityManagerShellCommand extends ShellCommand {
case "compact":
return runCompact(pw);
case "freeze":
- return runFreeze(pw);
+ return runFreeze(pw, true);
case "unfreeze":
- return runUnfreeze(pw);
+ return runFreeze(pw, false);
case "instrument":
getOutPrintWriter().println("Error: must be invoked through 'am instrument'.");
return -1;
@@ -1203,45 +1204,27 @@ final class ActivityManagerShellCommand extends ShellCommand {
}
@NeverCompile
- int runFreeze(PrintWriter pw) throws RemoteException {
+ int runFreeze(PrintWriter pw, boolean freeze) throws RemoteException {
String freezerOpt = getNextOption();
boolean isSticky = false;
- if (freezerOpt != null) {
- isSticky = freezerOpt.equals("--sticky");
- }
- ProcessRecord app = getProcessFromShell();
- if (app == null) {
- getErrPrintWriter().println("Error: could not find process");
- return -1;
- }
- pw.println("Freezing pid: " + app.mPid + " sticky=" + isSticky);
- synchronized (mInternal) {
- synchronized (mInternal.mProcLock) {
- app.mOptRecord.setFreezeSticky(isSticky);
- mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app);
- }
- }
- return 0;
- }
- @NeverCompile
- int runUnfreeze(PrintWriter pw) throws RemoteException {
- String freezerOpt = getNextOption();
- boolean isSticky = false;
if (freezerOpt != null) {
isSticky = freezerOpt.equals("--sticky");
}
- ProcessRecord app = getProcessFromShell();
- if (app == null) {
- getErrPrintWriter().println("Error: could not find process");
+ ProcessRecord proc = getProcessFromShell();
+ if (proc == null) {
return -1;
}
- pw.println("Unfreezing pid: " + app.mPid);
+ pw.print(freeze ? "Freezing" : "Unfreezing");
+ pw.print(" process " + proc.processName);
+ pw.println(" (" + proc.mPid + ") sticky=" + isSticky);
synchronized (mInternal) {
synchronized (mInternal.mProcLock) {
- synchronized (mInternal.mOomAdjuster.mCachedAppOptimizer.mFreezerLock) {
- app.mOptRecord.setFreezeSticky(isSticky);
- mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(app, 0,
+ proc.mOptRecord.setFreezeSticky(isSticky);
+ if (freeze) {
+ mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(proc);
+ } else {
+ mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(proc, 0,
true);
}
}
@@ -1250,43 +1233,42 @@ final class ActivityManagerShellCommand extends ShellCommand {
}
/**
- * Parses from the shell the process name and user id if provided and provides the corresponding
- * {@link ProcessRecord)} If no user is provided, it will fallback to current user.
- * Example usage: {@code <processname> --user current} or {@code <processname>}
- * @return process record of process, null if none found.
+ * Parses from the shell the pid or process name and provides the corresponding
+ * {@link ProcessRecord}.
+ * Example usage: {@code <processname>} or {@code <pid>}
+ * @return process record of process, null if none or more than one found.
* @throws RemoteException
*/
@NeverCompile
ProcessRecord getProcessFromShell() throws RemoteException {
- ProcessRecord app;
- String processName = getNextArgRequired();
- synchronized (mInternal.mProcLock) {
- // Default to current user
- int userId = getUserIdFromShellOrFallback();
- final int uid =
- mInternal.getPackageManagerInternal().getPackageUid(processName, 0, userId);
- app = mInternal.getProcessRecordLocked(processName, uid);
- }
- return app;
- }
+ ProcessRecord proc = null;
+ String process = getNextArgRequired();
+ try {
+ int pid = Integer.parseInt(process);
+ synchronized (mInternal.mPidsSelfLocked) {
+ proc = mInternal.mPidsSelfLocked.get(pid);
+ }
+ } catch (NumberFormatException e) {
+ // Fallback to process name if it's not a valid pid
+ }
- /**
- * @return User id from command line provided in the form of
- * {@code --user <userid|current|all>} and if the argument is not found it will fallback
- * to current user.
- * @throws RemoteException
- */
- @NeverCompile
- int getUserIdFromShellOrFallback() throws RemoteException {
- int userId = mInterface.getCurrentUserId();
- String userOpt = getNextOption();
- if (userOpt != null && "--user".equals(userOpt)) {
- int inputUserId = UserHandle.parseUserArg(getNextArgRequired());
- if (inputUserId != UserHandle.USER_CURRENT) {
- userId = inputUserId;
+ if (proc == null) {
+ synchronized (mInternal.mProcLock) {
+ ArrayMap<String, SparseArray<ProcessRecord>> all =
+ mInternal.mProcessList.getProcessNamesLOSP().getMap();
+ SparseArray<ProcessRecord> procs = all.get(process);
+ if (procs == null || procs.size() == 0) {
+ getErrPrintWriter().println("Error: could not find process");
+ return null;
+ } else if (procs.size() > 1) {
+ getErrPrintWriter().println("Error: more than one processes found");
+ return null;
+ }
+ proc = procs.valueAt(0);
}
}
- return userId;
+
+ return proc;
}
int runDumpHeap(PrintWriter pw) throws RemoteException {
@@ -4306,24 +4288,26 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" --allow-background-activity-starts: The receiver may start activities");
pw.println(" even if in the background.");
pw.println(" --async: Send without waiting for the completion of the receiver.");
- pw.println(" compact [some|full] <process_name> [--user <USER_ID>]");
- pw.println(" Perform a single process compaction.");
+ pw.println(" compact {some|full} <PROCESS>");
+ pw.println(" Perform a single process compaction. The given <PROCESS> argument");
+ pw.println(" may be either a process name or pid.");
pw.println(" some: execute file compaction.");
pw.println(" full: execute anon + file compaction.");
- pw.println(" system: system compaction.");
pw.println(" compact system");
pw.println(" Perform a full system compaction.");
- pw.println(" compact native [some|full] <pid>");
+ pw.println(" compact native {some|full} <pid>");
pw.println(" Perform a native compaction for process with <pid>.");
pw.println(" some: execute file compaction.");
pw.println(" full: execute anon + file compaction.");
- pw.println(" freeze [--sticky] <processname> [--user <USER_ID>]");
- pw.println(" Freeze a process.");
- pw.println(" --sticky: persists the frozen state for the process lifetime or");
+ pw.println(" freeze [--sticky] <PROCESS>");
+ pw.println(" Freeze a process. The given <PROCESS> argument");
+ pw.println(" may be either a process name or pid. Options are:");
+ pw.println(" --sticky: persists the frozen state for the process lifetime or");
pw.println(" until an unfreeze is triggered via shell");
- pw.println(" unfreeze [--sticky] <processname> [--user <USER_ID>]");
- pw.println(" Unfreeze a process.");
- pw.println(" --sticky: persists the unfrozen state for the process lifetime or");
+ pw.println(" unfreeze [--sticky] <PROCESS>");
+ pw.println(" Unfreeze a process. The given <PROCESS> argument");
+ pw.println(" may be either a process name or pid. Options are:");
+ pw.println(" --sticky: persists the unfrozen state for the process lifetime or");
pw.println(" until a freeze is triggered via shell");
pw.println(" instrument [-r] [-e <NAME> <VALUE>] [-p <FILE>] [-w]");
pw.println(" [--user <USER_ID> | current]");
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 95206212c99d..a25817d491c4 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -185,6 +185,7 @@ public class SettingsToPropertiesMapper {
"pmw",
"power",
"preload_safety",
+ "printing",
"privacy_infra_policy",
"resource_manager",
"responsible_apis",
diff --git a/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java
new file mode 100644
index 000000000000..3e8acee26e81
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java
@@ -0,0 +1,93 @@
+/*
+ * 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.biometrics;
+
+import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.biometrics.BiometricsProtoEnums;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.server.biometrics.sensors.BiometricNotificationUtils;
+
+/**
+ * Receives broadcast to biometrics dangling notification.
+ */
+public class BiometricDanglingReceiver extends BroadcastReceiver {
+ private static final String TAG = "BiometricDanglingReceiver";
+
+ public static final String ACTION_FINGERPRINT_RE_ENROLL_LAUNCH =
+ "action_fingerprint_re_enroll_launch";
+ public static final String ACTION_FINGERPRINT_RE_ENROLL_DISMISS =
+ "action_fingerprint_re_enroll_dismiss";
+
+ public static final String ACTION_FACE_RE_ENROLL_LAUNCH =
+ "action_face_re_enroll_launch";
+ public static final String ACTION_FACE_RE_ENROLL_DISMISS =
+ "action_face_re_enroll_dismiss";
+
+ public static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+
+ /**
+ * Constructor for BiometricDanglingReceiver.
+ *
+ * @param context context
+ * @param modality the value from BiometricsProtoEnums.MODALITY_*
+ */
+ public BiometricDanglingReceiver(@NonNull Context context, int modality) {
+ final IntentFilter intentFilter = new IntentFilter();
+ if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
+ intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+ intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_DISMISS);
+ } else if (modality == BiometricsProtoEnums.MODALITY_FACE) {
+ intentFilter.addAction(ACTION_FACE_RE_ENROLL_LAUNCH);
+ intentFilter.addAction(ACTION_FACE_RE_ENROLL_DISMISS);
+ }
+ context.registerReceiver(this, intentFilter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Slog.d(TAG, "Received: " + intent.getAction());
+ if (ACTION_FINGERPRINT_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
+ launchBiometricEnrollActivity(context, Settings.ACTION_FINGERPRINT_ENROLL);
+ BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
+ } else if (ACTION_FINGERPRINT_RE_ENROLL_DISMISS.equals(intent.getAction())) {
+ BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
+ } else if (ACTION_FACE_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
+ launchBiometricEnrollActivity(context, FACE_SETTINGS_ACTION);
+ BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
+ } else if (ACTION_FACE_RE_ENROLL_DISMISS.equals(intent.getAction())) {
+ BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
+ }
+ context.unregisterReceiver(this);
+ }
+
+ private void launchBiometricEnrollActivity(Context context, String action) {
+ context.sendBroadcast(new Intent(ACTION_CLOSE_SYSTEM_DIALOGS));
+ final Intent intent = new Intent(action);
+ intent.setPackage(SETTINGS_PACKAGE);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 0e22f7511af9..eaa5e2a6befb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -24,13 +24,18 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceEnrollOptions;
import android.hardware.fingerprint.FingerprintEnrollOptions;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.text.BidiFormatter;
import android.util.Slog;
import com.android.internal.R;
+import com.android.server.biometrics.BiometricDanglingReceiver;
+
+import java.util.List;
/**
* Biometric notification helper class.
@@ -39,6 +44,7 @@ public class BiometricNotificationUtils {
private static final String TAG = "BiometricNotificationUtils";
private static final String FACE_RE_ENROLL_NOTIFICATION_TAG = "FaceReEnroll";
+ private static final String FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG = "FingerprintReEnroll";
private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintBadCalibration";
private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock";
private static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
@@ -50,6 +56,8 @@ public class BiometricNotificationUtils {
private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel";
private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel";
private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel";
+ private static final String FINGERPRINT_RE_ENROLL_CHANNEL =
+ "FingerprintReEnrollNotificationChannel";
private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL =
"FingerprintBadCalibrationNotificationChannel";
private static final long NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -177,10 +185,124 @@ public class BiometricNotificationUtils {
BAD_CALIBRATION_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false);
}
+ /**
+ * Shows a biometric re-enroll notification.
+ */
+ public static void showBiometricReEnrollNotification(@NonNull Context context,
+ @NonNull List<String> identifiers, boolean allIdentifiersDeleted, int modality) {
+ final boolean isFingerprint = modality == BiometricsProtoEnums.MODALITY_FINGERPRINT;
+ final String reEnrollName = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
+ : FACE_RE_ENROLL_NOTIFICATION_TAG;
+ if (identifiers.isEmpty()) {
+ Slog.v(TAG, "Skipping " + reEnrollName + " notification : empty list");
+ return;
+ }
+ Slog.d(TAG, "Showing " + reEnrollName + " notification :[" + identifiers.size()
+ + " identifier(s) deleted, allIdentifiersDeleted=" + allIdentifiersDeleted + "]");
+
+ final String name =
+ context.getString(R.string.device_unlock_notification_name);
+ final String title = context.getString(isFingerprint
+ ? R.string.fingerprint_dangling_notification_title
+ : R.string.face_dangling_notification_title);
+ final String content = isFingerprint
+ ? getFingerprintDanglingContentString(context, identifiers, allIdentifiersDeleted)
+ : context.getString(R.string.face_dangling_notification_msg);
+
+ // Create "Set up" notification action button.
+ final Intent setupIntent = new Intent(
+ isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH
+ : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
+ final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+ setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+ final String setupText =
+ context.getString(R.string.biometric_dangling_notification_action_set_up);
+ final Notification.Action setupAction = new Notification.Action.Builder(
+ null, setupText, setupPendingIntent).build();
+
+ // Create "Not now" notification action button.
+ final Intent notNowIntent = new Intent(
+ isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS
+ : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_DISMISS);
+ final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+ notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+ final String notNowText = context.getString(
+ R.string.biometric_dangling_notification_action_not_now);
+ final Notification.Action notNowAction = new Notification.Action.Builder(
+ null, notNowText, notNowPendingIntent).build();
+
+ final String channel = isFingerprint ? FINGERPRINT_RE_ENROLL_CHANNEL
+ : FACE_RE_ENROLL_CHANNEL;
+ final String tag = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
+ : FACE_RE_ENROLL_NOTIFICATION_TAG;
+
+ showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction,
+ notNowAction, Notification.CATEGORY_SYSTEM, channel, tag,
+ Notification.VISIBILITY_SECRET, false);
+ }
+
+ private static String getFingerprintDanglingContentString(Context context,
+ @NonNull List<String> fingerprints, boolean allFingerprintDeleted) {
+ if (fingerprints.isEmpty()) {
+ return null;
+ }
+
+ final int resId;
+ final int size = fingerprints.size();
+ final StringBuilder first = new StringBuilder();
+ final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+ if (size > 1) {
+ // If there are more than 1 fingerprint deleted, the "second" will be the last
+ // fingerprint and set the others to "first".
+ // For example, if we have 3 fingerprints deleted(fp1, fp2 and fp3):
+ // first = "fp1, fp2"
+ // second = "fp3"
+ final String separator = ", ";
+ String second = null;
+ for (int i = 0; i < size; i++) {
+ if (i == size - 1) {
+ second = bidiFormatter.unicodeWrap("\"" + fingerprints.get(i) + "\"");
+ } else {
+ first.append(bidiFormatter.unicodeWrap("\""));
+ first.append(bidiFormatter.unicodeWrap(fingerprints.get(i)));
+ first.append(bidiFormatter.unicodeWrap("\""));
+ if (i < size - 2) {
+ first.append(bidiFormatter.unicodeWrap(separator));
+ }
+ }
+ }
+ if (allFingerprintDeleted) {
+ resId = R.string.fingerprint_dangling_notification_msg_all_deleted_2;
+ } else {
+ resId = R.string.fingerprint_dangling_notification_msg_2;
+ }
+
+ return String.format(context.getString(resId), first, second);
+ } else {
+ if (allFingerprintDeleted) {
+ resId = R.string.fingerprint_dangling_notification_msg_all_deleted_1;
+ } else {
+ resId = R.string.fingerprint_dangling_notification_msg_1;
+ }
+ first.append(bidiFormatter.unicodeWrap("\""));
+ first.append(bidiFormatter.unicodeWrap(fingerprints.get(0)));
+ first.append(bidiFormatter.unicodeWrap("\""));
+ return String.format(context.getString(resId), first);
+ }
+ }
+
+ private static void showNotificationHelper(Context context, String name, String title,
+ String content, PendingIntent pendingIntent, String category, String channelName,
+ String notificationTag, int visibility, boolean listenToDismissEvent) {
+ showNotificationHelper(context, name, title, content, pendingIntent,
+ null /* positiveAction */, null /* negativeAction */, category, channelName,
+ notificationTag, visibility, listenToDismissEvent);
+ }
+
private static void showNotificationHelper(Context context, String name, String title,
- String content, PendingIntent pendingIntent, String category,
- String channelName, String notificationTag, int visibility,
- boolean listenToDismissEvent) {
+ String content, PendingIntent pendingIntent, Notification.Action positiveAction,
+ Notification.Action negativeAction, String category, String channelName,
+ String notificationTag, int visibility, boolean listenToDismissEvent) {
Slog.v(TAG," listenToDismissEvent = " + listenToDismissEvent);
final PendingIntent dismissIntent = PendingIntent.getActivityAsUser(context,
0 /* requestCode */, DISMISS_FRR_INTENT, PendingIntent.FLAG_IMMUTABLE /* flags */,
@@ -202,6 +324,12 @@ public class BiometricNotificationUtils {
.setContentIntent(pendingIntent)
.setVisibility(visibility);
+ if (positiveAction != null) {
+ builder.addAction(positiveAction);
+ }
+ if (negativeAction != null) {
+ builder.addAction(negativeAction);
+ }
if (listenToDismissEvent) {
builder.setDeleteIntent(dismissIntent);
}
@@ -253,4 +381,14 @@ public class BiometricNotificationUtils {
UserHandle.CURRENT);
}
+ /**
+ * Cancels a fingerprint enrollment notification
+ */
+ public static void cancelFingerprintReEnrollNotification(@NonNull Context context) {
+ final NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+ notificationManager.cancelAsUser(FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID,
+ UserHandle.CURRENT);
+ }
+
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
index 6daaad1baf83..81ab26dd581e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
@@ -22,6 +22,7 @@ import android.hardware.biometrics.BiometricAuthenticator;
import android.os.IBinder;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.BiometricsProto;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
@@ -44,6 +45,7 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
private List<? extends BiometricAuthenticator.Identifier> mEnrolledList;
// List of templates to remove from the HAL
private List<BiometricAuthenticator.Identifier> mUnknownHALTemplates = new ArrayList<>();
+ private final int mInitialEnrolledSize;
protected InternalEnumerateClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
@NonNull IBinder token, int userId, @NonNull String owner,
@@ -55,6 +57,7 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner,
0 /* cookie */, sensorId, logger, biometricContext);
mEnrolledList = enrolledList;
+ mInitialEnrolledSize = mEnrolledList.size();
mUtils = utils;
}
@@ -111,8 +114,10 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
// At this point, mEnrolledList only contains templates known to the framework and
// not the HAL.
+ final List<String> names = new ArrayList<>();
for (int i = 0; i < mEnrolledList.size(); i++) {
BiometricAuthenticator.Identifier identifier = mEnrolledList.get(i);
+ names.add(identifier.getName().toString());
Slog.e(TAG, "doTemplateCleanup(): Removing dangling template from framework: "
+ identifier.getBiometricId() + " " + identifier.getName());
mUtils.removeBiometricForUser(getContext(),
@@ -120,6 +125,11 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
getLogger().logUnknownEnrollmentInFramework();
}
+
+ // Send dangling notification.
+ if (!names.isEmpty()) {
+ sendDanglingNotification(names);
+ }
mEnrolledList.clear();
}
@@ -127,8 +137,24 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T>
return mUnknownHALTemplates;
}
+ /**
+ * Send the dangling notification.
+ */
+ @VisibleForTesting
+ public void sendDanglingNotification(@NonNull List<String> identifierNames) {
+ if (!identifierNames.isEmpty()) {
+ Slog.e(TAG, "sendDanglingNotification(): initial enrolledSize="
+ + mInitialEnrolledSize + ", after clean up size=" + mEnrolledList.size());
+ final boolean allIdentifiersDeleted = mEnrolledList.size() == mInitialEnrolledSize;
+ BiometricNotificationUtils.showBiometricReEnrollNotification(
+ getContext(), identifierNames, allIdentifiersDeleted, getModality());
+ }
+ }
+
@Override
public int getProtoEnum() {
return BiometricsProto.CM_ENUMERATE;
}
+
+ protected abstract int getModality();
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
index d85455e0e76b..6ce3bc59eb56 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
@@ -18,12 +18,14 @@ package com.android.server.biometrics.sensors.face.aidl;
import android.annotation.NonNull;
import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.face.IFace;
import android.hardware.face.Face;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.BiometricUtils;
@@ -35,7 +37,8 @@ import java.util.function.Supplier;
/**
* Face-specific internal enumerate client for the {@link IFace} AIDL HAL interface.
*/
-class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
+@VisibleForTesting
+public class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
private static final String TAG = "FaceInternalEnumerateClient";
FaceInternalEnumerateClient(@NonNull Context context,
@@ -56,4 +59,9 @@ class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
mCallback.onClientFinished(this, false /* success */);
}
}
+
+ @Override
+ protected int getModality() {
+ return BiometricsProtoEnums.MODALITY_FACE;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index e71cffe0b8c5..f0a418951505 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,6 +52,7 @@ import android.view.Surface;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricDanglingReceiver;
import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Utils;
import com.android.server.biometrics.log.BiometricContext;
@@ -201,6 +202,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
mBiometricHandlerProvider = biometricHandlerProvider;
initAuthenticationBroadcastReceiver();
+ initFaceDanglingBroadcastReceiver();
initSensors(resetLockoutRequiresChallenge, props);
}
@@ -214,6 +216,10 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider {
});
}
+ private void initFaceDanglingBroadcastReceiver() {
+ new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FACE);
+ }
+
private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) {
if (resetLockoutRequiresChallenge) {
Slog.d(getTag(), "Adding HIDL configs");
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
index a5a832aaaf59..2849bd9c92a3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
@@ -18,11 +18,13 @@ package com.android.server.biometrics.sensors.fingerprint.aidl;
import android.annotation.NonNull;
import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.fingerprint.Fingerprint;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.BiometricUtils;
@@ -35,7 +37,8 @@ import java.util.function.Supplier;
* Fingerprint-specific internal client supporting the
* {@link android.hardware.biometrics.fingerprint.IFingerprint} AIDL interface.
*/
-class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
+@VisibleForTesting
+public class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
private static final String TAG = "FingerprintInternalEnumerateClient";
protected FingerprintInternalEnumerateClient(@NonNull Context context,
@@ -56,4 +59,9 @@ class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSes
mCallback.onClientFinished(this, false /* success */);
}
}
+
+ @Override
+ protected int getModality() {
+ return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 6874c71267fb..c0dcd4943260 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -58,6 +58,7 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricDanglingReceiver;
import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.Utils;
@@ -205,6 +206,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
mBiometricHandlerProvider = biometricHandlerProvider;
initAuthenticationBroadcastReceiver();
+ initFingerprintDanglingBroadcastReceiver();
initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
}
@@ -218,6 +220,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi
});
}
+ private void initFingerprintDanglingBroadcastReceiver() {
+ new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ }
+
private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props,
GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
if (!resetLockoutRequiresHardwareAuthToken) {
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 4e9cf51fda56..b47631c35e38 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -84,29 +84,6 @@ public abstract class InputManagerInternal {
@NonNull IBinder toChannelToken);
/**
- * Sets the display id that the MouseCursorController will be forced to target. Pass
- * {@link android.view.Display#INVALID_DISPLAY} to clear the override.
- *
- * Note: This method generally blocks until the pointer display override has propagated.
- * When setting a new override, the caller should ensure that an input device that can control
- * the mouse pointer is connected. If a new override is set when no such input device is
- * connected, the caller may be blocked for an arbitrary period of time.
- *
- * @return true if the pointer displayId was set successfully, or false if it fails.
- *
- * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete.
- */
- public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId);
-
- /**
- * Gets the display id that the MouseCursorController is being forced to target. Returns
- * {@link android.view.Display#INVALID_DISPLAY} if there is no override
- *
- * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete.
- */
- public abstract int getVirtualMousePointerDisplayId();
-
- /**
* Gets the current position of the mouse cursor.
*
* Returns NaN-s as the coordinates if the cursor is not available.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index eb719527b06d..cbd309e1f957 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -282,33 +282,9 @@ public class InputManagerService extends IInputManager.Stub
// WARNING: Do not call other services outside of input while holding this lock.
private final Object mAdditionalDisplayInputPropertiesLock = new Object();
- // Forces the PointerController to target a specific display id.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mOverriddenPointerDisplayId = Display.INVALID_DISPLAY;
-
- // PointerController is the source of truth of the pointer display. This is the value of the
- // latest pointer display id reported by PointerController.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mAcknowledgedPointerDisplayId = Display.INVALID_DISPLAY;
- // This is the latest display id that IMS has requested PointerController to use. If there are
- // no devices that can control the pointer, PointerController may end up disregarding this
- // value.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mRequestedPointerDisplayId = Display.INVALID_DISPLAY;
@GuardedBy("mAdditionalDisplayInputPropertiesLock")
private final SparseArray<AdditionalDisplayInputProperties> mAdditionalDisplayInputProperties =
new SparseArray<>();
- // This contains the per-display properties that are currently applied by native code. It should
- // be kept in sync with the properties for mRequestedPointerDisplayId.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private final AdditionalDisplayInputProperties mCurrentDisplayProperties =
- new AdditionalDisplayInputProperties();
- // TODO(b/293587049): Pointer Icon Refactor: There can be more than one pointer icon
- // visible at once. Update this to support multi-pointer use cases.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private PointerIcon mPointerIcon;
// Holds all the registered gesture monitors that are implemented as spy windows. The spy
// windows are mapped by their InputChannel tokens.
@@ -617,14 +593,9 @@ public class InputManagerService extends IInputManager.Stub
}
mNative.setDisplayViewports(vArray);
- // Attempt to update the pointer display when viewports change when there is no override.
+ // Attempt to update the default pointer display when the viewports change.
// Take care to not make calls to window manager while holding internal locks.
- final int pointerDisplayId = mWindowManagerCallbacks.getPointerDisplayId();
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- if (mOverriddenPointerDisplayId == Display.INVALID_DISPLAY) {
- updatePointerDisplayIdLocked(pointerDisplayId);
- }
- }
+ mNative.setPointerDisplayId(mWindowManagerCallbacks.getPointerDisplayId());
}
/**
@@ -1353,84 +1324,11 @@ public class InputManagerService extends IInputManager.Stub
properties -> properties.pointerIconVisible = visible);
}
- /**
- * Update the display on which the mouse pointer is shown.
- *
- * @return true if the pointer displayId changed, false otherwise.
- */
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private boolean updatePointerDisplayIdLocked(int pointerDisplayId) {
- if (mRequestedPointerDisplayId == pointerDisplayId) {
- return false;
- }
- mRequestedPointerDisplayId = pointerDisplayId;
- mNative.setPointerDisplayId(pointerDisplayId);
- applyAdditionalDisplayInputProperties();
- return true;
- }
-
private void handlePointerDisplayIdChanged(PointerDisplayIdChangedArgs args) {
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mAcknowledgedPointerDisplayId = args.mPointerDisplayId;
- // Notify waiting threads that the display of the mouse pointer has changed.
- mAdditionalDisplayInputPropertiesLock.notifyAll();
- }
mWindowManagerCallbacks.notifyPointerDisplayIdChanged(
args.mPointerDisplayId, args.mXPosition, args.mYPosition);
}
- private boolean setVirtualMousePointerDisplayIdBlocking(int overrideDisplayId) {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- throw new IllegalStateException(
- "This must not be used when PointerChoreographer is enabled");
- }
- final boolean isRemovingOverride = overrideDisplayId == Display.INVALID_DISPLAY;
-
- // Take care to not make calls to window manager while holding internal locks.
- final int resolvedDisplayId = isRemovingOverride
- ? mWindowManagerCallbacks.getPointerDisplayId()
- : overrideDisplayId;
-
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mOverriddenPointerDisplayId = overrideDisplayId;
-
- if (!updatePointerDisplayIdLocked(resolvedDisplayId)
- && mAcknowledgedPointerDisplayId == resolvedDisplayId) {
- // The requested pointer display is already set.
- return true;
- }
- if (isRemovingOverride && mAcknowledgedPointerDisplayId == Display.INVALID_DISPLAY) {
- // The pointer display override is being removed, but the current pointer display
- // is already invalid. This can happen when the PointerController is destroyed as a
- // result of the removal of all input devices that can control the pointer.
- return true;
- }
- try {
- // The pointer display changed, so wait until the change has propagated.
- mAdditionalDisplayInputPropertiesLock.wait(5_000 /*mills*/);
- } catch (InterruptedException ignored) {
- }
- // This request succeeds in two cases:
- // - This request was to remove the override, in which case the new pointer display
- // could be anything that WM has set.
- // - We are setting a new override, in which case the request only succeeds if the
- // reported new displayId is the one we requested. This check ensures that if two
- // competing overrides are requested in succession, the caller can be notified if one
- // of them fails.
- return isRemovingOverride || mAcknowledgedPointerDisplayId == overrideDisplayId;
- }
- }
-
- private int getVirtualMousePointerDisplayId() {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- throw new IllegalStateException(
- "This must not be used when PointerChoreographer is enabled");
- }
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- return mOverriddenPointerDisplayId;
- }
- }
-
private void setDisplayEligibilityForPointerCapture(int displayId, boolean isEligible) {
mNative.setDisplayEligibilityForPointerCapture(displayId, isEligible);
}
@@ -1715,31 +1613,13 @@ public class InputManagerService extends IInputManager.Stub
// Binder call
@Override
public void setPointerIconType(int iconType) {
- if (iconType == PointerIcon.TYPE_CUSTOM) {
- throw new IllegalArgumentException("Use setCustomPointerIcon to set custom pointers");
- }
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mPointerIcon = null;
- mPointerIconType = iconType;
-
- if (!mCurrentDisplayProperties.pointerIconVisible) return;
-
- mNative.setPointerIconType(mPointerIconType);
- }
+ // TODO(b/311416205): Remove.
}
// Binder call
@Override
public void setCustomPointerIcon(PointerIcon icon) {
- Objects.requireNonNull(icon);
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mPointerIconType = PointerIcon.TYPE_CUSTOM;
- mPointerIcon = icon;
-
- if (!mCurrentDisplayProperties.pointerIconVisible) return;
-
- mNative.setCustomPointerIcon(mPointerIcon);
- }
+ // TODO(b/311416205): Remove.
}
// Binder call
@@ -1747,12 +1627,7 @@ public class InputManagerService extends IInputManager.Stub
public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId,
IBinder inputToken) {
Objects.requireNonNull(icon);
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mPointerIconType = icon.getType();
- mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null;
-
- return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
- }
+ return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
}
/**
@@ -2281,28 +2156,24 @@ public class InputManagerService extends IInputManager.Stub
private void dumpDisplayInputPropertiesValues(IndentingPrintWriter pw) {
synchronized (mAdditionalDisplayInputPropertiesLock) {
- if (mAdditionalDisplayInputProperties.size() != 0) {
- pw.println("mAdditionalDisplayInputProperties:");
- pw.increaseIndent();
+ pw.println("mAdditionalDisplayInputProperties:");
+ pw.increaseIndent();
+ try {
+ if (mAdditionalDisplayInputProperties.size() == 0) {
+ pw.println("<none>");
+ return;
+ }
for (int i = 0; i < mAdditionalDisplayInputProperties.size(); i++) {
- pw.println("displayId: "
- + mAdditionalDisplayInputProperties.keyAt(i));
+ pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i));
final AdditionalDisplayInputProperties properties =
mAdditionalDisplayInputProperties.valueAt(i);
pw.println("mousePointerAccelerationEnabled: "
+ properties.mousePointerAccelerationEnabled);
pw.println("pointerIconVisible: " + properties.pointerIconVisible);
}
+ } finally {
pw.decreaseIndent();
}
- if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
- pw.println("mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
- }
-
- pw.println("mAcknowledgedPointerDisplayId=" + mAcknowledgedPointerDisplayId);
- pw.println("mRequestedPointerDisplayId=" + mRequestedPointerDisplayId);
- pw.println("mPointerIconType=" + PointerIcon.typeToString(mPointerIconType));
- pw.println("mPointerIcon=" + mPointerIcon);
}
}
private boolean checkCallingPermission(String permission, String func) {
@@ -3267,17 +3138,6 @@ public class InputManagerService extends IInputManager.Stub
}
@Override
- public boolean setVirtualMousePointerDisplayId(int pointerDisplayId) {
- return InputManagerService.this
- .setVirtualMousePointerDisplayIdBlocking(pointerDisplayId);
- }
-
- @Override
- public int getVirtualMousePointerDisplayId() {
- return InputManagerService.this.getVirtualMousePointerDisplayId();
- }
-
- @Override
public PointF getCursorPosition(int displayId) {
final float[] p = mNative.getMouseCursorPosition(displayId);
if (p == null || p.length != 2) {
@@ -3413,44 +3273,6 @@ public class InputManagerService extends IInputManager.Stub
}
}
- private void applyAdditionalDisplayInputProperties() {
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- AdditionalDisplayInputProperties properties =
- mAdditionalDisplayInputProperties.get(mRequestedPointerDisplayId);
- if (properties == null) properties = DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES;
- applyAdditionalDisplayInputPropertiesLocked(properties);
- }
- }
-
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private void applyAdditionalDisplayInputPropertiesLocked(
- AdditionalDisplayInputProperties properties) {
- // Handle changes to each of the individual properties.
- // TODO(b/293587049): This approach for updating pointer display properties is only for when
- // PointerChoreographer is disabled. Remove this logic when PointerChoreographer is
- // permanently enabled.
-
- if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
- mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
- if (properties.pointerIconVisible) {
- if (mPointerIconType == PointerIcon.TYPE_CUSTOM) {
- Objects.requireNonNull(mPointerIcon);
- mNative.setCustomPointerIcon(mPointerIcon);
- } else {
- mNative.setPointerIconType(mPointerIconType);
- }
- } else {
- mNative.setPointerIconType(PointerIcon.TYPE_NULL);
- }
- }
-
- if (properties.mousePointerAccelerationEnabled
- != mCurrentDisplayProperties.mousePointerAccelerationEnabled) {
- mCurrentDisplayProperties.mousePointerAccelerationEnabled =
- properties.mousePointerAccelerationEnabled;
- }
- }
-
private void updateAdditionalDisplayInputProperties(int displayId,
Consumer<AdditionalDisplayInputProperties> updater) {
synchronized (mAdditionalDisplayInputPropertiesLock) {
@@ -3473,13 +3295,6 @@ public class InputManagerService extends IInputManager.Stub
if (properties.allDefaults()) {
mAdditionalDisplayInputProperties.remove(displayId);
}
- if (displayId != mRequestedPointerDisplayId) {
- Log.i(TAG, "Not applying additional properties for display " + displayId
- + " because the pointer is currently targeting display "
- + mRequestedPointerDisplayId + ".");
- return;
- }
- applyAdditionalDisplayInputPropertiesLocked(properties);
}
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 32d5044d9e41..f742360484f5 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -189,12 +189,8 @@ interface NativeInputManagerService {
void disableInputDevice(int deviceId);
- void setPointerIconType(int iconId);
-
void reloadPointerIcons();
- void setCustomPointerIcon(@NonNull PointerIcon icon);
-
boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId,
@NonNull IBinder inputToken);
@@ -467,15 +463,9 @@ interface NativeInputManagerService {
public native void disableInputDevice(int deviceId);
@Override
- public native void setPointerIconType(int iconId);
-
- @Override
public native void reloadPointerIcons();
@Override
- public native void setCustomPointerIcon(PointerIcon icon);
-
- @Override
public native boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId,
int pointerId, IBinder inputToken);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 1b9d6c598545..0fde760fd02a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2234,7 +2234,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
mInputManagerInternal.notifyInputMethodConnectionActive(connectionIsActive);
}
final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-
+ final var bindingController = userData.mBindingController;
// If configured, we want to avoid starting up the IME if it is not supposed to be showing
if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags,
@@ -2243,7 +2243,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
Slog.d(TAG, "Avoiding IME startup and unbinding current input method.");
}
invalidateAutofillSessionLocked();
- userData.mBindingController.unbindCurrentMethod();
+ bindingController.unbindCurrentMethod();
return InputBindResult.NO_EDITOR;
}
@@ -2275,8 +2275,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
}
}
- userData.mBindingController.unbindCurrentMethod();
- return userData.mBindingController.bindCurrentMethod();
+ bindingController.unbindCurrentMethod();
+ return bindingController.bindCurrentMethod();
}
/**
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 573116105dea..d6d134d892f5 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -78,10 +78,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
+import java.util.PriorityQueue;
+import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -152,6 +156,16 @@ public class ContextHubService extends IContextHubService.Stub {
private final ScheduledThreadPoolExecutor mDailyMetricTimer =
new ScheduledThreadPoolExecutor(1);
+ // A queue of reliable message records for duplicate detection
+ private final PriorityQueue<ReliableMessageRecord> mReliableMessageRecordQueue =
+ new PriorityQueue<ReliableMessageRecord>(
+ (ReliableMessageRecord left, ReliableMessageRecord right) -> {
+ return Long.compare(left.getTimestamp(), right.getTimestamp());
+ });
+
+ // The test mode manager that manages behaviors during test mode.
+ private final TestModeManager mTestModeManager = new TestModeManager();
+
// The period of the recurring time
private static final int PERIOD_METRIC_QUERY_DAYS = 1;
@@ -164,6 +178,9 @@ public class ContextHubService extends IContextHubService.Stub {
private boolean mIsBtScanningEnabled = false;
private boolean mIsBtMainEnabled = false;
+ // True if test mode is enabled for the Context Hub
+ private AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
+
// A hashmap used to record if a contexthub is waiting for daily query
private Set<Integer> mMetricQueryPendingContextHubIds =
Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
@@ -210,8 +227,17 @@ public class ContextHubService extends IContextHubService.Stub {
@Override
public void handleNanoappMessage(short hostEndpointId, NanoAppMessage message,
List<String> nanoappPermissions, List<String> messagePermissions) {
- handleClientMessageCallback(mContextHubId, hostEndpointId, message, nanoappPermissions,
- messagePermissions);
+ if (Flags.reliableMessageImplementation()
+ && Flags.reliableMessageTestModeBehavior()
+ && mIsTestModeEnabled.get()
+ && mTestModeManager.handleNanoappMessage(mContextHubId, hostEndpointId,
+ message, nanoappPermissions, messagePermissions)) {
+ // The TestModeManager handled the nanoapp message, so return here.
+ return;
+ }
+
+ handleClientMessageCallback(mContextHubId, hostEndpointId, message,
+ nanoappPermissions, messagePermissions);
}
@Override
@@ -228,6 +254,106 @@ public class ContextHubService extends IContextHubService.Stub {
}
}
+ /**
+ * Records a reliable message from a nanoapp for duplicate detection.
+ */
+ private static class ReliableMessageRecord {
+ public static final int TIMEOUT_NS = 1000000000;
+
+ public int mContextHubId;
+ public long mTimestamp;
+ public int mMessageSequenceNumber;
+ byte mErrorCode;
+
+ ReliableMessageRecord(int contextHubId, long timestamp,
+ int messageSequenceNumber, byte errorCode) {
+ mContextHubId = contextHubId;
+ mTimestamp = timestamp;
+ mMessageSequenceNumber = messageSequenceNumber;
+ mErrorCode = errorCode;
+ }
+
+ public int getContextHubId() {
+ return mContextHubId;
+ }
+
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ public int getMessageSequenceNumber() {
+ return mMessageSequenceNumber;
+ }
+
+ public byte getErrorCode() {
+ return mErrorCode;
+ }
+
+ public void setErrorCode(byte errorCode) {
+ mErrorCode = errorCode;
+ }
+
+ public boolean isExpired() {
+ return mTimestamp + TIMEOUT_NS < SystemClock.elapsedRealtimeNanos();
+ }
+ }
+
+ /**
+ * A class to manage behaviors during test mode. This is used for testing.
+ */
+ private class TestModeManager {
+ /**
+ * Probability (in percent) of duplicating a message.
+ */
+ private static final int MESSAGE_DUPLICATION_PROBABILITY_PERCENT = 50;
+
+ /**
+ * The number of total messages to send when the duplicate event happens.
+ */
+ private static final int NUM_MESSAGES_TO_DUPLICATE = 3;
+
+ /**
+ * A probability percent for a certain event.
+ */
+ private static final int MAX_PROBABILITY_PERCENT = 100;
+
+ /**
+ * Random number generator.
+ */
+ private Random mRandom = new Random();
+
+ /**
+ * @see ContextHubServiceCallback.handleNanoappMessage
+ * @return whether the message was handled
+ */
+ public boolean handleNanoappMessage(int contextHubId,
+ short hostEndpointId, NanoAppMessage message,
+ List<String> nanoappPermissions, List<String> messagePermissions) {
+ if (!message.isReliable()) {
+ return false;
+ }
+
+ if (didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) {
+ for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) {
+ handleClientMessageCallback(contextHubId, hostEndpointId,
+ message, nanoappPermissions, messagePermissions);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the event with percentPercent did happen.
+ *
+ * @param probabilityPercent the percent probability of the event.
+ * @return true if the event happened, false otherwise.
+ */
+ private boolean didEventHappen(int probabilityPercent) {
+ return mRandom.nextInt(MAX_PROBABILITY_PERCENT) < probabilityPercent;
+ }
+ }
+
public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
Log.i(TAG, "Starting Context Hub Service init");
mContext = context;
@@ -563,6 +689,8 @@ public class ContextHubService extends IContextHubService.Stub {
* Resets the settings. Called when a context hub restarts or the AIDL HAL dies
*/
private void resetSettings() {
+ mIsTestModeEnabled.set(false);
+
sendLocationSettingUpdate();
sendWifiSettingUpdate(/* forceUpdate= */ true);
sendAirplaneModeSettingUpdate();
@@ -854,14 +982,76 @@ public class ContextHubService extends IContextHubService.Stub {
private void handleClientMessageCallback(int contextHubId, short hostEndpointId,
NanoAppMessage message, List<String> nanoappPermissions,
List<String> messagePermissions) {
- byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId, message,
- nanoappPermissions, messagePermissions);
- if (message.isReliable() && errorCode != ErrorCode.OK) {
- sendMessageDeliveryStatusToContextHub(contextHubId, message.getMessageSequenceNumber(),
- errorCode);
+ if (!Flags.reliableMessageImplementation()
+ || !Flags.reliableMessageDuplicateDetectionService()) {
+ byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId,
+ message, nanoappPermissions, messagePermissions);
+ if (message.isReliable() && errorCode != ErrorCode.OK) {
+ sendMessageDeliveryStatusToContextHub(contextHubId,
+ message.getMessageSequenceNumber(), errorCode);
+ }
+ return;
+ }
+
+ if (message.isReliable()) {
+ byte errorCode = ErrorCode.OK;
+ synchronized (mReliableMessageRecordQueue) {
+ Optional<ReliableMessageRecord> record = Optional.empty();
+ for (ReliableMessageRecord r: mReliableMessageRecordQueue) {
+ if (r.getContextHubId() == contextHubId
+ && r.getMessageSequenceNumber() == message.getMessageSequenceNumber()) {
+ record = Optional.of(r);
+ break;
+ }
+ }
+
+ if (record.isPresent()) {
+ errorCode = record.get().getErrorCode();
+ if (errorCode == ErrorCode.TRANSIENT_ERROR) {
+ Log.w(TAG, "Found duplicate reliable message with message sequence number: "
+ + record.get().getMessageSequenceNumber() + ": retrying");
+ errorCode = mClientManager.onMessageFromNanoApp(
+ contextHubId, hostEndpointId, message,
+ nanoappPermissions, messagePermissions);
+ record.get().setErrorCode(errorCode);
+ } else {
+ Log.w(TAG, "Found duplicate reliable message with message sequence number: "
+ + record.get().getMessageSequenceNumber());
+ }
+ } else {
+ errorCode = mClientManager.onMessageFromNanoApp(
+ contextHubId, hostEndpointId, message,
+ nanoappPermissions, messagePermissions);
+ mReliableMessageRecordQueue.add(
+ new ReliableMessageRecord(contextHubId,
+ SystemClock.elapsedRealtimeNanos(),
+ message.getMessageSequenceNumber(),
+ errorCode));
+ }
+ }
+ sendMessageDeliveryStatusToContextHub(contextHubId,
+ message.getMessageSequenceNumber(), errorCode);
+ } else {
+ mClientManager.onMessageFromNanoApp(
+ contextHubId, hostEndpointId, message,
+ nanoappPermissions, messagePermissions);
+ }
+
+ synchronized (mReliableMessageRecordQueue) {
+ while (mReliableMessageRecordQueue.peek() != null
+ && mReliableMessageRecordQueue.peek().isExpired()) {
+ mReliableMessageRecordQueue.poll();
+ }
}
}
+ /**
+ * Sends the message delivery status to the Context Hub.
+ *
+ * @param contextHubId the ID of the hub
+ * @param messageSequenceNumber the message sequence number
+ * @param errorCode the error code, one of the enum ErrorCode
+ */
private void sendMessageDeliveryStatusToContextHub(int contextHubId,
int messageSequenceNumber, byte errorCode) {
if (!Flags.reliableMessageImplementation()) {
@@ -1229,6 +1419,9 @@ public class ContextHubService extends IContextHubService.Stub {
public boolean setTestMode(boolean enable) {
super.setTestMode_enforcePermission();
boolean status = mContextHubWrapper.setTestMode(enable);
+ if (status) {
+ mIsTestModeEnabled.set(enable);
+ }
// Query nanoapps to update service state after test mode state change.
for (int contextHubId: mDefaultClientMap.keySet()) {
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 869b89a6670c..73647dbbe978 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1305,22 +1305,20 @@ class MediaRouter2ServiceImpl {
route.getId(),
requestId));
+ UserHandler userHandler = routerRecord.mUserRecord.mHandler;
if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) {
- ManagerRecord manager = routerRecord.mUserRecord.mHandler.findManagerWithId(
- toRequesterId(managerRequestId));
+ ManagerRecord manager = userHandler.findManagerWithId(toRequesterId(managerRequestId));
if (manager == null || manager.mLastSessionCreationRequest == null) {
Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
+ "Ignoring unknown request.");
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(),
oldSession.getId())) {
Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
+ "Ignoring unmatched routing session.");
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
if (!TextUtils.equals(manager.mLastSessionCreationRequest.mRoute.getId(),
@@ -1333,29 +1331,28 @@ class MediaRouter2ServiceImpl {
} else {
Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
+ "Ignoring unmatched route.");
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
}
manager.mLastSessionCreationRequest = null;
} else {
+ String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId();
if (route.isSystemRoute()
&& !routerRecord.hasSystemRoutingPermission()
- && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+ && !TextUtils.equals(route.getId(), defaultRouteId)) {
Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to"
+ route);
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
}
long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
- routerRecord.mUserRecord.mHandler.sendMessage(
+ userHandler.sendMessage(
obtainMessage(
UserHandler::requestCreateSessionWithRouter2OnHandler,
- routerRecord.mUserRecord.mHandler,
+ userHandler,
uniqueRequestId,
managerRequestId,
transferInitiatorUserHandle,
@@ -1429,18 +1426,22 @@ class MediaRouter2ServiceImpl {
"transferToRouteWithRouter2 | router: %s(id: %d), route: %s",
routerRecord.mPackageName, routerRecord.mRouterId, route.getId()));
+ UserHandler userHandler = routerRecord.mUserRecord.mHandler;
+ String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId();
if (route.isSystemRoute()
&& !routerRecord.hasSystemRoutingPermission()
- && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::notifySessionCreationFailedToRouter,
- routerRecord.mUserRecord.mHandler,
- routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID)));
+ && !TextUtils.equals(route.getId(), defaultRouteId)) {
+ userHandler.sendMessage(
+ obtainMessage(
+ UserHandler::notifySessionCreationFailedToRouter,
+ userHandler,
+ routerRecord,
+ toOriginalRequestId(DUMMY_REQUEST_ID)));
} else {
- routerRecord.mUserRecord.mHandler.sendMessage(
+ userHandler.sendMessage(
obtainMessage(
UserHandler::transferToRouteOnHandler,
- routerRecord.mUserRecord.mHandler,
+ userHandler,
DUMMY_REQUEST_ID,
transferInitiatorUserHandle,
routerRecord.mPackageName,
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index c105b9c37026..6ce3ab4b2d65 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -232,10 +232,16 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
String sessionId,
String routeId,
@RoutingSessionInfo.TransferReason int transferReason) {
+ String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId();
if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
- // The currently selected route is the default route.
- Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
- return;
+ if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+ // Transfer to the default route (which is the selected route). We replace the id to
+ // be the selected route id so that the transfer reason gets updated.
+ routeId = selectedDeviceRouteId;
+ } else {
+ Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
+ return;
+ }
}
if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
@@ -250,11 +256,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
}
}
- MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
+ String finalRouteId = routeId; // Make a final copy to use it in the lambda.
boolean isAvailableDeviceRoute =
mDeviceRouteController.getAvailableRoutes().stream()
- .anyMatch(it -> it.getId().equals(routeId));
- boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRoute.getId());
+ .anyMatch(it -> it.getId().equals(finalRouteId));
+ boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId);
if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
// The requested route is managed by the device route controller. Note that the selected
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 143bc5cb20ff..b589f4972155 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -486,6 +486,7 @@ public class ZenModeHelper {
}
}
+ @GuardedBy("mConfigLock")
private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd,
AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) {
if (!Flags.modesApi()) {
@@ -1112,6 +1113,7 @@ public class ZenModeHelper {
* <p>The rule's {@link ZenRule#condition} is cleared (meaning that an active rule will be
* deactivated) unless the update has origin == {@link ZenModeConfig#UPDATE_ORIGIN_USER}.
*/
+ @GuardedBy("mConfigLock")
private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenRule rule,
@ConfigChangeOrigin int origin, boolean isNew) {
if (Flags.modesApi()) {
@@ -1261,12 +1263,14 @@ public class ZenModeHelper {
*
* <p>Returns {@code true} if the policy of the rule was modified.
*/
+ @GuardedBy("mConfigLock")
private boolean updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
boolean updateBitmask, boolean isNew) {
if (newPolicy == null) {
if (isNew) {
// Newly created rule with no provided policy; fill in with the default.
- zenRule.zenPolicy = mDefaultConfig.toZenPolicy();
+ zenRule.zenPolicy =
+ Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy();
return true;
}
// Otherwise, a null policy means no policy changes, so we can stop here.
@@ -1275,8 +1279,9 @@ public class ZenModeHelper {
// If oldPolicy is null, we compare against the default policy when determining which
// fields in the bitmask should be marked as updated.
- ZenPolicy oldPolicy =
- zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy();
+ ZenPolicy oldPolicy = zenRule.zenPolicy != null
+ ? zenRule.zenPolicy
+ : (Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy());
// If this is updating a rule rather than creating a new one, keep any fields from the
// old policy if they are unspecified in the new policy. For newly created rules, oldPolicy
@@ -2033,7 +2038,8 @@ public class ZenModeHelper {
// rule's policy fields should be set upon creation, this is a fallback to
// catch any that may have fallen through the cracks.
Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
- policy.apply(mDefaultConfig.toZenPolicy());
+ policy.apply(
+ Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy());
}
} else {
// active rule with no specified policy inherits the global config settings
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index c60f0afcc2ff..209cbb7f591e 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -46,6 +46,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApexStagedEvent;
+import android.content.pm.Flags;
import android.content.pm.IPackageManagerNative;
import android.content.pm.IStagedApexObserver;
import android.content.pm.PackageManager;
@@ -766,6 +767,10 @@ public final class DexOptHelper {
final PackageSetting ps = installRequest.getScannedPackageSetting();
final AndroidPackage pkg = ps.getPkg();
final boolean onIncremental = isIncrementalPath(ps.getPathString());
+ final boolean performDexOptForRollback = Flags.recoverabilityDetection()
+ ? !(installRequest.isRollback()
+ && installRequest.getInstallSource().mInitiatingPackageName.equals("android"))
+ : true;
return (!instantApp || Global.getInt(context.getContentResolver(),
Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
@@ -773,7 +778,8 @@ public final class DexOptHelper {
&& !pkg.isDebuggable()
&& (!onIncremental)
&& dexoptOptions.isCompilationEnabled()
- && !isApex;
+ && !isApex
+ && performDexOptForRollback;
}
private static class StagedApexObserver extends IStagedApexObserver.Stub {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 179379487aba..7a36f6dabe06 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -35,6 +35,8 @@ import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.role.RoleManager;
+import android.app.usage.StorageStats;
+import android.app.usage.StorageStatsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.IIntentReceiver;
@@ -136,6 +138,7 @@ import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.security.SecureRandom;
+import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@@ -275,6 +278,8 @@ class PackageManagerShellCommand extends ShellCommand {
return runClear();
case "get-archived-package-metadata":
return runGetArchivedPackageMetadata();
+ case "get-package-storage-stats":
+ return runGetPackageStorageStats();
case "install-archived":
return runArchivedInstall();
case "enable":
@@ -1861,6 +1866,103 @@ class PackageManagerShellCommand extends ShellCommand {
return 0;
}
+ /**
+ * Returns a string that shows the number of bytes in b, Kb, Mb or Gb.
+ */
+ protected static String getFormattedBytes(long size) {
+ double k = size/1024.0;
+ double m = size/1048576.0;
+ double g = size/1073741824.0;
+
+ DecimalFormat dec = new DecimalFormat("0.00");
+ if (g > 1) {
+ return dec.format(g).concat(" Gb");
+ } else if (m > 1) {
+ return dec.format(m).concat(" Mb");
+ } else if (k > 1) {
+ return dec.format(k).concat(" Kb");
+ }
+ return "";
+ }
+
+ /**
+ * Return the string that displays the data size.
+ */
+ private String getDataSizeDisplay(long size) {
+ String formattedOutput = getFormattedBytes(size);
+ if (!formattedOutput.isEmpty()) {
+ formattedOutput = " (" + formattedOutput + ")";
+ }
+ return Long.toString(size) + " bytes" + formattedOutput;
+ }
+
+ /**
+ * Display storage stats of the specified package.
+ *
+ * Usage: get-package-storage-stats [--usr USER_ID] PACKAGE
+ */
+ private int runGetPackageStorageStats() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ if (!android.content.pm.Flags.getPackageStorageStats()) {
+ pw.println("Error: get_package_storage_stats flag is not enabled");
+ return 1;
+ }
+ if (!android.app.usage.Flags.getAppBytesByDataTypeApi()) {
+ pw.println("Error: get_app_bytes_by_data_type_api flag is not enabled");
+ return 1;
+ }
+ int userId = UserHandle.USER_CURRENT;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+ default:
+ pw.println("Error: Unknown option: " + opt);
+ return 1;
+ }
+ }
+
+ final String packageName = getNextArg();
+ if (packageName == null) {
+ pw.println("Error: package name not specified");
+ return 1;
+ }
+ try {
+ StorageStatsManager storageStatsManager =
+ mContext.getSystemService(StorageStatsManager.class);
+ final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL,
+ "runGetPackageStorageStats");
+ StorageStats stats =
+ storageStatsManager.queryStatsForPackage(StorageManager.UUID_DEFAULT,
+ packageName, UserHandle.of(translatedUserId));
+
+ pw.println("code: " + getDataSizeDisplay(stats.getAppBytes()));
+ pw.println("data: " + getDataSizeDisplay(stats.getDataBytes()));
+ pw.println("cache: " + getDataSizeDisplay(stats.getCacheBytes()));
+ pw.println("apk: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_APK)));
+ pw.println("lib: " + getDataSizeDisplay(
+ stats.getAppBytesByDataType(StorageStats.APP_DATA_TYPE_LIB)));
+ pw.println("dm: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_DM)));
+ pw.println("dexopt artifacts: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT)));
+ pw.println("current profile : " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE)));
+ pw.println("reference profile: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE)));
+ pw.println("external cache: " + getDataSizeDisplay(stats.getExternalCacheBytes()));
+ } catch (Exception e) {
+ getErrPrintWriter().println("Failed to get storage stats, reason: " + e);
+ pw.println("Failure [failed to get storage stats], reason: " + e);
+ return -1;
+ }
+ return 0;
+ }
+
private int runInstallExisting() throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
int userId = UserHandle.USER_CURRENT;
@@ -4869,6 +4971,8 @@ class PackageManagerShellCommand extends ShellCommand {
pw.println(" Displays the component name of the domain verification agent on device.");
pw.println(" If the component isn't enabled, an error message will be displayed.");
pw.println(" --user: return the agent of the given user (SYSTEM_USER if unspecified)");
+ pw.println(" get-package-storage-stats [--user <USER_ID>] <PACKAGE>");
+ pw.println(" Return the storage stats for the given app, if present");
pw.println("");
printArtServiceHelp();
pw.println("");
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index b1976cd0d13b..f6487ceea43b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1371,7 +1371,7 @@ public class UserManagerService extends IUserManager.Stub {
for (int i = 0; i < userSize; i++) {
UserInfo ui = mUsers.valueAt(i).info;
if ((excludePartial && ui.partial)
- || (excludeDying && mRemovingUserIds.get(ui.id))
+ || (excludeDying && isDyingLU(ui))
|| (excludePreCreated && ui.preCreated)) {
continue;
}
@@ -1381,6 +1381,17 @@ public class UserManagerService extends IUserManager.Stub {
}
}
+ @GuardedBy("mUsersLock")
+ private boolean isDyingLU(UserInfo ui) {
+ if (mRemovingUserIds.get(ui.id)) {
+ return true;
+ }
+ if (ui.isEphemeral() && ui.isInitialized() && ui.id != getCurrentUserId()) {
+ return true;
+ }
+ return false;
+ }
+
@Override
public List<UserInfo> getProfiles(@UserIdInt int userId, boolean enabledOnly) {
boolean returnFullInfo;
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 1a9e012a7c53..f7eb29fe3ee9 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -141,6 +141,8 @@ final class VerifyingSession {
@NonNull
private final PackageManagerService mPm;
+ private final int mInstallReason;
+
VerifyingSession(UserHandle user, File stagedDir, IPackageInstallObserver2 observer,
PackageInstaller.SessionParams sessionParams, InstallSource installSource,
int installerUid, SigningDetails signingDetails, int sessionId, PackageLite lite,
@@ -168,6 +170,7 @@ final class VerifyingSession {
mUserActionRequiredType = sessionParams.requireUserAction;
mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING;
mIsStaged = sessionParams.isStaged;
+ mInstallReason = sessionParams.installReason;
}
@Override
@@ -190,7 +193,9 @@ final class VerifyingSession {
// Perform package verification and enable rollback (unless we are simply moving the
// package).
if (!mOriginInfo.mExisting) {
- if (!isApex() && !isArchivedInstallation()) {
+ final boolean verifyForRollback = Flags.recoverabilityDetection()
+ ? !isARollback() : true;
+ if (!isApex() && !isArchivedInstallation() && verifyForRollback) {
// TODO(b/182426975): treat APEX as APK when APK verification is concerned
sendApkVerificationRequest(pkgLite);
}
@@ -200,6 +205,11 @@ final class VerifyingSession {
}
}
+ private boolean isARollback() {
+ return mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK
+ && mInstallSource.mInitiatingPackageName.equals("android");
+ }
+
private void sendApkVerificationRequest(PackageInfoLite pkgLite) {
final int verificationId = mPm.mPendingVerificationToken++;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5e95a4b79d00..202e94c6ec84 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -7401,6 +7401,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
}
static boolean isPip2ExperimentEnabled() {
- return Flags.enablePip2Implementation();
+ return Flags.enablePip2Implementation() || SystemProperties.getBoolean(
+ "wm_shell.pip2", false);
}
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 0e446b8eaf8c..eb1f3b402364 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -39,6 +39,7 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONL
import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel;
import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg;
+import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck;
import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
import static com.android.window.flags.Flags.balRequireOptInSameUid;
@@ -805,14 +806,25 @@ public class BackgroundActivityStartController {
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
- int callingUid = state.mCallingUid;
- int callingPid = state.mCallingPid;
- final String callingPackage = state.mCallingPackage;
- WindowProcessController callerApp = state.mCallerApp;
+ // This is used to block background activity launch even if the app is still
+ // visible to user after user clicking home button.
+
+ // Normal apps with visible app window will be allowed to start activity if app switching
+ // is allowed, or apps like live wallpaper with non app visible window will be allowed.
+ final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
+ || state.mAppSwitchState == APP_SWITCH_FG_ONLY;
+ if (appSwitchAllowedOrFg && state.mCallingUidHasAnyVisibleWindow) {
+ return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+ /*background*/ false, "callingUid has visible window");
+ }
+ if (mService.mActiveUids.hasNonAppVisibleWindow(state.mCallingUid)) {
+ return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
+ /*background*/ false, "callingUid has non-app visible window");
+ }
// don't abort for the most important UIDs
- final int callingAppId = UserHandle.getAppId(callingUid);
- if (callingUid == Process.ROOT_UID
+ final int callingAppId = UserHandle.getAppId(state.mCallingUid);
+ if (state.mCallingUid == Process.ROOT_UID
|| callingAppId == Process.SYSTEM_UID
|| callingAppId == Process.NFC_UID) {
return new BalVerdict(
@@ -821,7 +833,7 @@ public class BackgroundActivityStartController {
}
// Always allow home application to start activities.
- if (isHomeApp(callingUid, callingPackage)) {
+ if (isHomeApp(state.mCallingUid, state.mCallingPackage)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
"Home app");
@@ -836,67 +848,46 @@ public class BackgroundActivityStartController {
"Active ime");
}
- // This is used to block background activity launch even if the app is still
- // visible to user after user clicking home button.
- final int appSwitchState = mService.getBalAppSwitchesState();
-
- // don't abort if the callingUid has a visible window or is a persistent system process
- final int callingUidProcState = mService.mActiveUids.getUidState(callingUid);
- final boolean callingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid);
- final boolean isCallingUidPersistentSystemProcess =
- callingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
-
- // Normal apps with visible app window will be allowed to start activity if app switching
- // is allowed, or apps like live wallpaper with non app visible window will be allowed.
- final boolean appSwitchAllowedOrFg =
- appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY;
- if (appSwitchAllowedOrFg && callingUidHasAnyVisibleWindow) {
- return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
- /*background*/ false, "callingUid has visible window");
- }
- if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) {
- return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
- /*background*/ false, "callingUid has non-app visible window");
- }
-
- if (isCallingUidPersistentSystemProcess) {
+ // don't abort if the callingUid is a persistent system process
+ if (state.mIsCallingUidPersistentSystemProcess) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false, "callingUid is persistent system process");
}
// don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
- if (hasBalPermission(callingUid, callingPid)) {
+ if (hasBalPermission(state.mCallingUid, state.mCallingPid)) {
return new BalVerdict(BAL_ALLOW_PERMISSION,
/*background*/ true,
"START_ACTIVITIES_FROM_BACKGROUND permission granted");
}
// don't abort if the caller has the same uid as the recents component
- if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
+ if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Recents Component");
}
// don't abort if the callingUid is the device owner
- if (mService.isDeviceOwner(callingUid)) {
+ if (mService.isDeviceOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Device Owner");
}
// don't abort if the callingUid is a affiliated profile owner
- if (mService.isAffiliatedProfileOwner(callingUid)) {
+ if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Affiliated Profile Owner");
}
// don't abort if the callingUid has companion device
- final int callingUserId = UserHandle.getUserId(callingUid);
- if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
+ final int callingUserId = UserHandle.getUserId(state.mCallingUid);
+ if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Companion App");
}
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
- if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
+ if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+ state.mCallingPackage)) {
Slog.w(
TAG,
"Background activity start for "
- + callingPackage
+ + state.mCallingPackage
+ " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -905,7 +896,7 @@ public class BackgroundActivityStartController {
// OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
- callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) {
+ state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
"OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
}
@@ -914,7 +905,7 @@ public class BackgroundActivityStartController {
// That's the case for PendingIntent-based starts, since the creator's process might not be
// up and alive.
// Don't abort if the callerApp or other processes of that uid are allowed in any way.
- BalVerdict callerAppAllowsBal = checkProcessAllowsBal(callerApp, state);
+ BalVerdict callerAppAllowsBal = checkProcessAllowsBal(state.mCallerApp, state);
if (callerAppAllowsBal.allows()) {
return callerAppAllowsBal;
}
@@ -929,13 +920,6 @@ public class BackgroundActivityStartController {
*/
BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) {
- if (state.isPendingIntentBalAllowedByPermission()
- && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
- return new BalVerdict(BAL_ALLOW_PERMISSION,
- /*background*/ false,
- "realCallingUid has BAL permission.");
- }
-
// Normal apps with visible app window will be allowed to start activity if app switching
// is allowed, or apps like live wallpaper with non app visible window will be allowed.
// The home app can start apps even if app switches are usually disallowed.
@@ -961,6 +945,13 @@ public class BackgroundActivityStartController {
}
}
+ if (state.isPendingIntentBalAllowedByPermission()
+ && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
+ return new BalVerdict(BAL_ALLOW_PERMISSION,
+ /*background*/ false,
+ "realCallingUid has BAL permission.");
+ }
+
// if the realCallingUid is a persistent system process, abort if the IntentSender
// wasn't allowed to start an activity
if (state.mForcedBalByPiSender.allowsBackgroundActivityStarts()
@@ -1660,26 +1651,61 @@ public class BackgroundActivityStartController {
(state.mOriginatingPendingIntent != null));
}
- @BalCode int code = finalVerdict.getCode();
- int callingUid = state.mCallingUid;
- int realCallingUid = state.mRealCallingUid;
- Intent intent = state.mIntent;
-
- if (code == BAL_ALLOW_PENDING_INTENT
- && (callingUid < Process.FIRST_APPLICATION_UID
- || realCallingUid < Process.FIRST_APPLICATION_UID)) {
- String activityName = intent != null
- ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
- writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
- state);
+ if (balImprovedMetrics()) {
+ if (shouldLogStats(finalVerdict, state)) {
+ String activityName;
+ if (shouldLogIntentActivity(finalVerdict, state)) {
+ Intent intent = state.mIntent;
+ activityName = intent == null ? "noIntent" // should never happen
+ : requireNonNull(intent.getComponent()).flattenToShortString();
+ } else {
+ activityName = "";
+ }
+ writeBalAllowedLog(activityName, finalVerdict.getCode(), state);
+ }
+ } else {
+ @BalCode int code = finalVerdict.getCode();
+ int callingUid = state.mCallingUid;
+ int realCallingUid = state.mRealCallingUid;
+ Intent intent = state.mIntent;
+
+ if (code == BAL_ALLOW_PENDING_INTENT
+ && (callingUid < Process.FIRST_APPLICATION_UID
+ || realCallingUid < Process.FIRST_APPLICATION_UID)) {
+ String activityName = intent != null
+ ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
+ writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
+ state);
+ }
+ if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
+ || code == BAL_ALLOW_SAW_PERMISSION) {
+ // We don't need to know which activity in this case.
+ writeBalAllowedLog("", code, state);
+ }
}
- if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
- || code == BAL_ALLOW_SAW_PERMISSION) {
- // We don't need to know which activity in this case.
- writeBalAllowedLog("", code, state);
+ return finalVerdict;
+ }
+ @VisibleForTesting
+ boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
+ if (finalVerdict.blocks()) {
+ return false;
}
- return finalVerdict;
+ if (!state.isPendingIntent() && finalVerdict.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) {
+ return false;
+ }
+ if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()
+ && state.mResultForRealCaller.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
+ return finalVerdict.mBasedOnRealCaller
+ ? state.mRealCallingUid < Process.FIRST_APPLICATION_UID
+ : state.mCallingUid < Process.FIRST_APPLICATION_UID;
}
@VisibleForTesting void writeBalAllowedLog(String activityName, int code, BalState state) {
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 5aa0ed7ce76c..ce1a72deb523 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -25,7 +25,6 @@ import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Color;
import android.provider.DeviceConfig;
-import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +32,7 @@ import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.Function;
+import java.util.function.IntSupplier;
/** Reads letterbox configs from resources and controls their overrides at runtime. */
final class LetterboxConfiguration {
@@ -265,6 +265,12 @@ final class LetterboxConfiguration {
// unresizable apps
private boolean mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
+ // Supplier for the value in pixel to consider when detecting vertical thin letterboxing
+ private final IntSupplier mThinLetterboxWidthFn;
+
+ // Supplier for the value in pixel to consider when detecting horizontal thin letterboxing
+ private final IntSupplier mThinLetterboxHeightFn;
+
// Allows to enable letterboxing strategy for translucent activities ignoring flags.
private boolean mTranslucentLetterboxingOverrideEnabled;
@@ -358,6 +364,10 @@ final class LetterboxConfiguration {
R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled);
mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
+ mThinLetterboxWidthFn = () -> mContext.getResources().getDimensionPixelSize(
+ R.dimen.config_letterboxThinLetterboxWidthDp);
+ mThinLetterboxHeightFn = () -> mContext.getResources().getDimensionPixelSize(
+ R.dimen.config_letterboxThinLetterboxHeightDp);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
@@ -1129,6 +1139,24 @@ final class LetterboxConfiguration {
}
/**
+ * @return Width in pixel about the padding to use to understand if the letterbox for an
+ * activity is thin. If the available space has width W and the app has width w, this
+ * is the maximum value for (W - w) / 2 to be considered for a thin letterboxed app.
+ */
+ int getThinLetterboxWidthPx() {
+ return mThinLetterboxWidthFn.getAsInt();
+ }
+
+ /**
+ * @return Height in pixel about the padding to use to understand if a letterbox is thin.
+ * If the available space has height H and the app has height h, this is the maximum
+ * value for (H - h) / 2 to be considered for a thin letterboxed app.
+ */
+ int getThinLetterboxHeightPx() {
+ return mThinLetterboxHeightFn.getAsInt();
+ }
+
+ /**
* Overrides whether using split screen aspect ratio as a default aspect ratio for unresizable
* apps.
*/
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index b38e666ea691..9e16b8abe0de 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -1024,6 +1024,67 @@ final class LetterboxUiController {
return getSplitScreenAspectRatio();
}
+ /**
+ * @return {@value true} if the resulting app is letterboxed in a way defined as thin.
+ */
+ boolean isVerticalThinLetterboxed() {
+ final int thinHeight = mLetterboxConfiguration.getThinLetterboxHeightPx();
+ if (thinHeight < 0) {
+ return false;
+ }
+ final Task task = mActivityRecord.getTask();
+ if (task == null) {
+ return false;
+ }
+ final int padding = Math.abs(
+ task.getBounds().height() - mActivityRecord.getBounds().height()) / 2;
+ return padding <= thinHeight;
+ }
+
+ /**
+ * @return {@value true} if the resulting app is pillarboxed in a way defined as thin.
+ */
+ boolean isHorizontalThinLetterboxed() {
+ final int thinWidth = mLetterboxConfiguration.getThinLetterboxWidthPx();
+ if (thinWidth < 0) {
+ return false;
+ }
+ final Task task = mActivityRecord.getTask();
+ if (task == null) {
+ return false;
+ }
+ final int padding = Math.abs(
+ task.getBounds().width() - mActivityRecord.getBounds().width()) / 2;
+ return padding <= thinWidth;
+ }
+
+
+ /**
+ * @return {@value true} if the vertical reachability should be allowed in case of
+ * thin letteboxing
+ */
+ boolean allowVerticalReachabilityForThinLetterbox() {
+ if (!Flags.disableThinLetterboxingReachability()) {
+ return true;
+ }
+ // When the flag is enabled we allow vertical reachability only if the
+ // app is not thin letterboxed vertically.
+ return !isVerticalThinLetterboxed();
+ }
+
+ /**
+ * @return {@value true} if the vertical reachability should be enabled in case of
+ * thin letteboxing
+ */
+ boolean allowHorizontalReachabilityForThinLetterbox() {
+ if (!Flags.disableThinLetterboxingReachability()) {
+ return true;
+ }
+ // When the flag is enabled we allow horizontal reachability only if the
+ // app is not thin pillarboxed.
+ return !isHorizontalThinLetterboxed();
+ }
+
float getSplitScreenAspectRatio() {
// Getting the same aspect ratio that apps get in split screen.
final DisplayArea displayArea = mActivityRecord.getDisplayArea();
@@ -1263,6 +1324,9 @@ final class LetterboxUiController {
* </ul>
*/
private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
+ if (!allowHorizontalReachabilityForThinLetterbox()) {
+ return false;
+ }
// Use screen resolved bounds which uses resolved bounds or size compat bounds
// as activity bounds can sometimes be empty
final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
@@ -1298,6 +1362,9 @@ final class LetterboxUiController {
* </ul>
*/
private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
+ if (!allowVerticalReachabilityForThinLetterbox()) {
+ return false;
+ }
// Use screen resolved bounds which uses resolved bounds or size compat bounds
// as activity bounds can sometimes be empty
final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
@@ -1566,6 +1633,8 @@ final class LetterboxUiController {
if (!shouldShowLetterboxUi) {
return;
}
+ pw.println(prefix + " isVerticalThinLetterboxed=" + isVerticalThinLetterboxed());
+ pw.println(prefix + " isHorizontalThinLetterboxed=" + isHorizontalThinLetterboxed());
pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString(
getLetterboxBackgroundColor().toArgb()));
pw.println(prefix + " letterboxBackgroundType="
diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS
index ce47f5cc8a7e..60454fc5d7c6 100644
--- a/services/core/java/com/android/server/wm/OWNERS
+++ b/services/core/java/com/android/server/wm/OWNERS
@@ -18,6 +18,7 @@ rgl@google.com
yunfanc@google.com
wilsonshih@google.com
jiamingliu@google.com
+pdwilliams@google.com
# Files related to background activity launches
per-file Background*Start* = set noparent
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a9c47b89b8ff..8bd7b5f78cf4 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -20,7 +20,6 @@ import static android.app.ActivityManager.isStartResultSuccessful;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
-import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
@@ -171,7 +170,6 @@ import android.util.proto.ProtoOutputStream;
import android.view.DisplayInfo;
import android.view.InsetsState;
import android.view.RemoteAnimationAdapter;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.view.WindowManager.TransitionOldType;
@@ -362,6 +360,10 @@ class Task extends TaskFragment {
* user wants to return to it. */
private WindowProcessController mRootProcess;
+ /** The TF host info are set once the task has ever added an organized task fragment. */
+ int mTaskFragmentHostUid;
+ String mTaskFragmentHostProcessName;
+
/** Takes on same value as first root activity */
boolean isPersistable = false;
int maxRecents;
@@ -438,16 +440,6 @@ class Task extends TaskFragment {
// Id of the previous display the root task was on.
int mPrevDisplayId = INVALID_DISPLAY;
- /** ID of the display which rotation {@link #mRotation} has. */
- private int mLastRotationDisplayId = INVALID_DISPLAY;
-
- /**
- * Display rotation as of the last time {@link #setBounds(Rect)} was called or this task was
- * moved to a new display.
- */
- @Surface.Rotation
- private int mRotation;
-
int mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
/**
@@ -458,10 +450,7 @@ class Task extends TaskFragment {
*/
int mLastReportedRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
- // For comparison with DisplayContent bounds.
- private Rect mTmpRect = new Rect();
- // For handling display rotations.
- private Rect mTmpRect2 = new Rect();
+ private final Rect mTmpRect = new Rect();
// Resize mode of the task. See {@link ActivityInfo#resizeMode}
// Based on the {@link ActivityInfo#resizeMode} of the root activity.
@@ -1194,9 +1183,6 @@ class Task extends TaskFragment {
updateOverrideConfigurationFromLaunchBounds();
}
- // Update task bounds if needed.
- adjustBoundsForDisplayChangeIfNeeded(getDisplayContent());
-
mRootWindowContainer.updateUIDsPresentOnDisplay();
// Ensure all animations are finished at same time in split-screen mode.
@@ -1468,6 +1454,11 @@ class Task extends TaskFragment {
// passed from Task constructor.
final TaskFragment childTaskFrag = child.asTaskFragment();
if (childTaskFrag != null && childTaskFrag.asTask() == null) {
+ if (childTaskFrag.mTaskFragmentOrganizerProcessName != null
+ && mTaskFragmentHostProcessName == null) {
+ mTaskFragmentHostUid = childTaskFrag.mTaskFragmentOrganizerUid;
+ mTaskFragmentHostProcessName = childTaskFrag.mTaskFragmentOrganizerProcessName;
+ }
childTaskFrag.setMinDimensions(mMinWidth, mMinHeight);
// The starting window should keep covering its task when a pure TaskFragment is added
@@ -2731,15 +2722,7 @@ class Task extends TaskFragment {
return setBounds(getRequestedOverrideBounds(), bounds);
}
- int rotation = Surface.ROTATION_0;
- final DisplayContent displayContent = getRootTask() != null
- ? getRootTask().getDisplayContent() : null;
- if (displayContent != null) {
- rotation = displayContent.getDisplayInfo().rotation;
- }
-
final int boundsChange = super.setBounds(bounds);
- mRotation = rotation;
updateSurfacePositionNonOrganized();
return boundsChange;
}
@@ -2799,10 +2782,6 @@ class Task extends TaskFragment {
@Override
void onDisplayChanged(DisplayContent dc) {
- final boolean isRootTask = isRootTask();
- if (!isRootTask && !mCreatedByOrganizer) {
- adjustBoundsForDisplayChangeIfNeeded(dc);
- }
super.onDisplayChanged(dc);
if (isLeafTask()) {
final int displayId = (dc != null) ? dc.getDisplayId() : INVALID_DISPLAY;
@@ -2953,48 +2932,6 @@ class Task extends TaskFragment {
return mDragResizing;
}
- void adjustBoundsForDisplayChangeIfNeeded(final DisplayContent displayContent) {
- if (displayContent == null) {
- return;
- }
- if (getRequestedOverrideBounds().isEmpty()) {
- return;
- }
- final int displayId = displayContent.getDisplayId();
- final int newRotation = displayContent.getDisplayInfo().rotation;
- if (displayId != mLastRotationDisplayId) {
- // This task is on a display that it wasn't on. There is no point to keep the relative
- // position if display rotations for old and new displays are different. Just keep these
- // values.
- mLastRotationDisplayId = displayId;
- mRotation = newRotation;
- return;
- }
-
- if (mRotation == newRotation) {
- // Rotation didn't change. We don't need to adjust the bounds to keep the relative
- // position.
- return;
- }
-
- // Device rotation changed.
- // - We don't want the task to move around on the screen when this happens, so update the
- // task bounds so it stays in the same place.
- // - Rotate the bounds and notify activity manager if the task can be resized independently
- // from its root task. The root task will take care of task rotation for the other case.
- mTmpRect2.set(getBounds());
-
- if (!getWindowConfiguration().canResizeTask()) {
- setBounds(mTmpRect2);
- return;
- }
-
- displayContent.rotateBounds(mRotation, newRotation, mTmpRect2);
- if (setBounds(mTmpRect2) != BOUNDS_CHANGE_NONE) {
- mAtmService.resizeTask(mTaskId, getBounds(), RESIZE_MODE_SYSTEM_SCREEN_ROTATION);
- }
- }
-
/** Cancels any running app transitions associated with the task. */
void cancelTaskWindowTransition() {
for (int i = mChildren.size() - 1; i >= 0; --i) {
@@ -3525,8 +3462,6 @@ class Task extends TaskFragment {
info.isVisibleRequested = isVisibleRequested();
info.isSleeping = shouldSleepActivities();
info.isTopActivityTransparent = top != null && !top.fillsParent();
- appCompatTaskInfo.isLetterboxDoubleTapEnabled = top != null
- && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled();
appCompatTaskInfo.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
appCompatTaskInfo.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
appCompatTaskInfo.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET;
@@ -3541,15 +3476,29 @@ class Task extends TaskFragment {
appCompatTaskInfo.topActivityLetterboxWidth = top.getBounds().width();
appCompatTaskInfo.topActivityLetterboxHeight = top.getBounds().height();
}
+ // We need to consider if letterboxed or pillarboxed
+ // TODO(b/336807329) Encapsulate reachability logic
+ appCompatTaskInfo.isLetterboxDoubleTapEnabled = top != null
+ && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled();
if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) {
if (appCompatTaskInfo.isTopActivityPillarboxed()) {
- // Pillarboxed
- appCompatTaskInfo.topActivityLetterboxHorizontalPosition =
- top.mLetterboxUiController.getLetterboxPositionForHorizontalReachability();
+ if (top.mLetterboxUiController.allowHorizontalReachabilityForThinLetterbox()) {
+ // Pillarboxed
+ appCompatTaskInfo.topActivityLetterboxHorizontalPosition =
+ top.mLetterboxUiController
+ .getLetterboxPositionForHorizontalReachability();
+ } else {
+ appCompatTaskInfo.isLetterboxDoubleTapEnabled = false;
+ }
} else {
- // Letterboxed
- appCompatTaskInfo.topActivityLetterboxVerticalPosition =
- top.mLetterboxUiController.getLetterboxPositionForVerticalReachability();
+ if (top.mLetterboxUiController.allowVerticalReachabilityForThinLetterbox()) {
+ // Letterboxed
+ appCompatTaskInfo.topActivityLetterboxVerticalPosition =
+ top.mLetterboxUiController
+ .getLetterboxPositionForVerticalReachability();
+ } else {
+ appCompatTaskInfo.isLetterboxDoubleTapEnabled = false;
+ }
}
}
appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton = top != null
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 2b631f7a404e..6a7f60b3447d 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -320,9 +320,9 @@ class TaskFragment extends WindowContainer<WindowContainer> {
/** Organizer that organizing this TaskFragment. */
@Nullable
private ITaskFragmentOrganizer mTaskFragmentOrganizer;
- @VisibleForTesting
+
int mTaskFragmentOrganizerUid = INVALID_UID;
- private @Nullable String mTaskFragmentOrganizerProcessName;
+ @Nullable String mTaskFragmentOrganizerProcessName;
/** Client assigned unique token for this TaskFragment if this is created by an organizer. */
@Nullable
@@ -485,14 +485,16 @@ class TaskFragment extends WindowContainer<WindowContainer> {
*/
@Nullable
private WindowProcessController getOrganizerProcessIfDifferent(@Nullable ActivityRecord r) {
- if ((r == null || mTaskFragmentOrganizerProcessName == null)
- || (mTaskFragmentOrganizerProcessName.equals(r.processName)
- && mTaskFragmentOrganizerUid == r.getUid())) {
- // No organizer or the process is the same.
+ final Task task = getTask();
+ if (r == null || task == null || task.mTaskFragmentHostProcessName == null) {
+ return null;
+ }
+ if (task.mTaskFragmentHostProcessName.equals(r.processName)
+ && task.mTaskFragmentHostUid == r.getUid()) {
return null;
}
- return mAtmService.getProcessController(mTaskFragmentOrganizerProcessName,
- mTaskFragmentOrganizerUid);
+ return mAtmService.getProcessController(task.mTaskFragmentHostProcessName,
+ task.mTaskFragmentHostUid);
}
void setAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index c25080f9e756..e90c845f0d21 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -858,6 +858,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
* {@link InsetsStateController#notifyInsetsChanged}.
*/
boolean isReadyToDispatchInsetsState() {
+ if (mStartingData != null) {
+ // Starting window doesn't consider insets.
+ return false;
+ }
final boolean visible = shouldCheckTokenVisibleRequested()
? isVisibleRequested() : isVisible();
return visible && mFrozenInsetsState == null;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 32cb2510bc9f..62f5b89e701c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -88,7 +88,6 @@ namespace input_flags = com::android::input::flags;
namespace android {
-static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer();
static const bool ENABLE_INPUT_FILTER_RUST = input_flags::enable_input_filter_rust_impl();
// The exponent used to calculate the pointer speed scaling factor.
@@ -298,10 +297,8 @@ public:
void setShowTouches(bool enabled);
void setInteractive(bool interactive);
void reloadCalibration();
- void setPointerIconType(PointerIconStyle iconId);
void reloadPointerIcons();
void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
- void setCustomPointerIcon(const SpriteIcon& icon);
bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon,
int32_t displayId, DeviceId deviceId, int32_t pointerId,
const sp<IBinder>& inputToken);
@@ -316,7 +313,6 @@ public:
/* --- InputReaderPolicyInterface implementation --- */
void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
- std::shared_ptr<PointerControllerInterface> obtainPointerController(int32_t deviceId) override;
void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override;
std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay(
const InputDeviceIdentifier& identifier,
@@ -375,7 +371,6 @@ public:
virtual PointerIconStyle getDefaultPointerIconId();
virtual PointerIconStyle getDefaultStylusIconId();
virtual PointerIconStyle getCustomPointerIconId();
- virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position);
/* --- PointerChoreographerPolicyInterface implementation --- */
std::shared_ptr<PointerControllerInterface> createPointerController(
@@ -409,19 +404,12 @@ private:
// True if pointer gestures are enabled.
bool pointerGesturesEnabled{true};
- // Show touches feature enable/disable.
- bool showTouches{false};
-
// The latest request to enable or disable Pointer Capture.
PointerCaptureRequest pointerCaptureRequest{};
// Sprite controller singleton, created on first use.
std::shared_ptr<SpriteController> spriteController{};
- // TODO(b/293587049): Remove when the PointerChoreographer refactoring is complete.
- // Pointer controller singleton, created and destroyed as needed.
- std::weak_ptr<PointerController> legacyPointerController{};
-
// The list of PointerControllers created and managed by the PointerChoreographer.
std::list<std::weak_ptr<PointerController>> pointerControllers{};
@@ -504,14 +492,10 @@ void NativeInputManager::dump(std::string& dump) {
dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n",
dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str());
dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n",
- toString(mLocked.pointerGesturesEnabled));
- dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches));
+ toString(mLocked.pointerGesturesEnabled));
dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n",
mLocked.pointerCaptureRequest.isEnable() ? "Enabled" : "Disabled",
mLocked.pointerCaptureRequest.seq);
- if (auto pc = mLocked.legacyPointerController.lock(); pc) {
- dump += pc->dump();
- }
} // release lock
dump += "\n";
@@ -556,9 +540,7 @@ void NativeInputManager::setDisplayViewports(JNIEnv* env, jobjectArray viewportO
[&viewports](PointerController& pc) { pc.onDisplayViewportsUpdated(viewports); });
} // release lock
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- mInputManager->getChoreographer().setDisplayViewports(viewports);
- }
+ mInputManager->getChoreographer().setDisplayViewports(viewports);
mInputManager->getReader().requestRefreshConfiguration(
InputReaderConfiguration::Change::DISPLAY_INFO);
}
@@ -700,8 +682,6 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon
: 1;
outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;
- outConfig->showTouches = mLocked.showTouches;
-
outConfig->pointerCaptureRequest = mLocked.pointerCaptureRequest;
outConfig->setDisplayViewports(mLocked.viewports);
@@ -743,10 +723,6 @@ std::unordered_map<std::string, T> NativeInputManager::readMapFromInterleavedJav
void NativeInputManager::forEachPointerControllerLocked(
std::function<void(PointerController&)> apply) {
- if (auto pc = mLocked.legacyPointerController.lock(); pc) {
- apply(*pc);
- }
-
auto it = mLocked.pointerControllers.begin();
while (it != mLocked.pointerControllers.end()) {
auto pc = it->lock();
@@ -780,50 +756,16 @@ PointerIcon NativeInputManager::loadPointerIcon(JNIEnv* env, int32_t displayId,
return android_view_PointerIcon_toNative(env, pointerIconObj.get());
}
-// TODO(b/293587049): Remove the old way of obtaining PointerController when the
-// PointerChoreographer refactoring is complete.
-std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController(
- int32_t /* deviceId */) {
- ATRACE_CALL();
- std::scoped_lock _l(mLock);
-
- std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock();
- if (controller == nullptr) {
- ensureSpriteControllerLocked();
-
- // Disable the functionality of the legacy PointerController if PointerChoreographer is
- // enabled.
- controller = PointerController::create(this, mLooper, *mLocked.spriteController,
- /*enabled=*/!ENABLE_POINTER_CHOREOGRAPHER);
- mLocked.legacyPointerController = controller;
- updateInactivityTimeoutLocked();
- }
-
- return controller;
-}
-
std::shared_ptr<PointerControllerInterface> NativeInputManager::createPointerController(
PointerControllerInterface::ControllerType type) {
std::scoped_lock _l(mLock);
ensureSpriteControllerLocked();
std::shared_ptr<PointerController> pc =
- PointerController::create(this, mLooper, *mLocked.spriteController, /*enabled=*/true,
- type);
+ PointerController::create(this, mLooper, *mLocked.spriteController, type);
mLocked.pointerControllers.emplace_back(pc);
return pc;
}
-void NativeInputManager::onPointerDisplayIdChanged(int32_t pointerDisplayId,
- const FloatPoint& position) {
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- return;
- }
- JNIEnv* env = jniEnv();
- env->CallVoidMethod(mServiceObj, gServiceClassInfo.onPointerDisplayIdChanged, pointerDisplayId,
- position.x, position.y);
- checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged");
-}
-
void NativeInputManager::notifyPointerDisplayIdChanged(int32_t pointerDisplayId,
const FloatPoint& position) {
// Notify the Reader so that devices can be reconfigured.
@@ -1210,23 +1152,7 @@ void NativeInputManager::updateInactivityTimeoutLocked() REQUIRES(mLock) {
}
void NativeInputManager::setPointerDisplayId(int32_t displayId) {
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId);
- } else {
- { // acquire lock
- std::scoped_lock _l(mLock);
-
- if (mLocked.pointerDisplayId == displayId) {
- return;
- }
-
- ALOGI("Setting pointer display id to %d.", displayId);
- mLocked.pointerDisplayId = displayId;
- } // release lock
-
- mInputManager->getReader().requestRefreshConfiguration(
- InputReaderConfiguration::Change::DISPLAY_INFO);
- }
+ mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId);
}
int32_t NativeInputManager::getMousePointerSpeed() {
@@ -1378,24 +1304,7 @@ void NativeInputManager::setInputDeviceEnabled(uint32_t deviceId, bool enabled)
}
void NativeInputManager::setShowTouches(bool enabled) {
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- mInputManager->getChoreographer().setShowTouchesEnabled(enabled);
- return;
- }
-
- { // acquire lock
- std::scoped_lock _l(mLock);
-
- if (mLocked.showTouches == enabled) {
- return;
- }
-
- ALOGI("Setting show touches feature to %s.", enabled ? "enabled" : "disabled");
- mLocked.showTouches = enabled;
- } // release lock
-
- mInputManager->getReader().requestRefreshConfiguration(
- InputReaderConfiguration::Change::SHOW_TOUCHES);
+ mInputManager->getChoreographer().setShowTouchesEnabled(enabled);
}
void NativeInputManager::requestPointerCapture(const sp<IBinder>& windowToken, bool enabled) {
@@ -1411,27 +1320,11 @@ void NativeInputManager::reloadCalibration() {
InputReaderConfiguration::Change::TOUCH_AFFINE_TRANSFORMATION);
}
-void NativeInputManager::setPointerIconType(PointerIconStyle iconId) {
- std::scoped_lock _l(mLock);
- std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock();
- if (controller != nullptr) {
- controller->updatePointerIcon(iconId);
- }
-}
-
void NativeInputManager::reloadPointerIcons() {
std::scoped_lock _l(mLock);
forEachPointerControllerLocked([](PointerController& pc) { pc.reloadPointerResources(); });
}
-void NativeInputManager::setCustomPointerIcon(const SpriteIcon& icon) {
- std::scoped_lock _l(mLock);
- std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock();
- if (controller != nullptr) {
- controller->setCustomPointerIcon(icon);
- }
-}
-
bool NativeInputManager::setPointerIcon(
std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon, int32_t displayId,
DeviceId deviceId, int32_t pointerId, const sp<IBinder>& inputToken) {
@@ -1447,9 +1340,6 @@ bool NativeInputManager::setPointerIcon(
}
void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) {
- if (!ENABLE_POINTER_CHOREOGRAPHER) {
- return;
- }
mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible);
}
@@ -1819,36 +1709,12 @@ void NativeInputManager::setStylusButtonMotionEventsEnabled(bool enabled) {
}
FloatPoint NativeInputManager::getMouseCursorPosition(int32_t displayId) {
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- return mInputManager->getChoreographer().getMouseCursorPosition(displayId);
- }
- // To maintain the status-quo, the displayId parameter (used when PointerChoreographer is
- // enabled) is ignored in the old pipeline.
- std::scoped_lock _l(mLock);
- const auto pc = mLocked.legacyPointerController.lock();
- if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION};
-
- return pc->getPosition();
+ return mInputManager->getChoreographer().getMouseCursorPosition(displayId);
}
void NativeInputManager::setStylusPointerIconEnabled(bool enabled) {
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- mInputManager->getChoreographer().setStylusPointerIconEnabled(enabled);
- return;
- }
-
- { // acquire lock
- std::scoped_lock _l(mLock);
-
- if (mLocked.stylusPointerIconEnabled == enabled) {
- return;
- }
-
- mLocked.stylusPointerIconEnabled = enabled;
- } // release lock
-
- mInputManager->getReader().requestRefreshConfiguration(
- InputReaderConfiguration::Change::DISPLAY_INFO);
+ mInputManager->getChoreographer().setStylusPointerIconEnabled(enabled);
+ return;
}
void NativeInputManager::setInputMethodConnectionIsActive(bool isActive) {
@@ -2597,27 +2463,12 @@ static void nativeDisableInputDevice(JNIEnv* env, jobject nativeImplObj, jint de
im->setInputDeviceEnabled(deviceId, false);
}
-static void nativeSetPointerIconType(JNIEnv* env, jobject nativeImplObj, jint iconId) {
- // iconId is set in java from from frameworks/base/core/java/android/view/PointerIcon.java,
- // where the definition in <input/Input.h> is duplicated as a sealed class (type safe enum
- // equivalent in Java).
-
- NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-
- im->setPointerIconType(static_cast<PointerIconStyle>(iconId));
-}
-
static void nativeReloadPointerIcons(JNIEnv* env, jobject nativeImplObj) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
im->reloadPointerIcons();
}
-static void nativeSetCustomPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj) {
- NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
- im->setCustomPointerIcon(toSpriteIcon(android_view_PointerIcon_toNative(env, iconObj)));
-}
-
static bool nativeSetPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj,
jint displayId, jint deviceId, jint pointerId,
jobject inputTokenObj) {
@@ -2937,10 +2788,7 @@ static const JNINativeMethod gInputManagerMethods[] = {
{"isInputDeviceEnabled", "(I)Z", (void*)nativeIsInputDeviceEnabled},
{"enableInputDevice", "(I)V", (void*)nativeEnableInputDevice},
{"disableInputDevice", "(I)V", (void*)nativeDisableInputDevice},
- {"setPointerIconType", "(I)V", (void*)nativeSetPointerIconType},
{"reloadPointerIcons", "()V", (void*)nativeReloadPointerIcons},
- {"setCustomPointerIcon", "(Landroid/view/PointerIcon;)V",
- (void*)nativeSetCustomPointerIcon},
{"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z",
(void*)nativeSetPointerIcon},
{"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility},
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8755a8077ddb..cfe4e17eb1be 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -47,6 +47,7 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.res.Configuration;
import android.content.res.Resources.Theme;
+import android.crashrecovery.flags.Flags;
import android.credentials.CredentialManager;
import android.database.sqlite.SQLiteCompatibilityWalFlags;
import android.database.sqlite.SQLiteGlobal;
@@ -1195,11 +1196,13 @@ public final class SystemServer implements Dumpable {
mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class);
t.traceEnd();
- // Now that we have the bare essentials of the OS up and running, take
- // note that we just booted, which might send out a rescue party if
- // we're stuck in a runtime restart loop.
- RescueParty.registerHealthObserver(mSystemContext);
- PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ if (!Flags.recoverabilityDetection()) {
+ // Now that we have the bare essentials of the OS up and running, take
+ // note that we just booted, which might send out a rescue party if
+ // we're stuck in a runtime restart loop.
+ RescueParty.registerHealthObserver(mSystemContext);
+ PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ }
// Manages LEDs and display backlight so we need it to bring up the display.
t.traceBegin("StartLightsService");
@@ -1469,9 +1472,12 @@ public final class SystemServer implements Dumpable {
boolean enableVrService = context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
- // For debugging RescueParty
- if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.crash_system", false)) {
- throw new RuntimeException();
+ if (!Flags.recoverabilityDetection()) {
+ // For debugging RescueParty
+ if (Build.IS_DEBUGGABLE
+ && SystemProperties.getBoolean("debug.crash_system", false)) {
+ throw new RuntimeException();
+ }
}
try {
@@ -2910,6 +2916,14 @@ public final class SystemServer implements Dumpable {
mPackageManagerService.systemReady();
t.traceEnd();
+ if (Flags.recoverabilityDetection()) {
+ // Now that we have the essential services needed for rescue party, initialize
+ // RescuParty. note that we just booted, which might send out a rescue party if
+ // we're stuck in a runtime restart loop.
+ RescueParty.registerHealthObserver(mSystemContext);
+ PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ }
+
t.traceBegin("MakeDisplayManagerServiceReady");
try {
// TODO: use boot phase and communicate this flag some other way
@@ -3313,6 +3327,14 @@ public final class SystemServer implements Dumpable {
* are updated outside of OTA; and to avoid breaking dependencies from system into apexes.
*/
private void startApexServices(@NonNull TimingsTraceAndSlog t) {
+ if (Flags.recoverabilityDetection()) {
+ // For debugging RescueParty
+ if (Build.IS_DEBUGGABLE
+ && SystemProperties.getBoolean("debug.crash_system", false)) {
+ throw new RuntimeException();
+ }
+ }
+
t.traceBegin("startApexServices");
// TODO(b/192880996): get the list from "android" package, once the manifest entries
// are migrated to system manifest.
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index 4a2164582890..42814e7c775e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -239,6 +239,9 @@ public class RescuePartyTest {
@Test
public void testBootLoopDetectionWithExecutionForAllRescueLevels() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
any(Executor.class),
@@ -449,6 +452,9 @@ public class RescuePartyTest {
@Test
public void testNonPersistentAppCrashDetectionWithScopedResets() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
any(Executor.class),
@@ -506,6 +512,9 @@ public class RescuePartyTest {
@Test
public void testNonDeviceConfigSettingsOnlyResetOncePerLevel() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
any(Executor.class),
@@ -879,6 +888,9 @@ public class RescuePartyTest {
@Test
public void testBootLoopLevels() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
assertEquals(observer.onBootLoop(0), PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 11f20e35b4b1..d15c24bd68b9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -31,6 +31,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sUptimeMillisClock;
import static com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS;
import static com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK;
+import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -74,6 +75,9 @@ import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import com.android.server.AppStateTracker;
@@ -85,6 +89,8 @@ import com.android.server.SystemServiceManager;
import com.android.server.job.controllers.ConnectivityController;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.QuotaController;
+import com.android.server.job.restrictions.JobRestriction;
+import com.android.server.job.restrictions.ThermalStatusRestriction;
import com.android.server.pm.UserManagerInternal;
import com.android.server.usage.AppStandbyInternal;
@@ -121,6 +127,9 @@ public class JobSchedulerServiceTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private ChargingPolicyChangeListener mChargingPolicyChangeListener;
private class TestJobSchedulerService extends JobSchedulerService {
@@ -2385,6 +2394,108 @@ public class JobSchedulerServiceTest {
assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job2b));
}
+ /**
+ * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with single {@link
+ * JobRestriction} registered.
+ */
+ @Test
+ public void testCheckIfRestrictedSingleRestriction() {
+ int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE;
+ JobStatus fgsJob =
+ createJobStatus(
+ "testCheckIfRestrictedSingleRestriction", createJobInfo(1).setBias(bias));
+ ThermalStatusRestriction mockThermalStatusRestriction =
+ mock(ThermalStatusRestriction.class);
+ mService.mJobRestrictions.clear();
+ mService.mJobRestrictions.add(mockThermalStatusRestriction);
+ when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+
+ synchronized (mService.mLock) {
+ assertEquals(mService.checkIfRestricted(fgsJob), mockThermalStatusRestriction);
+ }
+
+ when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(fgsJob));
+ }
+ }
+
+ /**
+ * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with multiple {@link
+ * JobRestriction} registered.
+ */
+ @Test
+ public void testCheckIfRestrictedMultipleRestrictions() {
+ int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE;
+ JobStatus fgsJob =
+ createJobStatus(
+ "testGetMinJobExecutionGuaranteeMs", createJobInfo(1).setBias(bias));
+ JobRestriction mock1JobRestriction = mock(JobRestriction.class);
+ JobRestriction mock2JobRestriction = mock(JobRestriction.class);
+ mService.mJobRestrictions.clear();
+ mService.mJobRestrictions.add(mock1JobRestriction);
+ mService.mJobRestrictions.add(mock2JobRestriction);
+
+ // Jobs will be restricted if any one of the registered {@link JobRestriction}
+ // reports true.
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ synchronized (mService.mLock) {
+ assertEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction);
+ }
+
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ synchronized (mService.mLock) {
+ assertEquals(mService.checkIfRestricted(fgsJob), mock2JobRestriction);
+ }
+
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(fgsJob));
+ }
+
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ synchronized (mService.mLock) {
+ assertNotEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction);
+ }
+ }
+
+ /**
+ * Jobs with foreground service and top app biases must not be restricted when the flag is
+ * disabled.
+ */
+ @Test
+ @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+ public void testCheckIfRestricted_highJobBias_flagThermalRestrictionsToFgsJobsDisabled() {
+ JobStatus fgsJob =
+ createJobStatus(
+ "testCheckIfRestrictedJobBiasFgs",
+ createJobInfo(1).setBias(JobInfo.BIAS_FOREGROUND_SERVICE));
+ JobStatus topAppJob =
+ createJobStatus(
+ "testCheckIfRestrictedJobBiasTopApp",
+ createJobInfo(2).setBias(JobInfo.BIAS_TOP_APP));
+
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(fgsJob));
+ assertNull(mService.checkIfRestricted(topAppJob));
+ }
+ }
+
+ /** Jobs with top app biases must not be restricted. */
+ @Test
+ public void testCheckIfRestricted_highJobBias() {
+ JobStatus topAppJob = createJobStatus(
+ "testCheckIfRestrictedJobBiasTopApp",
+ createJobInfo(1).setBias(JobInfo.BIAS_TOP_APP));
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(topAppJob));
+ }
+ }
+
private void setBatteryLevel(int level) {
doReturn(level).when(mBatteryManagerInternal).getBatteryLevel();
mService.mBatteryStateTracker
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
index 754f409b3966..c2c67e615228 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
@@ -28,6 +28,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -43,7 +44,12 @@ import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Context;
import android.os.PowerManager;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.DeviceConfig;
+import android.util.DebugUtils;
import androidx.test.runner.AndroidJUnit4;
@@ -53,6 +59,7 @@ import com.android.server.job.controllers.JobStatus;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -76,6 +83,157 @@ public class ThermalStatusRestrictionTest {
@Mock
private JobSchedulerService mJobSchedulerService;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ class JobStatusContainer {
+ public final JobStatus jobMinPriority;
+ public final JobStatus jobLowPriority;
+ public final JobStatus jobLowPriorityRunning;
+ public final JobStatus jobLowPriorityRunningLong;
+ public final JobStatus jobDefaultPriority;
+ public final JobStatus jobHighPriority;
+ public final JobStatus jobHighPriorityRunning;
+ public final JobStatus jobHighPriorityRunningLong;
+ public final JobStatus ejDowngraded;
+ public final JobStatus ej;
+ public final JobStatus ejRetried;
+ public final JobStatus ejRunning;
+ public final JobStatus ejRunningLong;
+ public final JobStatus ui;
+ public final JobStatus uiRetried;
+ public final JobStatus uiRunning;
+ public final JobStatus uiRunningLong;
+ public final JobStatus importantWhileForeground;
+ public final JobStatus importantWhileForegroundRunning;
+ public final JobStatus importantWhileForegroundRunningLong;
+ public final int[] allJobBiases = {
+ JobInfo.BIAS_ADJ_ALWAYS_RUNNING,
+ JobInfo.BIAS_ADJ_OFTEN_RUNNING,
+ JobInfo.BIAS_DEFAULT,
+ JobInfo.BIAS_SYNC_EXPEDITED,
+ JobInfo.BIAS_SYNC_INITIALIZATION,
+ JobInfo.BIAS_BOUND_FOREGROUND_SERVICE,
+ JobInfo.BIAS_FOREGROUND_SERVICE,
+ JobInfo.BIAS_TOP_APP
+ };
+ public final int[] biasesBelowFgs = {
+ JobInfo.BIAS_ADJ_ALWAYS_RUNNING,
+ JobInfo.BIAS_ADJ_OFTEN_RUNNING,
+ JobInfo.BIAS_DEFAULT,
+ JobInfo.BIAS_SYNC_EXPEDITED,
+ JobInfo.BIAS_SYNC_INITIALIZATION,
+ JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+ };
+ public final int[] thermalStatuses = {
+ THERMAL_STATUS_NONE,
+ THERMAL_STATUS_LIGHT,
+ THERMAL_STATUS_MODERATE,
+ THERMAL_STATUS_SEVERE,
+ THERMAL_STATUS_CRITICAL,
+ THERMAL_STATUS_EMERGENCY,
+ THERMAL_STATUS_SHUTDOWN
+ };
+
+ JobStatusContainer(String jobName, JobSchedulerService mJobSchedulerService) {
+ jobMinPriority =
+ createJobStatus(
+ jobName, createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build());
+ jobLowPriority =
+ createJobStatus(
+ jobName, createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build());
+ jobLowPriorityRunning =
+ createJobStatus(
+ jobName, createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build());
+ jobLowPriorityRunningLong =
+ createJobStatus(
+ jobName, createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build());
+ jobDefaultPriority =
+ createJobStatus(
+ jobName,
+ createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build());
+ jobHighPriority =
+ createJobStatus(
+ jobName,
+ createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build());
+ jobHighPriorityRunning =
+ createJobStatus(
+ jobName,
+ createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build());
+ jobHighPriorityRunningLong =
+ createJobStatus(
+ jobName,
+ createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build());
+ ejDowngraded = createJobStatus(jobName, createJobBuilder(7).setExpedited(true).build());
+ ej = spy(createJobStatus(jobName, createJobBuilder(8).setExpedited(true).build()));
+ ejRetried =
+ spy(createJobStatus(jobName, createJobBuilder(11).setExpedited(true).build()));
+ ejRunning =
+ spy(createJobStatus(jobName, createJobBuilder(12).setExpedited(true).build()));
+ ejRunningLong =
+ spy(createJobStatus(jobName, createJobBuilder(13).setExpedited(true).build()));
+ ui = spy(createJobStatus(jobName, createJobBuilder(14).build()));
+ uiRetried = spy(createJobStatus(jobName, createJobBuilder(15).build()));
+ uiRunning = spy(createJobStatus(jobName, createJobBuilder(16).build()));
+ uiRunningLong = spy(createJobStatus(jobName, createJobBuilder(17).build()));
+ importantWhileForeground = spy(createJobStatus(jobName, createJobBuilder(18)
+ .setImportantWhileForeground(true)
+ .build()));
+ importantWhileForegroundRunning = spy(createJobStatus(jobName, createJobBuilder(20)
+ .setImportantWhileForeground(true)
+ .build()));
+ importantWhileForegroundRunningLong = spy(createJobStatus(jobName, createJobBuilder(19)
+ .setImportantWhileForeground(true)
+ .build()));
+
+ when(ej.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(ejRetried.getNumPreviousAttempts()).thenReturn(1);
+ when(uiRetried.getNumPreviousAttempts()).thenReturn(2);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunning))
+ .thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(importantWhileForegroundRunningLong))
+ .thenReturn(true);
+ }
+ }
+
+ private boolean isJobRestricted(JobStatus status, int bias) {
+ return mThermalStatusRestriction.isJobRestricted(status, bias);
+ }
+
+ private static String debugTag(int bias, @PowerManager.ThermalStatus int status) {
+ return "Bias = "
+ + JobInfo.getBiasString(bias)
+ + " Thermal Status = "
+ + DebugUtils.valueToString(PowerManager.class, "THERMAL_STATUS_", status);
+ }
+
@Before
public void setUp() {
mMockingSession = mockitoSession()
@@ -156,169 +314,302 @@ public class ThermalStatusRestrictionTest {
assertEquals(THERMAL_STATUS_EMERGENCY, mThermalStatusRestriction.getThermalStatus());
}
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Thermal is in default state
+ */
@Test
- public void testIsJobRestricted() {
+ public void testIsJobRestrictedDefaultStates() {
mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE);
+ JobStatusContainer jc = new JobStatusContainer("testIsJobRestricted", mJobSchedulerService);
+
+ for (int jobBias : jc.allJobBiases) {
+ assertFalse(isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.ej, jobBias));
+ assertFalse(isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.ui, jobBias));
+ assertFalse(isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
- final JobStatus jobMinPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build());
- final JobStatus jobLowPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build());
- final JobStatus jobLowPriorityRunning = createJobStatus("testIsJobRestricted",
- createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build());
- final JobStatus jobLowPriorityRunningLong = createJobStatus("testIsJobRestricted",
- createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build());
- final JobStatus jobDefaultPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build());
- final JobStatus jobHighPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build());
- final JobStatus jobHighPriorityRunning = createJobStatus("testIsJobRestricted",
- createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build());
- final JobStatus jobHighPriorityRunningLong = createJobStatus("testIsJobRestricted",
- createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build());
- final JobStatus ejDowngraded = createJobStatus("testIsJobRestricted",
- createJobBuilder(7).setExpedited(true).build());
- final JobStatus ej = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(8).setExpedited(true).build()));
- final JobStatus ejRetried = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(11).setExpedited(true).build()));
- final JobStatus ejRunning = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(12).setExpedited(true).build()));
- final JobStatus ejRunningLong = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(13).setExpedited(true).build()));
- final JobStatus ui = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(14).build()));
- final JobStatus uiRetried = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(15).build()));
- final JobStatus uiRunning = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(16).build()));
- final JobStatus uiRunningLong = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(17).build()));
- when(ej.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(ejRetried.getNumPreviousAttempts()).thenReturn(1);
- when(uiRetried.getNumPreviousAttempts()).thenReturn(2);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
- .thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true);
-
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
-
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_LIGHT);
-
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
-
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_MODERATE);
-
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
-
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SEVERE);
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Top App and all
+ * Thermal states.
+ */
+ @Test
+ public void testIsJobRestrictedBiasTopApp() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasTopApp", mJobSchedulerService);
+
+ int jobBias = JobInfo.BIAS_TOP_APP;
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = "Thermal Status = " + DebugUtils.valueToString(
+ PowerManager.class, "THERMAL_STATUS_", thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+
+ // No restrictions on any jobs
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ej));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ui));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground
+ * Service and all Thermal states.
+ */
+ @Test
+ @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+ public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsDisabled() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService);
+
+ int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE;
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = "Thermal Status = " + DebugUtils.valueToString(
+ PowerManager.class, "THERMAL_STATUS_", thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+ // No restrictions on any jobs
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_CRITICAL);
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground
+ * Service and all Thermal states.
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+ public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsEnabled() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService);
+ int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE;
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = debugTag(jobBias, thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+ if (thermalStatus >= THERMAL_STATUS_SEVERE) {
+ // Full restrictions on all jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ui, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ } else if (thermalStatus >= THERMAL_STATUS_MODERATE) {
+ // No restrictions on user related jobs
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ // Some restrictions on expedited jobs
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ // Some restrictions on high priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ // Some restructions on important while foreground jobs
+ assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertTrue(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ // Full restriction on default priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ // Full restriction on low priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ // Full restriction on min priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ } else {
+ // thermalStatus < THERMAL_STATUS_MODERATE
+ // No restrictions on any job type
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ }
+ }
+ }
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ej));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ui));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is less than
+ * Foreground Service and all Thermal states.
+ */
+ @Test
+ public void testIsJobRestrictedBiasLessThanFgs() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasLessThanFgs", mJobSchedulerService);
+
+ for (int jobBias : jc.biasesBelowFgs) {
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = debugTag(jobBias, thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+ if (thermalStatus >= THERMAL_STATUS_SEVERE) {
+ // Full restrictions on all jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ui, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ } else if (thermalStatus >= THERMAL_STATUS_MODERATE) {
+ // No restrictions on user related jobs
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ // Some restrictions on expedited jobs
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ // Some restrictions on high priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ // Full restriction on default priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ // Full restriction on low priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ // Full restriction on min priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ } else if (thermalStatus >= THERMAL_STATUS_LIGHT) {
+ // No restrictions on any user related jobs
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ // No restrictions on any expedited jobs
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ // No restrictions on any high priority jobs
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ // No restrictions on default priority jobs
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ // Some restrictions on low priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ // Full restriction on min priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ } else { // THERMAL_STATUS_NONE
+ // No restrictions on any jobs
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
+ }
}
private JobInfo.Builder createJobBuilder(int jobId) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 79f1574105ba..7d58a2e53693 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -778,6 +778,49 @@ public final class UserManagerServiceTest {
}
@Test
+ public void testGetAliveUsers_shouldExcludeInitialisedEphemeralNonCurrentUsers() {
+ assertWithMessage("Ephemeral user should not exist at all initially")
+ .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID))
+ .isFalse();
+
+ // add an ephemeral full user
+ TestUserData userData = new TestUserData(USER_ID);
+ userData.info.flags = UserInfo.FLAG_FULL | UserInfo.FLAG_EPHEMERAL;
+ addUserData(userData);
+
+ assertWithMessage("Ephemeral user should exist as alive after being created")
+ .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID))
+ .isTrue();
+
+ // mock switch to the user (mark it as initialized & make it the current user)
+ userData.info.flags |= UserInfo.FLAG_INITIALIZED;
+ mockCurrentUser(USER_ID);
+
+ assertWithMessage("Ephemeral user should still exist as alive after being switched to")
+ .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID))
+ .isTrue();
+
+ // switch away from the user
+ mockCurrentUser(OTHER_USER_ID);
+
+ assertWithMessage("Ephemeral user should not exist as alive after getting switched away")
+ .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID))
+ .isFalse();
+
+ assertWithMessage("Ephemeral user should still exist as dying after getting switched away")
+ .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID))
+ .isTrue();
+
+ // finally remove the user
+ mUms.removeUserInfo(USER_ID);
+
+ assertWithMessage("Ephemeral user should not exist at all after cleanup")
+ .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID))
+ .isFalse();
+ }
+
+
+ @Test
@RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
public void testCreatePrivateProfileOnHeadlessSystemUser_shouldAllowCreation() {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java
new file mode 100644
index 000000000000..0716a5c5561d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.biometrics;
+
+import static com.android.server.biometrics.sensors.BiometricNotificationUtils.NOTIFICATION_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricsProtoEnums;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BiometricDanglingReceiverTest {
+ @Rule
+ public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ private BiometricDanglingReceiver mBiometricDanglingReceiver;
+
+ @Rule
+ public final TestableContext mContext = spy(new TestableContext(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(), null));
+
+ @Mock
+ NotificationManager mNotificationManager;
+
+ @Mock
+ Intent mIntent;
+
+ @Captor
+ private ArgumentCaptor<Intent> mArgumentCaptor;
+
+ @Before
+ public void setUp() {
+ mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
+ }
+
+ @Test
+ public void testFingerprintRegisterReceiver() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any());
+ }
+
+ @Test
+ public void testFaceRegisterReceiver() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE);
+ verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any());
+ }
+
+ @Test
+ public void testOnReceive_fingerprintReEnrollLaunch() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ when(mIntent.getAction()).thenReturn(
+ BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+
+ mBiometricDanglingReceiver.onReceive(mContext, mIntent);
+
+ // Verify fingerprint enroll process is launched.
+ verify(mContext).startActivity(mArgumentCaptor.capture());
+ assertThat(mArgumentCaptor.getValue().getAction())
+ .isEqualTo(Settings.ACTION_FINGERPRINT_ENROLL);
+
+ // Verify notification is canceled
+ verify(mNotificationManager).cancelAsUser("FingerprintReEnroll", NOTIFICATION_ID,
+ UserHandle.CURRENT);
+
+ // Verify receiver is unregistered after receiving the broadcast
+ verify(mContext).unregisterReceiver(mBiometricDanglingReceiver);
+ }
+
+ @Test
+ public void testOnReceive_faceReEnrollLaunch() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE);
+ when(mIntent.getAction()).thenReturn(
+ BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
+
+ mBiometricDanglingReceiver.onReceive(mContext, mIntent);
+
+ // Verify face enroll process is launched.
+ verify(mContext).startActivity(mArgumentCaptor.capture());
+ assertThat(mArgumentCaptor.getValue().getAction())
+ .isEqualTo(BiometricDanglingReceiver.FACE_SETTINGS_ACTION);
+
+ // Verify notification is canceled
+ verify(mNotificationManager).cancelAsUser("FaceReEnroll", NOTIFICATION_ID,
+ UserHandle.CURRENT);
+
+ // Verify receiver is unregistered after receiving the broadcast.
+ verify(mContext).unregisterReceiver(mBiometricDanglingReceiver);
+ }
+
+ private void initBroadcastReceiver(int modality) {
+ mBiometricDanglingReceiver = new BiometricDanglingReceiver(mContext, modality);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index fc573d243129..37895315557a 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -1228,6 +1228,11 @@ public class BiometricSchedulerTest {
Slog.d(TAG, "TestInternalEnumerateClient#startHalOperation");
onEnumerationResult(TEST_FINGERPRINT, 0 /* remaining */);
}
+
+ @Override
+ protected int getModality() {
+ return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+ }
}
private static class TestRemovalClient extends RemovalClient<Fingerprint, Object> {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
index 9845b58f79bf..d8bdd50d8c08 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
@@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -79,15 +81,21 @@ public class FaceInternalEnumerateClientTest {
private final int mBiometricId = 1;
private final Face mFace = new Face("face", mBiometricId, 1 /* deviceId */);
private FaceInternalEnumerateClient mClient;
+ private boolean mNotificationSent;
@Before
public void setUp() {
when(mAidlSession.getSession()).thenReturn(mSession);
-
final List<Face> enrolled = new ArrayList<>();
enrolled.add(mFace);
- mClient = new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID,
- TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext);
+ mClient = spy(new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID,
+ TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext));
+
+ mNotificationSent = false;
+ doAnswer(invocation -> {
+ mNotificationSent = true;
+ return null;
+ }).when(mClient).sendDanglingNotification(anyList());
}
@Test
@@ -101,6 +109,7 @@ public class FaceInternalEnumerateClientTest {
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback).onClientFinished(mClient, true);
}
@@ -116,6 +125,7 @@ public class FaceInternalEnumerateClientTest {
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback, never()).onClientFinished(mClient, true);
}
@@ -131,6 +141,7 @@ public class FaceInternalEnumerateClientTest {
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isTrue();
verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
verify(mCallback).onClientFinished(mClient, true);
}
@@ -147,6 +158,7 @@ public class FaceInternalEnumerateClientTest {
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback, never()).onClientFinished(mClient, true);
}
@@ -164,6 +176,7 @@ public class FaceInternalEnumerateClientTest {
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
+ assertThat(mNotificationSent).isTrue();
verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
verify(mCallback).onClientFinished(mClient, true);
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
index b5df8362b09f..fab120016434 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
@@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -80,15 +82,23 @@ public class FingerprintInternalEnumerateClientTest {
private FingerprintInternalEnumerateClient mClient;
+ private boolean mNotificationSent;
+
@Before
public void setUp() {
when(mAidlSession.getSession()).thenReturn(mSession);
List<Fingerprint> enrolled = new ArrayList<>();
enrolled.add(new Fingerprint("one", 1, 1));
- mClient = new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken,
+ mClient = spy(new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken,
USER_ID, TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger,
- mBiometricContext);
+ mBiometricContext));
+
+ mNotificationSent = false;
+ doAnswer(invocation -> {
+ mNotificationSent = true;
+ return null;
+ }).when(mClient).sendDanglingNotification(anyList());
}
@Test
@@ -104,6 +114,7 @@ public class FingerprintInternalEnumerateClientTest {
assertThat(mClient.getUnknownHALTemplates().stream()
.flatMap(x -> Stream.of(x.getBiometricId()))
.collect(Collectors.toList())).containsExactly(2, 3);
+ assertThat(mNotificationSent).isTrue();
verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, 1);
verify(mCallback).onClientFinished(mClient, true);
}
@@ -118,6 +129,7 @@ public class FingerprintInternalEnumerateClientTest {
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback).onClientFinished(mClient, true);
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index fd880dd23f25..178e7ec649c6 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -33,7 +33,6 @@ import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.DisplayInfo;
@@ -41,13 +40,11 @@ import android.view.WindowManager;
import androidx.test.InstrumentationRegistry;
-import com.android.input.flags.Flags;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
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;
@@ -60,9 +57,6 @@ public class InputControllerTest {
private static final String LANGUAGE_TAG = "en-US";
private static final String LAYOUT_TYPE = "qwerty";
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
@Mock
private InputManagerInternal mInputManagerInternalMock;
@Mock
@@ -77,8 +71,6 @@ public class InputControllerTest {
@Before
public void setUp() throws Exception {
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER);
-
MockitoAnnotations.initMocks(this);
mInputManagerMockHelper = new InputManagerMockHelper(
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 2b81d78a9163..da8961df38a8 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -339,8 +339,6 @@ public class VirtualDeviceManagerServiceTest {
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
- mSetFlagsRule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER);
-
doNothing().when(mInputManagerInternalMock)
.setMousePointerAccelerationEnabled(anyBoolean(), anyInt());
doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 5fdb3965be76..d1423fe71e50 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -3002,39 +3002,45 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
}
- private enum ModesApiFlag {
- ENABLED(true, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
- DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
+ private enum ModesFlag {
+ MODES_UI(2, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
+ MODES_API(1, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
+ DISABLED(0, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
- private final boolean mEnabled;
+ private final int mFlagsEnabled;
@ConfigChangeOrigin
private final int mOriginForUserActionInSystemUi;
- ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
- this.mEnabled = enabled;
+ ModesFlag(int flagsEnabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
+ this.mFlagsEnabled = flagsEnabled;
this.mOriginForUserActionInSystemUi = originForUserActionInSystemUi;
}
- void applyFlag(SetFlagsRule setFlagsRule) {
- if (mEnabled) {
+ void applyFlags(SetFlagsRule setFlagsRule) {
+ if (mFlagsEnabled >= 1) {
setFlagsRule.enableFlags(Flags.FLAG_MODES_API);
} else {
setFlagsRule.disableFlags(Flags.FLAG_MODES_API);
}
+ if (mFlagsEnabled >= 2) {
+ setFlagsRule.enableFlags(Flags.FLAG_MODES_UI);
+ } else {
+ setFlagsRule.disableFlags(Flags.FLAG_MODES_UI);
+ }
}
}
@Test
- public void testZenModeEventLog_setManualZenMode(@TestParameter ModesApiFlag modesApiFlag)
+ public void testZenModeEventLog_setManualZenMode(@TestParameter ModesFlag modesFlag)
throws IllegalArgumentException {
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
// Turn zen mode on (to important_interruptions)
// Need to additionally call the looper in order to finish the post-apply-config process
mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
- modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
// Now turn zen mode off, but via a different package UID -- this should get registered as
// "not an action by the user" because some other app is changing zen mode
@@ -3062,7 +3068,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo(
- modesApiFlag == ModesApiFlag.DISABLED);
+ modesFlag == ModesFlag.DISABLED);
assertTrue(mZenModeEventLogger.getIsUserAction(0));
assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
@@ -3091,9 +3097,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- public void testZenModeEventLog_automaticRules(@TestParameter ModesApiFlag modesApiFlag)
+ public void testZenModeEventLog_automaticRules(@TestParameter ModesFlag modesFlag)
throws IllegalArgumentException {
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -3116,7 +3122,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// Event 2: "User" turns off the automatic rule (sets it to not enabled)
zenRule.setEnabled(false);
mZenModeHelper.updateAutomaticZenRule(id, zenRule,
- modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
// Add a new system rule
AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -3134,7 +3140,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// Event 4: "User" deletes the rule
- mZenModeHelper.removeAutomaticZenRule(systemId, modesApiFlag.mOriginForUserActionInSystemUi,
+ mZenModeHelper.removeAutomaticZenRule(systemId, modesFlag.mOriginForUserActionInSystemUi,
"", Process.SYSTEM_UID);
// In total, this represents 4 events
@@ -3282,22 +3288,22 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- public void testZenModeEventLog_policyChanges(@TestParameter ModesApiFlag modesApiFlag)
+ public void testZenModeEventLog_policyChanges(@TestParameter ModesFlag modesFlag)
throws IllegalArgumentException {
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
// First just turn zen mode on
mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
- modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
// Now change the policy slightly; want to confirm that this'll be reflected in the logs
ZenModeConfig newConfig = mZenModeHelper.mConfig.copy();
newConfig.allowAlarms = true;
newConfig.allowRepeatCallers = false;
mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
- modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
// Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
// is off.
@@ -3308,7 +3314,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
newConfig.allowMessages = false;
newConfig.allowRepeatCallers = true;
mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
- modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
// Total events: we only expect ones for turning on, changing policy, and turning off
assertEquals(3, mZenModeEventLogger.numLoggedChanges());
@@ -3341,9 +3347,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- public void testZenModeEventLog_ruleCounts(@TestParameter ModesApiFlag modesApiFlag)
+ public void testZenModeEventLog_ruleCounts(@TestParameter ModesFlag modesFlag)
throws IllegalArgumentException {
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -3447,9 +3453,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
@Test
public void testZenModeEventLog_noLogWithNoConfigChange(
- @TestParameter ModesApiFlag modesApiFlag) throws IllegalArgumentException {
+ @TestParameter ModesFlag modesFlag) throws IllegalArgumentException {
// If evaluateZenMode is called independently of a config change, don't log.
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -3466,11 +3472,11 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- public void testZenModeEventLog_reassignUid(@TestParameter ModesApiFlag modesApiFlag)
+ public void testZenModeEventLog_reassignUid(@TestParameter ModesFlag modesFlag)
throws IllegalArgumentException {
// Test that, only in specific cases, we reassign the calling UID to one associated with
// the automatic rule owner.
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -3496,7 +3502,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
manualRulePolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
- modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
// Turn on rule 1; call looks like it's from the system. Because setting a condition is
// typically an automatic (non-user-initiated) action, expect the calling UID to be
@@ -3515,7 +3521,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// from the system-provided one.
zenRule.setEnabled(false);
mZenModeHelper.updateAutomaticZenRule(id, zenRule,
- modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
+ modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
// Add a manual rule. Any manual rule changes should not get calling uids reassigned.
mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP,
@@ -3573,9 +3579,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
@Test
public void testZenModeEventLog_channelsBypassingChanges(
- @TestParameter ModesApiFlag modesApiFlag) {
+ @TestParameter ModesFlag modesFlag) {
// Verify that the right thing happens when the canBypassDnd value changes.
- modesApiFlag.applyFlag(mSetFlagsRule);
+ modesFlag.applyFlags(mSetFlagsRule);
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
@@ -3847,8 +3853,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() {
+ public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDefault(
+ @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+ modesFlag.applyFlags(mSetFlagsRule);
setupZenConfig();
// When there's one automatic rule active and it doesn't specify a policy, test that the
@@ -3869,7 +3876,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// inspect the consolidated policy, which should match the device default settings.
assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy))
- .isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+ .isEqualTo(modesFlag == ModesFlag.MODES_UI
+ ? mZenModeHelper.getDefaultZenPolicy()
+ : mZenModeHelper.mConfig.toZenPolicy());
}
@Test
@@ -3904,7 +3913,8 @@ public class ZenModeHelperTest extends UiServiceTestCase {
UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// since this is the only active rule, the consolidated policy should match the custom
- // policy for every field specified, and take default values for unspecified things
+ // policy for every field specified, and take default values (from device default or
+ // manual policy) for unspecified things
assertTrue(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); // custom
assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMedia()); // custom
assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); // default
@@ -3918,8 +3928,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() {
+ public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDefault(
+ @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+ modesFlag.applyFlags(mSetFlagsRule);
setupZenConfig();
// when there's only one automatic rule active and it has a custom policy, make sure that's
@@ -3948,11 +3959,15 @@ public class ZenModeHelperTest extends UiServiceTestCase {
UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// since this is the only active rule, the consolidated policy should match the custom
- // policy for every field specified, and take default values for unspecified things
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default
+ // policy for every field specified, and take default values (from either device default
+ // policy or manual rule) for unspecified things
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? true : false); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? true : false); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // custom
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? false : true); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom
assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse(); // custom
@@ -4022,8 +4037,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() {
+ public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault(
+ @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+ modesFlag.applyFlags(mSetFlagsRule);
setupZenConfig();
// when there are two rules active, one inheriting the default policy and one setting its
@@ -4071,16 +4087,19 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// now both rules should be on, and the consolidated policy should reflect the most
// restrictive option of each of the two
assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // custom stricter
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? true : false); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default stricter
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? false : true); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom stricter
assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers())
.isFalse(); // custom stricter
assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom stricter
- assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse(); // default stricter
+ assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? false : true); // default
}
@Test
@@ -4134,8 +4153,9 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
- @EnableFlags(Flags.FLAG_MODES_API)
- public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll() {
+ public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll(
+ @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+ modesFlag.applyFlags(mSetFlagsRule);
setupZenConfig();
// Rules with INTERRUPTION_FILTER_ALL are skipped when calculating consolidated policy.
@@ -4172,10 +4192,12 @@ public class ZenModeHelperTest extends UiServiceTestCase {
UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
// Consolidated Policy should be default + rule1.
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? true : false); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule
assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // priority rule
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo(
+ modesFlag == ModesFlag.MODES_UI ? false : true); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default
@@ -6251,7 +6273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) {
- if (!Flags.modesApi()) {
+ if (!Flags.modesUi()) {
checkDndProtoMatchesSetupZenConfig(dndProto);
return;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 10eae577f706..13550923cf3d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -46,7 +46,6 @@ import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
-import static android.server.wm.ActivityManagerTestBase.isTablet;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
@@ -76,7 +75,6 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -124,7 +122,6 @@ import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.wm.BackgroundActivityStartController.BalVerdict;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
import com.android.server.wm.utils.MockTracker;
-import com.android.window.flags.Flags;
import org.junit.After;
import org.junit.Before;
@@ -1298,12 +1295,6 @@ public class ActivityStarterTests extends WindowTestsBase {
*/
@Test
public void testDeliverIntentToTopActivityOfNonTopDisplay() {
- // TODO(b/330152508): Remove check once legacy multi-display behaviour can coexist with
- // desktop windowing mode
- // Ignore test if desktop windowing is enabled on tablets as legacy multi-display
- // behaviour will not be respected
- assumeFalse(Flags.enableDesktopWindowingMode() && isTablet());
-
final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
false /* mockGetRootTask */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
index 695faa525dd6..39a2259cf77f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
@@ -19,6 +19,8 @@ package com.android.server.wm;
import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT;
import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
+import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.google.common.truth.Truth.assertThat;
@@ -145,6 +147,16 @@ public class BackgroundActivityStartControllerTests {
}
@Override
+ boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
+ return true;
+ }
+
+ @Override
+ boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
+ return true;
+ }
+
+ @Override
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
return mCallerVerdict.orElseGet(
() -> super.checkBackgroundActivityStartAllowedByCaller(state));
@@ -238,7 +250,12 @@ public class BackgroundActivityStartControllerTests {
// assertions
assertThat(verdict.getCode()).isEqualTo(BackgroundActivityStartController.BAL_BLOCK);
- assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ }
}
// Tests for BackgroundActivityStartController.checkBackgroundActivityStart
@@ -268,7 +285,12 @@ public class BackgroundActivityStartControllerTests {
// assertions
assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
- assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ }
}
@Test
@@ -298,7 +320,12 @@ public class BackgroundActivityStartControllerTests {
// assertions
assertThat(verdict).isEqualTo(callerVerdict);
- assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+ }
}
@Test
@@ -362,7 +389,13 @@ public class BackgroundActivityStartControllerTests {
// assertions
assertThat(verdict).isEqualTo(callerVerdict);
- assertThat(mBalAllowedLogs).containsExactly(new BalAllowedLog("", callerVerdict.getCode()));
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+ } else {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("", callerVerdict.getCode()));
+ }
}
@Test
@@ -398,7 +431,12 @@ public class BackgroundActivityStartControllerTests {
// assertions
assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
- assertThat(mBalAllowedLogs).isEmpty();
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty();
+ }
}
@Test
@@ -430,7 +468,12 @@ public class BackgroundActivityStartControllerTests {
// assertions
assertThat(verdict).isEqualTo(callerVerdict);
- assertThat(mBalAllowedLogs).isEmpty();
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty();
+ }
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index b74da1a888cd..a60d243c9dad 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -85,6 +85,8 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.Property;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.InsetsSource;
import android.view.InsetsState;
@@ -96,6 +98,7 @@ import android.view.WindowManager;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
+import com.android.window.flags.Flags;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -1528,6 +1531,98 @@ public class LetterboxUiControllerTest extends WindowTestsBase {
mActivity.getParent().getConfiguration()), /* delta */ 0.01);
}
+ @Test
+ public void testIsVerticalThinLetterboxed() {
+ // Vertical thin letterbox disabled
+ doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration)
+ .getThinLetterboxHeightPx();
+ assertFalse(mController.isVerticalThinLetterboxed());
+ // Define a Task 100x100
+ final Task task = mock(Task.class);
+ doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
+ doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration)
+ .getThinLetterboxHeightPx();
+
+ // Vertical thin letterbox disabled without Task
+ doReturn(null).when(mActivity).getTask();
+ assertFalse(mController.isVerticalThinLetterboxed());
+ // Assign a Task for the Activity
+ doReturn(task).when(mActivity).getTask();
+
+ // (task.width() - act.width()) / 2 = 5 < 10
+ doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds();
+ assertTrue(mController.isVerticalThinLetterboxed());
+
+ // (task.width() - act.width()) / 2 = 10 = 10
+ doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds();
+ assertTrue(mController.isVerticalThinLetterboxed());
+
+ // (task.width() - act.width()) / 2 = 11 > 10
+ doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds();
+ assertFalse(mController.isVerticalThinLetterboxed());
+ }
+
+ @Test
+ public void testIsHorizontalThinLetterboxed() {
+ // Horizontal thin letterbox disabled
+ doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration)
+ .getThinLetterboxWidthPx();
+ assertFalse(mController.isHorizontalThinLetterboxed());
+ // Define a Task 100x100
+ final Task task = mock(Task.class);
+ doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
+ doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration)
+ .getThinLetterboxWidthPx();
+
+ // Vertical thin letterbox disabled without Task
+ doReturn(null).when(mActivity).getTask();
+ assertFalse(mController.isHorizontalThinLetterboxed());
+ // Assign a Task for the Activity
+ doReturn(task).when(mActivity).getTask();
+
+ // (task.height() - act.height()) / 2 = 5 < 10
+ doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds();
+ assertTrue(mController.isHorizontalThinLetterboxed());
+
+ // (task.height() - act.height()) / 2 = 10 = 10
+ doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds();
+ assertTrue(mController.isHorizontalThinLetterboxed());
+
+ // (task.height() - act.height()) / 2 = 11 > 10
+ doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds();
+ assertFalse(mController.isHorizontalThinLetterboxed());
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_REACHABILITY)
+ public void testAllowReachabilityForThinLetterboxWithFlagEnabled() {
+ spyOn(mController);
+ doReturn(true).when(mController).isVerticalThinLetterboxed();
+ assertFalse(mController.allowVerticalReachabilityForThinLetterbox());
+ doReturn(true).when(mController).isHorizontalThinLetterboxed();
+ assertFalse(mController.allowHorizontalReachabilityForThinLetterbox());
+
+ doReturn(false).when(mController).isVerticalThinLetterboxed();
+ assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
+ doReturn(false).when(mController).isHorizontalThinLetterboxed();
+ assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_REACHABILITY)
+ public void testAllowReachabilityForThinLetterboxWithFlagDisabled() {
+ spyOn(mController);
+ doReturn(true).when(mController).isVerticalThinLetterboxed();
+ assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
+ doReturn(true).when(mController).isHorizontalThinLetterboxed();
+ assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+
+ doReturn(false).when(mController).isVerticalThinLetterboxed();
+ assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
+ doReturn(false).when(mController).isHorizontalThinLetterboxed();
+ assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+ }
+
private void mockThatProperty(String propertyName, boolean value) throws Exception {
Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
/* className */ "");
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 8677738f3edc..6b605ec6d0c0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -3646,11 +3646,27 @@ public class SizeCompatTests extends WindowTestsBase {
}
@Test
+ public void testIsReachabilityEnabled_thisLetterbox_false() {
+ // Case when the reachability would be enabled otherwise
+ setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800);
+ mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+ mActivity.getWindowConfiguration().setBounds(null);
+
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false);
+
+ assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+ assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+ }
+
+ @Test
public void testIsHorizontalReachabilityEnabled_splitScreen_false() {
mAtm.mDevEnableNonResizableMultiWindow = true;
setUpDisplaySizeWithApp(2800, 1000);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
final TestSplitOrganizer organizer =
new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
@@ -3673,6 +3689,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(1000, 2800);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
final TestSplitOrganizer organizer =
new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
@@ -3694,6 +3711,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(1000, 2800);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
// Unresizable landscape-only activity.
prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE);
@@ -3715,6 +3733,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
@@ -3731,6 +3750,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(/* dw */ 2800, /* dh */ 1000);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
@@ -3747,6 +3767,7 @@ public class SizeCompatTests extends WindowTestsBase {
// Portrait display
setUpDisplaySizeWithApp(1400, 1600);
mActivity.mWmService.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
// 16:9f unresizable portrait app
prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
@@ -3760,6 +3781,7 @@ public class SizeCompatTests extends WindowTestsBase {
// Landscape display
setUpDisplaySizeWithApp(1600, 1500);
mActivity.mWmService.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
// 16:9f unresizable landscape app
prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
@@ -3773,6 +3795,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(2800, 1000);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
// Unresizable portrait-only activity.
prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT);
@@ -3794,6 +3817,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(1800, 2200);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
// Unresizable portrait-only activity.
prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
@@ -3815,6 +3839,7 @@ public class SizeCompatTests extends WindowTestsBase {
setUpDisplaySizeWithApp(2200, 1800);
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+ setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
// Unresizable landscape-only activity.
prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
@@ -5011,6 +5036,14 @@ public class SizeCompatTests extends WindowTestsBase {
assertFalse(activity.shouldSendCompatFakeFocus());
}
+ private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) {
+ spyOn(mActivity.mLetterboxUiController);
+ doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController)
+ .allowVerticalReachabilityForThinLetterbox();
+ doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController)
+ .allowHorizontalReachabilityForThinLetterbox();
+ }
+
private int getExpectedSplitSize(int dimensionToSplit) {
int dividerWindowWidth =
mActivity.mWmService.mContext.getResources().getDimensionPixelSize(
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 83e4151235ea..afa669807c2e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -487,6 +487,16 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
// Flush EVENT_APPEARED.
mController.dispatchPendingEvents();
+ // Even if the activity is not launched in an organized TaskFragment, it is still considered
+ // as the remote activity to the organizer process. Because when the task becomes visible,
+ // the organizer process needs to be interactive (unfrozen) to receive TaskFragment events.
+ activity.setVisibleRequested(true);
+ activity.setState(ActivityRecord.State.RESUMED, "test");
+ assertTrue(organizerProc.hasVisibleActivities());
+ activity.setVisibleRequested(false);
+ activity.setState(ActivityRecord.State.STOPPED, "test");
+ assertFalse(organizerProc.hasVisibleActivities());
+
// Make sure the activity belongs to the same app, but it is in a different pid.
activity.info.applicationInfo.uid = uid;
doReturn(pid + 1).when(activity).getPid();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 9d14290bdd8a..2e93cba80386 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -654,7 +654,10 @@ public class UsageStatsService extends SystemService implements
}
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
if (userId >= 0) {
- mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget();
+ if (!Flags.disableIdleCheck() || userId > 0) {
+ // Don't check idle state for USER_SYSTEM during the boot up.
+ mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget();
+ }
}
}
}
@@ -2013,6 +2016,8 @@ public class UsageStatsService extends SystemService implements
+ ": " + Flags.useParceledList());
pw.println(" " + Flags.FLAG_FILTER_BASED_EVENT_QUERY_API
+ ": " + Flags.filterBasedEventQueryApi());
+ pw.println(" " + Flags.FLAG_DISABLE_IDLE_CHECK
+ + ": " + Flags.disableIdleCheck());
final int[] userIds;
synchronized (mLock) {
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index 40537c85784d..6d53c274cdf4 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -16,6 +16,8 @@
package com.android.server.usb;
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
+
import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
import android.Manifest;
@@ -43,6 +45,7 @@ import android.os.AsyncTask;
import android.os.Environment;
import android.os.UserHandle;
import android.os.UserManager;
+import android.provider.Settings;
import android.service.usb.UsbProfileGroupSettingsManagerProto;
import android.service.usb.UsbSettingsAccessoryPreferenceProto;
import android.service.usb.UsbSettingsDevicePreferenceProto;
@@ -939,13 +942,24 @@ public class UsbProfileGroupSettingsManager {
}
/**
- * @return true if any application in foreground have set restrict_usb_overlay_activities as
- * true in manifest file. The application needs to have MANAGE_USB permission.
+ * @return true if the user has not finished the setup process or if there are any
+ * foreground applications with MANAGE_USB permission and restrict_usb_overlay_activities
+ * enabled in the manifest file.
*/
private boolean shouldRestrictOverlayActivities() {
if (!Flags.allowRestrictionOfOverlayActivities()) return false;
+ if (Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ USER_SETUP_COMPLETE,
+ /* defaultValue= */ 1,
+ UserHandle.CURRENT.getIdentifier())
+ == 0) {
+ Slog.d(TAG, "restricting usb overlay activities as setup is not complete");
+ return true;
+ }
+
List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager
.getRunningAppProcesses();
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 0fc9d6ff1501..3b9ee807077e 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -28,14 +28,11 @@ import android.os.InputEventInjectionSync
import android.os.SystemClock
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
-import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.provider.Settings
import android.view.View.OnKeyListener
-import android.view.Display
import android.view.InputDevice
import android.view.KeyEvent
-import android.view.PointerIcon
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.test.mock.MockContentResolver
@@ -44,7 +41,6 @@ import com.android.internal.util.test.FakeSettingsProvider
import com.google.common.truth.Truth.assertThat
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import org.junit.After
-import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -53,22 +49,16 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.`when`
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.junit.MockitoJUnit
import org.mockito.stubbing.OngoingStubbing
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
/**
* Tests for {@link InputManagerService}.
@@ -187,197 +177,6 @@ class InputManagerServiceTests {
verify(wmCallbacks).notifyPointerDisplayIdChanged(displayId, x, y)
}
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun testSetVirtualMousePointerDisplayId() {
- // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
- // until the native callback happens.
- var countDownLatch = CountDownLatch(1)
- val overrideDisplayId = 123
- Thread {
- assertTrue("Setting virtual pointer display should succeed",
- localService.setVirtualMousePointerDisplayId(overrideDisplayId))
- countDownLatch.countDown()
- }.start()
- assertFalse("Setting virtual pointer display should block",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
- val x = 42f
- val y = 314f
- service.onPointerDisplayIdChanged(overrideDisplayId, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, x, y)
- assertTrue("Native callback unblocks calling thread",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native).setPointerDisplayId(overrideDisplayId)
-
- // Ensure that setting the same override again succeeds immediately.
- assertTrue("Setting the same virtual mouse pointer displayId again should succeed",
- localService.setVirtualMousePointerDisplayId(overrideDisplayId))
-
- // Ensure that we did not query WM for the pointerDisplayId when setting the override
- verify(wmCallbacks, never()).pointerDisplayId
-
- // Unset the virtual mouse pointer displayId, and ensure that we query WM for the new
- // pointer displayId and the calling thread is blocked until the native callback happens.
- countDownLatch = CountDownLatch(1)
- val pointerDisplayId = 42
- `when`(wmCallbacks.pointerDisplayId).thenReturn(pointerDisplayId)
- Thread {
- assertTrue("Unsetting virtual mouse pointer displayId should succeed",
- localService.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY))
- countDownLatch.countDown()
- }.start()
- assertFalse("Unsetting virtual mouse pointer displayId should block",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
- service.onPointerDisplayIdChanged(pointerDisplayId, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(pointerDisplayId, x, y)
- assertTrue("Native callback unblocks calling thread",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native).setPointerDisplayId(pointerDisplayId)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() {
- // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
- // until the native callback happens.
- val countDownLatch = CountDownLatch(1)
- val overrideDisplayId = 123
- Thread {
- assertFalse("Setting virtual pointer display should be unsuccessful",
- localService.setVirtualMousePointerDisplayId(overrideDisplayId))
- countDownLatch.countDown()
- }.start()
- assertFalse("Setting virtual pointer display should block",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
- val x = 42f
- val y = 314f
- // Assume the native callback updates the pointerDisplayId to the incorrect value.
- service.onPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
- assertTrue("Native callback unblocks calling thread",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native).setPointerDisplayId(overrideDisplayId)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun testSetVirtualMousePointerDisplayId_competingRequests() {
- val firstRequestSyncLatch = CountDownLatch(1)
- doAnswer {
- firstRequestSyncLatch.countDown()
- }.`when`(native).setPointerDisplayId(anyInt())
-
- val firstRequestLatch = CountDownLatch(1)
- val firstOverride = 123
- Thread {
- assertFalse("Setting virtual pointer display from thread 1 should be unsuccessful",
- localService.setVirtualMousePointerDisplayId(firstOverride))
- firstRequestLatch.countDown()
- }.start()
- assertFalse("Setting virtual pointer display should block",
- firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
-
- assertTrue("Wait for first thread's request should succeed",
- firstRequestSyncLatch.await(100, TimeUnit.MILLISECONDS))
-
- val secondRequestLatch = CountDownLatch(1)
- val secondOverride = 42
- Thread {
- assertTrue("Setting virtual mouse pointer from thread 2 should be successful",
- localService.setVirtualMousePointerDisplayId(secondOverride))
- secondRequestLatch.countDown()
- }.start()
- assertFalse("Setting virtual mouse pointer should block",
- secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
-
- val x = 42f
- val y = 314f
- // Assume the native callback updates directly to the second request.
- service.onPointerDisplayIdChanged(secondOverride, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(secondOverride, x, y)
- assertTrue("Native callback unblocks first thread",
- firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
- assertTrue("Native callback unblocks second thread",
- secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native, times(2)).setPointerDisplayId(anyInt())
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun onDisplayRemoved_resetAllAdditionalInputProperties() {
- setVirtualMousePointerDisplayIdAndVerify(10)
-
- localService.setPointerIconVisible(false, 10)
- verify(native).setPointerIconVisibility(10, false)
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- localService.setMousePointerAccelerationEnabled(false, 10)
- verify(native).setMousePointerAccelerationEnabled(10, false)
-
- service.onDisplayRemoved(10)
- verify(native).setPointerIconVisibility(10, true)
- verify(native).displayRemoved(eq(10))
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
- verify(native).setMousePointerAccelerationEnabled(10, true)
- verifyNoMoreInteractions(native)
-
- // This call should not block because the virtual mouse pointer override was never removed.
- localService.setVirtualMousePointerDisplayId(10)
-
- verify(native).setPointerDisplayId(eq(10))
- verifyNoMoreInteractions(native)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun updateAdditionalInputPropertiesForOverrideDisplay() {
- setVirtualMousePointerDisplayIdAndVerify(10)
-
- localService.setPointerIconVisible(false, 10)
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- verify(native).setPointerIconVisibility(10, false)
- localService.setMousePointerAccelerationEnabled(false, 10)
- verify(native).setMousePointerAccelerationEnabled(10, false)
-
- localService.setPointerIconVisible(true, 10)
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
- verify(native).setPointerIconVisibility(10, true)
- localService.setMousePointerAccelerationEnabled(true, 10)
- verify(native).setMousePointerAccelerationEnabled(10, true)
-
- localService.setPointerIconVisible(false, 20)
- verify(native).setPointerIconVisibility(20, false)
- localService.setMousePointerAccelerationEnabled(false, 20)
- verify(native).setMousePointerAccelerationEnabled(20, false)
- verifyNoMoreInteractions(native)
-
- clearInvocations(native)
- setVirtualMousePointerDisplayIdAndVerify(20)
-
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun setAdditionalInputPropertiesBeforeOverride() {
- localService.setPointerIconVisible(false, 10)
- localService.setMousePointerAccelerationEnabled(false, 10)
-
- verify(native).setPointerIconVisibility(10, false)
- verify(native).setMousePointerAccelerationEnabled(10, false)
- verifyNoMoreInteractions(native)
-
- setVirtualMousePointerDisplayIdAndVerify(10)
-
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- }
-
@Test
fun setDeviceTypeAssociation_setsDeviceTypeAssociation() {
val inputPort = "inputPort"
@@ -412,20 +211,6 @@ class InputManagerServiceTests {
verify(native, times(2)).changeKeyboardLayoutAssociation()
}
- private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) {
- val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) }
- thread.start()
-
- // Allow some time for the set override call to park while waiting for the native callback.
- Thread.sleep(100 /*millis*/)
- verify(native).setPointerDisplayId(overrideDisplayId)
-
- service.onPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
- thread.join(100 /*millis*/)
- }
-
private fun createVirtualDisplays(count: Int): List<VirtualDisplay> {
val displayManager: DisplayManager = context.getSystemService(
DisplayManager::class.java
diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp
index f0bea3f3c28a..2909e66b53be 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -43,6 +43,9 @@ android_test {
"libmultiplejvmtiagentsinterferenceagent",
"libstaticjvmtiagent",
],
+ libs: [
+ "android.test.mock",
+ ],
certificate: "platform",
platform_apis: true,
test_suites: ["device-tests"],
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
index 4780d8a610e8..87b26a63acc7 100644
--- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.usbtest;
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
+
import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES;
import static org.mockito.ArgumentMatchers.any;
@@ -32,16 +34,20 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManager.Property;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.hardware.usb.UsbDevice;
import android.os.UserHandle;
import android.os.UserManager;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.usb.UsbHandlerManager;
import com.android.server.usb.UsbProfileGroupSettingsManager;
import com.android.server.usb.UsbSettingsManager;
@@ -69,6 +75,7 @@ import java.util.List;
public class UsbProfileGroupSettingsManagerTest {
private static final String TEST_PACKAGE_NAME = "testPkg";
+
@Mock
private Context mContext;
@Mock
@@ -85,43 +92,78 @@ public class UsbProfileGroupSettingsManagerTest {
private UserManager mUserManager;
@Mock
private UsbUserSettingsManager mUsbUserSettingsManager;
- @Mock private Property mProperty;
- private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo;
- private PackageInfo mPackageInfo;
- private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+ @Mock
+ private Property mRestrictUsbOverlayActivitiesProperty;
+ @Mock
+ private UsbDevice mUsbDevice;
+
+ private MockContentResolver mContentResolver;
private MockitoSession mStaticMockSession;
+ private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ mStaticMockSession = ExtendedMockito.mockitoSession()
+ .mockStatic(Flags.class)
+ .strictness(Strictness.WARN)
+ .startMocking();
- mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo();
- mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME};
- mPackageInfo = new PackageInfo();
- mPackageInfo.packageName = TEST_PACKAGE_NAME;
- mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+ when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
+ when(mUserManager.getEnabledProfiles(anyInt()))
+ .thenReturn(List.of(Mockito.mock(UserInfo.class)));
+
+ mContentResolver = new MockContentResolver();
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ when(mContext.getContentResolver()).thenReturn(mContentResolver);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
- when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+ when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+ when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class)))
.thenReturn(mContext);
- when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
- mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle,
- mUsbSettingsManager, mUsbHandlerManager);
+ mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(
+ mContext, mUserHandle, mUsbSettingsManager, mUsbHandlerManager);
- mStaticMockSession = ExtendedMockito.mockitoSession()
- .mockStatic(Flags.class)
- .strictness(Strictness.WARN)
- .startMocking();
+ setupDefaultConfiguration();
+ }
+ /**
+ * Setups the following configuration
+ *
+ * <ul>
+ * <li>Flag is enabled
+ * <li>Device setup has completed
+ * <li>There is a foreground activity with MANAGE_USB permission
+ * <li>The foreground activity has PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES enabled
+ * </ul>
+ */
+ private void setupDefaultConfiguration() throws NameNotFoundException {
+ when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+
+ Settings.Secure.putInt(mContentResolver, USER_SETUP_COMPLETE, 1);
+
+ ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo =
+ new ActivityManager.RunningAppProcessInfo();
+ mRunningAppProcessInfo.pkgList = new String[] { TEST_PACKAGE_NAME };
+ when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+
+ PackageInfo mPackageInfo = new PackageInfo();
+ mPackageInfo.packageName = TEST_PACKAGE_NAME;
+ mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo);
- when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES),
- eq(TEST_PACKAGE_NAME))).thenReturn(mProperty);
- when(mUserManager.getEnabledProfiles(anyInt()))
- .thenReturn(List.of(Mockito.mock(UserInfo.class)));
- when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
+ when(mPackageManager.getPackagesHoldingPermissions(
+ new String[] { android.Manifest.permission.MANAGE_USB },
+ PackageManager.MATCH_SYSTEM_ONLY))
+ .thenReturn(List.of(mPackageInfo));
+
+ when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(true);
+ when(mPackageManager.getProperty(
+ eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES), eq(TEST_PACKAGE_NAME)))
+ .thenReturn(mRestrictUsbOverlayActivitiesProperty);
}
@After
@@ -130,66 +172,59 @@ public class UsbProfileGroupSettingsManagerTest {
}
@Test
- public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() {
- when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+ public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
+ mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
+ verify(mUsbUserSettingsManager, times(0)).queryIntentActivities(any(Intent.class));
+ }
+
+ @Test
+ public void testDeviceAttached_noForegroundActivity_resolveActivityCalled() {
when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>());
- when(mPackageManager.getPackagesHoldingPermissions(
- new String[]{android.Manifest.permission.MANAGE_USB},
- PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
- UsbDevice device = Mockito.mock(UsbDevice.class);
- mUsbProfileGroupSettingsManager.deviceAttached(device);
+
+ mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
}
@Test
public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() {
- when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
- when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
when(mPackageManager.getPackagesHoldingPermissions(
- new String[]{android.Manifest.permission.MANAGE_USB},
- PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>());
- UsbDevice device = Mockito.mock(UsbDevice.class);
- mUsbProfileGroupSettingsManager.deviceAttached(device);
+ new String[] { android.Manifest.permission.MANAGE_USB },
+ PackageManager.MATCH_SYSTEM_ONLY))
+ .thenReturn(new ArrayList<>());
+
+ mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
}
@Test
- public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
- when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
- when(mProperty.getBoolean()).thenReturn(true);
- when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
- when(mPackageManager.getPackagesHoldingPermissions(
- new String[]{android.Manifest.permission.MANAGE_USB},
- PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
- UsbDevice device = Mockito.mock(UsbDevice.class);
- mUsbProfileGroupSettingsManager.deviceAttached(device);
- verify(mUsbUserSettingsManager, times(0))
- .queryIntentActivities(any(Intent.class));
- }
+ public void testDeviceAttached_restricUsbOverlayPropertyDisabled_resolveActivityCalled() {
+ when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(false);
+
+ mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
- @Test
- public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() {
- when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
- when(mProperty.getBoolean()).thenReturn(false);
- when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
- when(mPackageManager.getPackagesHoldingPermissions(
- new String[]{android.Manifest.permission.MANAGE_USB},
- PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
- UsbDevice device = Mockito.mock(UsbDevice.class);
- mUsbProfileGroupSettingsManager.deviceAttached(device);
verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
}
@Test
- public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() {
+ public void testDeviceAttached_flagFalse_resolveActivityCalled() {
when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false);
- when(mProperty.getBoolean()).thenReturn(true);
- when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
- when(mPackageManager.getPackagesHoldingPermissions(
- new String[]{android.Manifest.permission.MANAGE_USB},
- PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
- UsbDevice device = Mockito.mock(UsbDevice.class);
- mUsbProfileGroupSettingsManager.deviceAttached(device);
+
+ mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
}
+
+ @Test
+ public void
+ testDeviceAttached_setupNotCompleteAndNoBlockingActivities_resolveActivityNotCalled() {
+ when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(false);
+ Settings.Secure.putInt(mContentResolver, USER_SETUP_COMPLETE, 0);
+
+ mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
+ verify(mUsbUserSettingsManager, times(0)).queryIntentActivities(any(Intent.class));
+ }
}
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 960b57cb632a..580efe126ea3 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -70,6 +70,7 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.Uri;
+import android.net.vcn.Flags;
import android.net.vcn.IVcnStatusCallback;
import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
import android.net.vcn.VcnConfig;
@@ -82,7 +83,9 @@ import android.os.ParcelUuid;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -101,6 +104,7 @@ import com.android.server.vcn.util.PersistableBundleUtils;
import com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -118,6 +122,8 @@ import java.util.UUID;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnManagementServiceTest {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final String CONTEXT_ATTRIBUTION_TAG = "VCN";
private static final String TEST_PACKAGE_NAME =
VcnManagementServiceTest.class.getPackage().getName();
@@ -129,7 +135,12 @@ public class VcnManagementServiceTest {
private static final ParcelUuid TEST_UUID_3 = new ParcelUuid(new UUID(2, 2));
private static final VcnConfig TEST_VCN_CONFIG;
private static final VcnConfig TEST_VCN_CONFIG_PKG_2;
- private static final int TEST_UID = Process.FIRST_APPLICATION_UID;
+
+ private static final int TEST_UID = 1010000; // A non-system user
+ private static final UserHandle TEST_USER_HANDLE = UserHandle.getUserHandleForUid(TEST_UID);
+ private static final UserHandle TEST_USER_HANDLE_OTHER =
+ UserHandle.of(TEST_USER_HANDLE.getIdentifier() + 1);
+
private static final String TEST_IFACE_NAME = "TEST_IFACE";
private static final String TEST_IFACE_NAME_2 = "TEST_IFACE2";
private static final LinkProperties TEST_LP_1 = new LinkProperties();
@@ -187,6 +198,7 @@ public class VcnManagementServiceTest {
private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
private final AppOpsManager mAppOpsMgr = mock(AppOpsManager.class);
+ private final UserManager mUserManager = mock(UserManager.class);
private final VcnContext mVcnContext = mock(VcnContext.class);
private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper =
mock(PersistableBundleUtils.LockingReadWriteHelper.class);
@@ -218,6 +230,9 @@ public class VcnManagementServiceTest {
Context.TELEPHONY_SUBSCRIPTION_SERVICE,
SubscriptionManager.class);
setupSystemService(mMockContext, mAppOpsMgr, Context.APP_OPS_SERVICE, AppOpsManager.class);
+ setupSystemService(mMockContext, mUserManager, Context.USER_SERVICE, UserManager.class);
+
+ doReturn(TEST_USER_HANDLE).when(mUserManager).getMainUser();
doReturn(TEST_PACKAGE_NAME).when(mMockContext).getOpPackageName();
@@ -267,6 +282,8 @@ public class VcnManagementServiceTest {
@Before
public void setUp() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_MAIN_USER);
+
doNothing()
.when(mMockContext)
.enforceCallingOrSelfPermission(
@@ -717,10 +734,8 @@ public class VcnManagementServiceTest {
}
@Test
- public void testSetVcnConfigRequiresSystemUser() throws Exception {
- doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
- .when(mMockDeps)
- .getBinderCallingUid();
+ public void testSetVcnConfigRequiresMainUser() throws Exception {
+ doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser();
try {
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
@@ -832,10 +847,8 @@ public class VcnManagementServiceTest {
}
@Test
- public void testClearVcnConfigRequiresSystemUser() throws Exception {
- doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
- .when(mMockDeps)
- .getBinderCallingUid();
+ public void testClearVcnConfigRequiresMainUser() throws Exception {
+ doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser();
try {
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1, TEST_PACKAGE_NAME);
@@ -921,10 +934,8 @@ public class VcnManagementServiceTest {
}
@Test
- public void testGetConfiguredSubscriptionGroupsRequiresSystemUser() throws Exception {
- doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
- .when(mMockDeps)
- .getBinderCallingUid();
+ public void testGetConfiguredSubscriptionGroupsRequiresMainUser() throws Exception {
+ doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser();
try {
mVcnMgmtSvc.getConfiguredSubscriptionGroups(TEST_PACKAGE_NAME);
diff --git a/tools/hoststubgen/TEST_MAPPING b/tools/hoststubgen/TEST_MAPPING
index f6885e1e74ba..856e6eefba15 100644
--- a/tools/hoststubgen/TEST_MAPPING
+++ b/tools/hoststubgen/TEST_MAPPING
@@ -1,63 +1,7 @@
-// Keep the following two TEST_MAPPINGs in sync:
-// frameworks/base/ravenwood/TEST_MAPPING
-// frameworks/base/tools/hoststubgen/TEST_MAPPING
{
- "presubmit": [
- { "name": "tiny-framework-dump-test" },
- { "name": "hoststubgentest" },
- { "name": "hoststubgen-invoke-test" },
+ "imports": [
{
- "name": "RavenwoodMockitoTest_device"
- },
- {
- "name": "RavenwoodBivalentTest_device"
- },
- // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
- {
- "name": "SystemUIGoogleTests",
- "options": [
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- }
- ],
- "presubmit-large": [
- {
- "name": "SystemUITests",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
- ],
- "ravenwood-presubmit": [
- {
- "name": "RavenwoodMinimumTest",
- "host": true
- },
- {
- "name": "RavenwoodMockitoTest",
- "host": true
- },
- {
- "name": "CtsUtilTestCasesRavenwood",
- "host": true
- },
- {
- "name": "RavenwoodCoreTest",
- "host": true
- },
- {
- "name": "RavenwoodBivalentTest",
- "host": true
+ "path": "frameworks/base/ravenwood"
}
]
}
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index 3c734bc7e1e3..c5bc0396de34 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -9,3 +9,11 @@ flag {
bug: "313038031"
is_fixed_read_only: true
}
+
+flag {
+ name: "network_provider_battery_charging_status"
+ is_exported: true
+ namespace: "wifi"
+ description: "Control the API that allows setting / reading the NetworkProviderInfo's battery charging status"
+ bug: "305067231"
+}