summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp13
-rw-r--r--Android.bp1
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java6
-rw-r--r--api/OWNERS2
-rwxr-xr-xcmds/am/am.sh5
-rw-r--r--cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java17
-rw-r--r--cmds/bootanimation/BootAnimation.cpp2
-rw-r--r--cmds/uinput/tests/Android.bp14
-rw-r--r--core/api/system-current.txt14
-rw-r--r--core/api/test-current.txt2
-rw-r--r--core/java/android/accessibilityservice/OWNERS5
-rw-r--r--core/java/android/app/ActivityOptions.java10
-rw-r--r--core/java/android/app/ApplicationPackageManager.java23
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java32
-rw-r--r--core/java/android/app/appfunctions/AppFunctionManager.java23
-rw-r--r--core/java/android/app/appfunctions/AppFunctionManagerHelper.java17
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java27
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceParams.java24
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCamera.java3
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraCallback.java3
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraConfig.java4
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java3
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig22
-rw-r--r--core/java/android/content/pm/PackageManager.java31
-rw-r--r--core/java/android/hardware/Camera.java3
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java13
-rw-r--r--core/java/android/hardware/display/DisplayManager.java10
-rw-r--r--core/java/android/hardware/display/DisplayManagerGlobal.java18
-rw-r--r--core/java/android/hardware/input/input_framework.aconfig9
-rw-r--r--core/java/android/os/BaseBundle.java11
-rw-r--r--core/java/android/os/Bundle.java36
-rw-r--r--core/java/android/os/Parcel.java65
-rw-r--r--core/java/android/os/health/SystemHealthManager.java30
-rw-r--r--core/java/android/os/storage/StorageManager.java266
-rw-r--r--core/java/android/view/InputEventConsistencyVerifier.java2
-rw-r--r--core/java/android/view/InputEventReceiver.java8
-rw-r--r--core/java/android/view/InsetsSourceConsumer.java6
-rw-r--r--core/java/android/view/RoundScrollbarRenderer.java43
-rw-r--r--core/java/android/view/ViewRootImpl.java19
-rw-r--r--core/java/android/view/WindowManager.java10
-rw-r--r--core/java/android/view/accessibility/OWNERS5
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java28
-rw-r--r--core/java/android/window/DesktopModeFlags.java1
-rw-r--r--core/java/android/window/TransitionInfo.java9
-rw-r--r--core/java/android/window/WindowTokenClient.java1
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig30
-rw-r--r--core/java/com/android/internal/accessibility/OWNERS5
-rw-r--r--core/java/com/android/internal/app/IBatteryStats.aidl12
-rw-r--r--core/java/com/android/internal/graphics/palette/OWNERS5
-rw-r--r--core/java/com/android/internal/jank/Cuj.java15
-rw-r--r--core/java/com/android/internal/policy/DecorView.java85
-rw-r--r--core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java10
-rw-r--r--core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java24
-rw-r--r--core/java/com/android/internal/protolog/WmProtoLogGroups.java2
-rw-r--r--core/jni/android_view_DisplayEventReceiver.cpp4
-rw-r--r--core/res/AndroidManifest.xml4
-rw-r--r--core/res/res/values-w225dp/dimens.xml20
-rw-r--r--core/res/res/values-watch/config.xml4
-rw-r--r--core/res/res/values/config.xml30
-rw-r--r--core/res/res/values/dimens.xml3
-rw-r--r--core/res/res/values/symbols.xml15
-rw-r--r--core/tests/coretests/src/android/content/IntentTest.java39
-rw-r--r--core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java6
-rw-r--r--core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java18
-rw-r--r--graphics/java/android/graphics/Bitmap.java4
-rw-r--r--libs/WindowManager/Shell/OWNERS4
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig7
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml118
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml29
-rw-r--r--libs/WindowManager/Shell/shared/res/values/dimen.xml19
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt90
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt78
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java29
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java115
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java73
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java50
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt16
-rw-r--r--libs/WindowManager/Shell/tests/OWNERS1
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java36
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt276
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt34
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java66
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt299
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt191
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt5
-rw-r--r--libs/hwui/OWNERS1
-rw-r--r--libs/hwui/hwui/Bitmap.cpp6
-rw-r--r--libs/hwui/hwui/Bitmap.h8
-rw-r--r--libs/hwui/jni/Bitmap.cpp26
-rw-r--r--libs/hwui/jni/Bitmap.h7
-rw-r--r--libs/hwui/jni/ScopedParcel.cpp10
-rw-r--r--libs/hwui/jni/ScopedParcel.h4
-rw-r--r--libs/input/PointerControllerContext.cpp3
-rw-r--r--media/java/android/media/audiofx/HapticGenerator.java14
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig10
-rw-r--r--media/jni/android_media_MediaDrm.cpp7
-rw-r--r--packages/EasterEgg/AndroidManifest.xml2
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_large.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_medium.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_small.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_planet_tiny.xml27
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft.xml44
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml45
-rw-r--r--packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml21
-rw-r--r--packages/EasterEgg/res/values/themes.xml23
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt20
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt46
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt10
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt208
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/Namer.kt26
-rw-r--r--packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt187
-rw-r--r--packages/SettingsLib/Graph/graph.proto2
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt96
-rw-r--r--packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt11
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt54
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt2
-rw-r--r--packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt55
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java1
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt18
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java34
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java8
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt64
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java37
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java8
-rw-r--r--packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt2
-rw-r--r--packages/Shell/src/com/android/shell/BugreportPrefs.java29
-rw-r--r--packages/Shell/src/com/android/shell/BugreportProgressService.java8
-rw-r--r--packages/Shell/src/com/android/shell/BugreportWarningActivity.java23
-rw-r--r--packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java37
-rw-r--r--packages/SystemUI/Android.bp10
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig24
-rw-r--r--packages/SystemUI/compose/core/Android.bp5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt18
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt8
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt12
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt8
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt11
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt4
-rw-r--r--packages/SystemUI/compose/scene/Android.bp5
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt9
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt84
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt57
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt85
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt100
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt47
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt94
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt42
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java)27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt115
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt328
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt196
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt274
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt322
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java1189
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt910
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt60
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt138
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt142
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt261
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt3
-rw-r--r--packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml20
-rw-r--r--packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml4
-rw-r--r--packages/SystemUI/res/layout/clipboard_overlay.xml8
-rw-r--r--packages/SystemUI/res/layout/volume_dialog.xml2
-rw-r--r--packages/SystemUI/res/layout/volume_ringer_button.xml25
-rw-r--r--packages/SystemUI/res/values/styles.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/SwipeHelper.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/OWNERS5
-rw-r--r--packages/SystemUI/src/com/android/systemui/ailabs/OWNERS1
-rw-r--r--packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt89
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java31
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java234
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java89
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt68
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt175
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java25
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/stylus/OWNERS3
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt153
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt243
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt8
-rw-r--r--packages/SystemUI/tests/res/layout/custom_view_flipper.xml14
-rw-r--r--packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java166
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java57
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java285
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt94
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java4
-rw-r--r--packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt20
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt54
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt39
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt113
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt14
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt)16
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt2
-rw-r--r--services/accessibility/OWNERS5
-rw-r--r--services/accessibility/accessibility.aconfig7
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java5
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java42
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/OWNERS8
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java5
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java198
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java13
-rw-r--r--services/core/Android.bp11
-rw-r--r--services/core/java/com/android/server/StorageManagerService.java247
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java3
-rw-r--r--services/core/java/com/android/server/am/BatteryStatsService.java15
-rw-r--r--services/core/java/com/android/server/am/UserController.java38
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java41
-rw-r--r--services/core/java/com/android/server/backup/InputBackupHelper.java82
-rw-r--r--services/core/java/com/android/server/backup/SystemBackupAgent.java6
-rw-r--r--services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java13
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java12
-rw-r--r--services/core/java/com/android/server/display/LocalDisplayAdapter.java22
-rw-r--r--services/core/java/com/android/server/display/feature/DisplayManagerFlags.java10
-rw-r--r--services/core/java/com/android/server/display/feature/display_flags.aconfig8
-rw-r--r--services/core/java/com/android/server/display/mode/DisplayModeDirector.java10
-rw-r--r--services/core/java/com/android/server/display/mode/VotesStatsReporter.java25
-rw-r--r--services/core/java/com/android/server/input/InputDataStore.java59
-rw-r--r--services/core/java/com/android/server/input/InputManagerInternal.java32
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java23
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java28
-rw-r--r--services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java36
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java44
-rw-r--r--services/core/java/com/android/server/notification/NotificationChannelExtractor.java2
-rw-r--r--services/core/java/com/android/server/pm/AppsFilterImpl.java5
-rw-r--r--services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java110
-rw-r--r--services/core/java/com/android/server/security/CertificateRevocationStatusManager.java366
-rw-r--r--services/core/java/com/android/server/security/OWNERS1
-rw-r--r--services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java81
-rw-r--r--services/core/java/com/android/server/storage/ImmutableVolumeInfo.java139
-rw-r--r--services/core/java/com/android/server/storage/StorageSessionController.java30
-rw-r--r--services/core/java/com/android/server/storage/WatchedVolumeInfo.java206
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java22
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java50
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskManagerService.java8
-rw-r--r--services/core/java/com/android/server/wm/AppWarnings.java4
-rw-r--r--services/core/java/com/android/server/wm/BackNavigationController.java48
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeHelper.java19
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java57
-rw-r--r--services/core/java/com/android/server/wm/EmbeddedWindowController.java15
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java10
-rw-r--r--services/core/java/com/android/server/wm/InsetsPolicy.java20
-rw-r--r--services/core/java/com/android/server/wm/InsetsStateController.java17
-rw-r--r--services/core/java/com/android/server/wm/KeyguardController.java24
-rw-r--r--services/core/java/com/android/server/wm/PresentationController.java86
-rw-r--r--services/core/java/com/android/server/wm/Session.java10
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java2
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java1
-rw-r--r--services/core/java/com/android/server/wm/Transition.java5
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java30
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java25
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java4
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java32
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java20
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java65
-rw-r--r--services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java56
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java64
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java2
-rw-r--r--services/tests/mockingservicestests/AndroidManifest.xml10
-rw-r--r--services/tests/mockingservicestests/res/xml/test_wallpaper.xml19
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java3
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java111
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java14
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java26
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java120
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/OWNERS5
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java90
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java25
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS6
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java19
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java33
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java180
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java14
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java9
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java87
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java35
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java4
-rw-r--r--telephony/java/android/telephony/SubscriptionManager.java6
-rw-r--r--telephony/java/com/android/internal/telephony/ISub.aidl6
-rw-r--r--tests/AttestationVerificationTest/AndroidManifest.xml2
-rw-r--r--tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json12
-rw-r--r--tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json16
-rw-r--r--tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java303
-rw-r--r--tests/FlickerTests/IME/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Rotation/AndroidTestTemplate.xml2
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt99
-rw-r--r--tools/aapt2/cmd/Command.cpp24
-rw-r--r--tools/aapt2/cmd/Command_test.cpp18
-rw-r--r--tools/aapt2/cmd/Convert.cpp3
-rw-r--r--tools/aapt2/cmd/Convert.h9
-rw-r--r--tools/aapt2/cmd/Link.cpp3
-rw-r--r--tools/aapt2/cmd/Link.h9
-rw-r--r--tools/aapt2/cmd/Optimize.cpp3
-rw-r--r--tools/aapt2/cmd/Optimize.h11
-rw-r--r--tools/aapt2/format/binary/TableFlattener.cpp2
-rw-r--r--tools/aapt2/format/binary/TableFlattener.h5
-rw-r--r--tools/aapt2/format/binary/TableFlattener_test.cpp12
-rw-r--r--tools/aapt2/readme.md2
432 files changed, 11429 insertions, 5518 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 071cc6555be7..e5c059ecbfb7 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -84,7 +84,7 @@ aconfig_declarations_group {
"android.view.inputmethod.flags-aconfig-java",
"android.webkit.flags-aconfig-java",
"android.widget.flags-aconfig-java",
- "android.xr.flags-aconfig-java",
+ "android.xr.flags-aconfig-java-export",
"art_exported_aconfig_flags_lib",
"backstage_power_flags_lib",
"backup_flags_lib",
@@ -989,15 +989,22 @@ java_aconfig_library {
// XR
aconfig_declarations {
name: "android.xr.flags-aconfig",
- package: "android.xr",
container: "system",
+ exportable: true,
+ package: "android.xr",
srcs: ["core/java/android/content/pm/xr.aconfig"],
}
java_aconfig_library {
- name: "android.xr.flags-aconfig-java",
+ name: "android.xr.flags-aconfig-java-export",
aconfig_declarations: "android.xr.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ min_sdk_version: "30",
+ mode: "exported",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.permission",
+ ],
}
// android.app
diff --git a/Android.bp b/Android.bp
index 9d3b64d7335b..303fa2cd18da 100644
--- a/Android.bp
+++ b/Android.bp
@@ -583,6 +583,7 @@ java_library {
"documents-ui-compat-config",
"calendar-provider-compat-config",
"contacts-provider-platform-compat-config",
+ "SystemUI-core-compat-config",
] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), {
"true": [],
default: [
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 9871d713178e..ab8131ba5126 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -264,6 +264,8 @@ public class AppStandbyController
@GuardedBy("mCarrierPrivilegedLock")
private boolean mHaveCarrierPrivilegedApps;
+ private final boolean mHasFeatureTelephonySubscription;
+
/** List of carrier-privileged apps that should be excluded from standby */
@GuardedBy("mCarrierPrivilegedLock")
private List<String> mCarrierPrivilegedApps;
@@ -603,6 +605,8 @@ public class AppStandbyController
mContext = mInjector.getContext();
mHandler = new AppStandbyHandler(mInjector.getLooper());
mPackageManager = mContext.getPackageManager();
+ mHasFeatureTelephonySubscription = mPackageManager.hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION);
DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver();
IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING);
@@ -1515,7 +1519,7 @@ public class AppStandbyController
}
// Check this last, as it can be the most expensive check
- if (isCarrierApp(packageName)) {
+ if (mHasFeatureTelephonySubscription && isCarrierApp(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
diff --git a/api/OWNERS b/api/OWNERS
index 965093c9ab38..f2bcf13d2d2e 100644
--- a/api/OWNERS
+++ b/api/OWNERS
@@ -9,4 +9,4 @@ per-file *.go,go.mod,go.work,go.work.sum = file:platform/build/soong:/OWNERS
per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
# For metalava team to disable lint checks in platform
-per-file Android.bp = aurimas@google.com,emberrose@google.com
+per-file Android.bp = aurimas@google.com
diff --git a/cmds/am/am.sh b/cmds/am/am.sh
index 76ec214cb446..f099be3e26a2 100755
--- a/cmds/am/am.sh
+++ b/cmds/am/am.sh
@@ -1,11 +1,10 @@
#!/system/bin/sh
-# set to top-app process group
-settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true
-
if [ "$1" != "instrument" ] ; then
cmd activity "$@"
else
+ # set to top-app process group for instrument
+ settaskprofile $$ SCHED_SP_TOP_APP >/dev/null 2>&1 || true
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index 6310d32515c5..696bc82a9ffc 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -18,6 +18,7 @@ package com.android.commands.bmgr;
import android.annotation.IntDef;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.backup.BackupManager;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.BackupProgress;
@@ -73,6 +74,8 @@ public class Bmgr {
"Error: Could not access the backup transport. Is the system running?";
private static final String PM_NOT_RUNNING_ERR =
"Error: Could not access the Package Manager. Is the system running?";
+ private static final String INVALID_USER_ID_ERR_TEMPLATE =
+ "Error: Invalid user id (%d).\n";
private String[] mArgs;
private int mNextArg;
@@ -104,6 +107,11 @@ public class Bmgr {
mArgs = args;
mNextArg = 0;
int userId = parseUserId();
+ if (userId < 0) {
+ System.err.printf(INVALID_USER_ID_ERR_TEMPLATE, userId);
+ return;
+ }
+
String op = nextArg();
Slog.v(TAG, "Running " + op + " for user:" + userId);
@@ -955,12 +963,15 @@ public class Bmgr {
private int parseUserId() {
String arg = nextArg();
- if ("--user".equals(arg)) {
- return UserHandle.parseUserArg(nextArg());
- } else {
+ if (!"--user".equals(arg)) {
mNextArg--;
return UserHandle.USER_SYSTEM;
}
+ int userId = UserHandle.parseUserArg(nextArg());
+ if (userId == UserHandle.USER_CURRENT) {
+ userId = ActivityManager.getCurrentUser();
+ }
+ return userId;
}
private static void showUsage() {
diff --git a/cmds/bootanimation/BootAnimation.cpp b/cmds/bootanimation/BootAnimation.cpp
index b43905b19239..844e52c3ecf2 100644
--- a/cmds/bootanimation/BootAnimation.cpp
+++ b/cmds/bootanimation/BootAnimation.cpp
@@ -441,7 +441,7 @@ public:
numEvents = mBootAnimation->mDisplayEventReceiver->getEvents(buffer, kBufferSize);
for (size_t i = 0; i < static_cast<size_t>(numEvents); i++) {
const auto& event = buffer[i];
- if (event.header.type == DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG) {
+ if (event.header.type == DisplayEventType::DISPLAY_EVENT_HOTPLUG) {
SLOGV("Hotplug received");
if (!event.hotplug.connected) {
diff --git a/cmds/uinput/tests/Android.bp b/cmds/uinput/tests/Android.bp
index e728bd270a46..516de3325f77 100644
--- a/cmds/uinput/tests/Android.bp
+++ b/cmds/uinput/tests/Android.bp
@@ -18,3 +18,17 @@ android_test {
"device-tests",
],
}
+
+android_ravenwood_test {
+ name: "UinputTestsRavenwood",
+ srcs: [
+ "src/**/*.java",
+ ],
+ static_libs: [
+ "androidx.test.runner",
+ "frameworks-base-testutils",
+ "platform-test-annotations",
+ "truth",
+ "uinput",
+ ],
+}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 9a848d423c9a..76cce7439454 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3436,7 +3436,7 @@ package android.companion.virtual {
method public void close();
method @NonNull public android.content.Context createContext();
method @NonNull public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
- method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
+ method @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
method @NonNull public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
@@ -3499,7 +3499,7 @@ package android.companion.virtual {
field public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; // 0x6
- field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
+ field public static final int POLICY_TYPE_CAMERA = 5; // 0x5
field public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
field @FlaggedApi("android.companion.virtualdevice.flags.default_device_camera_access_policy") public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7; // 0x7
field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
@@ -3577,18 +3577,18 @@ package android.companion.virtual.audio {
package android.companion.virtual.camera {
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
+ public final class VirtualCamera implements java.io.Closeable {
method public void close();
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
+ public interface VirtualCameraCallback {
method public default void onProcessCaptureRequest(int, long);
method public void onStreamClosed(int);
method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int);
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
+ public final class VirtualCameraConfig implements android.os.Parcelable {
method public int describeContents();
method public int getLensFacing();
method @NonNull public String getName();
@@ -3602,7 +3602,7 @@ package android.companion.virtual.camera {
field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
+ public static final class VirtualCameraConfig.Builder {
ctor public VirtualCameraConfig.Builder(@NonNull String);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
@@ -3611,7 +3611,7 @@ package android.companion.virtual.camera {
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
+ public final class VirtualCameraStreamConfig implements android.os.Parcelable {
method public int describeContents();
method public int getFormat();
method @IntRange(from=1) public int getHeight();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 5453e735ce17..e8ff546cc61a 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -942,7 +942,7 @@ package android.companion.virtual {
package android.companion.virtual.camera {
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
+ public final class VirtualCamera implements java.io.Closeable {
method @NonNull public String getId();
}
diff --git a/core/java/android/accessibilityservice/OWNERS b/core/java/android/accessibilityservice/OWNERS
index 1265dfa2c441..dac64f47ba7e 100644
--- a/core/java/android/accessibilityservice/OWNERS
+++ b/core/java/android/accessibilityservice/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
# Android Accessibility Framework owners
include /services/accessibility/OWNERS \ No newline at end of file
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 82c746a8ad4c..b8c20bd97264 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -2230,6 +2230,16 @@ public class ActivityOptions extends ComponentOptions {
return mLaunchCookie;
}
+ /**
+ * Set the ability for the current transition/animation to work cross-task.
+ * @param allowTaskOverride true to allow cross-task use, otherwise false.
+ *
+ * @hide
+ */
+ public ActivityOptions setOverrideTaskTransition(boolean allowTaskOverride) {
+ this.mOverrideTaskTransition = allowTaskOverride;
+ return this;
+ }
/** @hide */
public boolean getOverrideTaskTransition() {
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 2dead565fa85..f2e7e8513116 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -17,7 +17,6 @@
package android.app;
import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
-import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_NOT_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -1146,12 +1145,16 @@ public class ApplicationPackageManager extends PackageManager {
}
}
- private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY =
- createSystemCacheKey("get_packages_for_uid");
- private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
- mGetPackagesForUidCache =
- new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
- 1024, CACHE_KEY_PACKAGES_FOR_UID_PROPERTY) {
+ private static final String CACHE_KEY_PACKAGES_FOR_UID_API = "get_packages_for_uid";
+
+ /** @hide */
+ @VisibleForTesting
+ public static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
+ sGetPackagesForUidCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+ .maxEntries(1024).api(CACHE_KEY_PACKAGES_FOR_UID_API).cacheNulls(true),
+ CACHE_KEY_PACKAGES_FOR_UID_API, null) {
+
@Override
public GetPackagesForUidResult recompute(Integer uid) {
try {
@@ -1170,17 +1173,17 @@ public class ApplicationPackageManager extends PackageManager {
@Override
public String[] getPackagesForUid(int uid) {
- return mGetPackagesForUidCache.query(uid).value();
+ return sGetPackagesForUidCache.query(uid).value();
}
/** @hide */
public static void disableGetPackagesForUidCache() {
- mGetPackagesForUidCache.disableLocal();
+ sGetPackagesForUidCache.disableLocal();
}
/** @hide */
public static void invalidateGetPackagesForUidCache() {
- PropertyInvalidatedCache.invalidateCache(CACHE_KEY_PACKAGES_FOR_UID_PROPERTY);
+ sGetPackagesForUidCache.invalidateCache();
}
@Override
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index bfb33f2b7cb1..c573161f30cc 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1163,6 +1163,17 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
+ * Return the current cache nonce.
+ * @hide
+ */
+ @VisibleForTesting
+ public long getNonce() {
+ synchronized (mLock) {
+ return mNonce.getNonce();
+ }
+ }
+
+ /**
* Complete key prefixes.
*/
private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + ".";
@@ -1314,7 +1325,7 @@ public class PropertyInvalidatedCache<Query, Result> {
/**
* Burst a property name into module and api. Throw if the key is invalid. This method is
- * used in to transition legacy cache constructors to the args constructor.
+ * used to transition legacy cache constructors to the Args constructor.
*/
private static Args argsFromProperty(@NonNull String name) {
throwIfInvalidCacheKey(name);
@@ -1327,6 +1338,15 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
+ * Return the API porting of a legacy property. This method is used to transition caches to
+ * the Args constructor.
+ * @hide
+ */
+ public static String apiFromProperty(@NonNull String name) {
+ return argsFromProperty(name).mApi;
+ }
+
+ /**
* Make a new property invalidated cache. This constructor names the cache after the
* property name. New clients should prefer the constructor that takes an explicit
* cache name.
@@ -2036,11 +2056,11 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * Disable all caches in the local process. This is primarily useful for testing when
- * the test needs to bypass the cache or when the test is for a server, and the test
- * process does not have privileges to write SystemProperties. Once disabled it is not
- * possible to re-enable caching in the current process. If a client wants to
- * temporarily disable caching, use the corking mechanism.
+ * Disable all caches in the local process. This is primarily useful for testing when the
+ * test needs to bypass the cache or when the test is for a server, and the test process does
+ * not have privileges to write the nonce. Once disabled it is not possible to re-enable
+ * caching in the current process. See {@link #testPropertyName} for a more focused way to
+ * bypass caches when the test is for a server.
* @hide
*/
public static void disableForTestMode() {
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 0a3891fe47a1..a66d59ba9cb6 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,6 +27,7 @@ import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
+import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
import android.app.appsearch.AppSearchManager;
import android.content.Context;
import android.os.CancellationSignal;
@@ -325,8 +326,28 @@ public final class AppFunctionManager {
return;
}
+ // Wrap the callback to convert AppFunctionNotFoundException to IllegalArgumentException
+ // to match the documentation.
+ OutcomeReceiver<Boolean, Exception> callbackWithExceptionInterceptor =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(@NonNull Boolean result) {
+ callback.onResult(result);
+ }
+
+ @Override
+ public void onError(@NonNull Exception exception) {
+ if (exception instanceof AppFunctionNotFoundException) {
+ exception = new IllegalArgumentException(exception);
+ }
+ callback.onError(exception);
+ }
+ };
+
AppFunctionManagerHelper.isAppFunctionEnabled(
- functionIdentifier, targetPackage, appSearchManager, executor, callback);
+ functionIdentifier, targetPackage, appSearchManager, executor,
+ callbackWithExceptionInterceptor);
+
}
private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub {
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
index cc3ca03f423d..83abc048af8a 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
@@ -60,8 +60,8 @@ public class AppFunctionManagerHelper {
* <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
*
* <ul>
- * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
- * have access to it.
+ * <li>{@link AppFunctionNotFoundException}, if the function is not found or the caller does
+ * not have access to it.
* </ul>
*
* @param functionIdentifier the identifier of the app function to check (unique within the
@@ -216,7 +216,7 @@ public class AppFunctionManagerHelper {
private static @NonNull Exception failedResultToException(
@NonNull AppSearchResult appSearchResult) {
return switch (appSearchResult.getResultCode()) {
- case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
+ case AppSearchResult.RESULT_INVALID_ARGUMENT -> new AppFunctionNotFoundException(
appSearchResult.getErrorMessage());
case AppSearchResult.RESULT_IO_ERROR -> new IOException(
appSearchResult.getErrorMessage());
@@ -225,4 +225,15 @@ public class AppFunctionManagerHelper {
default -> new IllegalStateException(appSearchResult.getErrorMessage());
};
}
+
+ /**
+ * Throws when the app function is not found.
+ *
+ * @hide
+ */
+ public static class AppFunctionNotFoundException extends RuntimeException {
+ private AppFunctionNotFoundException(@NonNull String errorMessage) {
+ super(errorMessage);
+ }
+ }
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 252db824c69f..ab52db4b7a30 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -37,8 +37,8 @@ import android.companion.virtual.audio.VirtualAudioDevice;
import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback;
import android.companion.virtual.camera.VirtualCamera;
import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -624,7 +624,7 @@ public final class VirtualDeviceManager {
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public void goToSleep() {
mVirtualDeviceInternal.goToSleep();
}
@@ -642,7 +642,7 @@ public final class VirtualDeviceManager {
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public void wakeUp() {
mVirtualDeviceInternal.wakeUp();
}
@@ -838,7 +838,7 @@ public final class VirtualDeviceManager {
* @see #removeActivityPolicyExemption(ActivityPolicyExemption)
* @see #setDevicePolicy
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
mVirtualDeviceInternal.addActivityPolicyExemption(Objects.requireNonNull(exemption));
}
@@ -853,7 +853,7 @@ public final class VirtualDeviceManager {
* @see #addActivityPolicyExemption(ActivityPolicyExemption)
* @see #setDevicePolicy
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
mVirtualDeviceInternal.removeActivityPolicyExemption(Objects.requireNonNull(exemption));
}
@@ -875,7 +875,7 @@ public final class VirtualDeviceManager {
* @see VirtualDeviceParams#POLICY_TYPE_RECENTS
* @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public void setDevicePolicy(
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy,
@@ -1037,10 +1037,10 @@ public final class VirtualDeviceManager {
* @see android.view.InputDevice#SOURCE_ROTARY_ENCODER
*/
@NonNull
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_ROTARY)
+ @FlaggedApi(Flags.FLAG_VIRTUAL_ROTARY)
public VirtualRotaryEncoder createVirtualRotaryEncoder(
@NonNull VirtualRotaryEncoderConfig config) {
- if (!android.companion.virtualdevice.flags.Flags.virtualRotary()) {
+ if (!Flags.virtualRotary()) {
throw new UnsupportedOperationException("Virtual rotary support not enabled");
}
return mVirtualDeviceInternal.createVirtualRotaryEncoder(config);
@@ -1084,12 +1084,7 @@ public final class VirtualDeviceManager {
* @see VirtualDeviceParams#POLICY_TYPE_CAMERA
*/
@NonNull
- @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
- if (!Flags.virtualCamera()) {
- throw new UnsupportedOperationException(
- "Flag is not enabled: %s".formatted(Flags.FLAG_VIRTUAL_CAMERA));
- }
return mVirtualDeviceInternal.createVirtualCamera(Objects.requireNonNull(config));
}
@@ -1252,7 +1247,7 @@ public final class VirtualDeviceManager {
* @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
* @see VirtualDevice#addActivityPolicyExemption(ActivityPolicyExemption)
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
@NonNull UserHandle user, @Nullable IntentSender intentSender) {}
@@ -1268,7 +1263,7 @@ public final class VirtualDeviceManager {
* @see Display#FLAG_SECURE
* @see WindowManager.LayoutParams#FLAG_SECURE
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
default void onSecureWindowShown(int displayId, @NonNull ComponentName componentName,
@NonNull UserHandle user) {}
@@ -1284,7 +1279,7 @@ public final class VirtualDeviceManager {
* @see Display#FLAG_SECURE
* @see WindowManager.LayoutParams#FLAG_SECURE
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
default void onSecureWindowHidden(int displayId) {}
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 95dee9b72a88..699494790f35 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -29,12 +29,12 @@ import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.IVirtualSensorCallback;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorCallback;
import android.companion.virtual.sensor.VirtualSensorConfig;
import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Context;
import android.hardware.display.VirtualDisplayConfig;
@@ -279,7 +279,6 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Context#getDeviceId
*/
- @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public static final int POLICY_TYPE_CAMERA = 5;
/**
@@ -296,7 +295,7 @@ public final class VirtualDeviceParams implements Parcelable {
* </ul>
*/
// TODO(b/333443509): Link to POLICY_TYPE_ACTIVITY
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6;
/**
@@ -310,8 +309,7 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Context#DEVICE_ID_DEFAULT
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags
- .FLAG_DEFAULT_DEVICE_CAMERA_ACCESS_POLICY)
+ @FlaggedApi(Flags.FLAG_DEFAULT_DEVICE_CAMERA_ACCESS_POLICY)
public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7;
private final int mLockState;
@@ -407,7 +405,7 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Builder#setDimDuration(Duration)
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public @NonNull Duration getDimDuration() {
return Duration.ofMillis(mDimDuration);
}
@@ -417,7 +415,7 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Builder#setDimDuration(Duration)
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public @NonNull Duration getScreenOffTimeout() {
return Duration.ofMillis(mScreenOffTimeout);
}
@@ -876,7 +874,7 @@ public final class VirtualDeviceParams implements Parcelable {
* @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
* @see #setScreenOffTimeout
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
@NonNull
public Builder setDimDuration(@NonNull Duration dimDuration) {
if (Objects.requireNonNull(dimDuration).compareTo(Duration.ZERO) < 0) {
@@ -901,7 +899,7 @@ public final class VirtualDeviceParams implements Parcelable {
* @see #setDimDuration
* @see VirtualDeviceManager.VirtualDevice#goToSleep()
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
@NonNull
public Builder setScreenOffTimeout(@NonNull Duration screenOffTimeout) {
if (Objects.requireNonNull(screenOffTimeout).compareTo(Duration.ZERO) < 0) {
@@ -1311,15 +1309,11 @@ public final class VirtualDeviceParams implements Parcelable {
mScreenOffTimeout = INFINITE_TIMEOUT;
}
- if (!Flags.virtualCamera()) {
- mDevicePolicies.delete(POLICY_TYPE_CAMERA);
- }
-
- if (!android.companion.virtualdevice.flags.Flags.defaultDeviceCameraAccessPolicy()) {
+ if (!Flags.defaultDeviceCameraAccessPolicy()) {
mDevicePolicies.delete(POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS);
}
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY);
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java
index ece048d3a95b..b7bcc29a39cb 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -16,14 +16,12 @@
package android.companion.virtual.camera;
-import android.annotation.FlaggedApi;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
import android.hardware.camera2.CameraDevice;
import android.os.RemoteException;
@@ -51,7 +49,6 @@ import java.util.concurrent.Executor;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCamera implements Closeable {
private final IVirtualDevice mVirtualDevice;
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index c894de428b10..d326be83c404 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -16,11 +16,9 @@
package android.companion.virtual.camera;
-import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
import android.view.Surface;
@@ -34,7 +32,6 @@ import java.util.concurrent.Executor;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public interface VirtualCameraCallback {
/**
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 769b658c78ce..6c88ec99349e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -18,14 +18,12 @@ package android.companion.virtual.camera;
import static java.util.Objects.requireNonNull;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.companion.virtual.VirtualDevice;
-import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.hardware.camera2.CameraMetadata;
@@ -47,7 +45,6 @@ import java.util.concurrent.Executor;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCameraConfig implements Parcelable {
private static final int LENS_FACING_UNKNOWN = -1;
@@ -198,7 +195,6 @@ public final class VirtualCameraConfig implements Parcelable {
* VirtualCameraCallback)}
* <li>A lens facing must be set with {@link #setLensFacing(int)}
*/
- @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public static final class Builder {
private final String mName;
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 6ab66b3d2309..be498806697c 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -16,11 +16,9 @@
package android.companion.virtual.camera;
-import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
import android.os.Parcel;
import android.os.Parcelable;
@@ -35,7 +33,6 @@ import java.util.Objects;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCameraStreamConfig implements Parcelable {
// TODO(b/310857519): Check if we should increase the fps upper limit in future.
static final int MAX_FPS_UPPER_LIMIT = 60;
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 1cf42820f356..fcdb02ab5da2 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -11,13 +11,6 @@ package: "android.companion.virtualdevice.flags"
container: "system"
flag {
- namespace: "virtual_devices"
- name: "virtual_camera_service_discovery"
- description: "Enable discovery of the Virtual Camera HAL without a VINTF entry"
- bug: "305170199"
-}
-
-flag {
namespace: "virtual_devices"
name: "virtual_display_insets"
description: "APIs for specifying virtual display insets (via cutout)"
@@ -34,13 +27,6 @@ flag {
}
flag {
- namespace: "virtual_devices"
- name: "camera_device_awareness"
- description: "Enable device awareness in camera service"
- bug: "305170199"
-}
-
-flag {
name: "virtual_rotary"
is_exported: true
namespace: "virtual_devices"
@@ -49,14 +35,6 @@ flag {
}
flag {
- namespace: "virtual_devices"
- name: "device_aware_drm"
- description: "Makes MediaDrm APIs device-aware"
- bug: "303535376"
- is_fixed_read_only: true
-}
-
-flag {
namespace: "virtual_devices"
name: "enforce_remote_device_opt_out_on_all_virtual_displays"
description: "Respect canDisplayOnRemoteDevices on all virtual displays"
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 0369b7d9bc28..6ae2df2cd7a2 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -16,6 +16,7 @@
package android.content.pm;
+import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
import static android.content.pm.SigningInfo.AppSigningSchemeVersion;
import static android.media.audio.Flags.FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY;
@@ -11659,11 +11660,22 @@ public abstract class PackageManager {
}
}
- private static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
- sApplicationInfoCache =
- new PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>(
- 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE,
- "getApplicationInfo") {
+ private static String packageInfoApi() {
+ return PropertyInvalidatedCache.apiFromProperty(
+ PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE);
+ }
+
+ // The maximum number of entries to keep in the packageInfo and applicationInfo caches.
+ private final static int MAX_INFO_CACHE_ENTRIES = 2048;
+
+ /** @hide */
+ @VisibleForTesting
+ public static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
+ sApplicationInfoCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+ .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true),
+ "getApplicationInfo", null) {
+
@Override
public ApplicationInfo recompute(ApplicationInfoQuery query) {
return getApplicationInfoAsUserUncached(
@@ -11749,10 +11761,11 @@ public abstract class PackageManager {
}
private static final PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>
- sPackageInfoCache =
- new PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>(
- 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE,
- "getPackageInfo") {
+ sPackageInfoCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+ .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true),
+ "getPackageInfo", null) {
+
@Override
public PackageInfo recompute(PackageInfoQuery query) {
return getPackageInfoAsUserUncached(
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index ca3e3d2ad61b..6ec70045f1f4 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -358,8 +358,7 @@ public class Camera {
CameraInfo cameraInfo);
private static int getDevicePolicyFromContext(Context context) {
- if (context.getDeviceId() == DEVICE_ID_DEFAULT
- || !android.companion.virtual.flags.Flags.virtualCamera()) {
+ if (context.getDeviceId() == DEVICE_ID_DEFAULT) {
return DEVICE_POLICY_DEFAULT;
}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index bfaff941939c..bcb7ebfb286f 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -67,6 +67,7 @@ import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
@@ -591,8 +592,7 @@ public final class CameraManager {
/** @hide */
public int getDevicePolicyFromContext(@NonNull Context context) {
- if (context.getDeviceId() == DEVICE_ID_DEFAULT
- || !android.companion.virtual.flags.Flags.virtualCamera()) {
+ if (context.getDeviceId() == DEVICE_ID_DEFAULT) {
return DEVICE_POLICY_DEFAULT;
}
@@ -1705,7 +1705,9 @@ public final class CameraManager {
return ICameraService.ROTATION_OVERRIDE_NONE;
}
- if (context != null) {
+ // Isolated process does not have access to ActivityTaskManager service, which is used
+ // indirectly in `ActivityManager.getAppTasks()`.
+ if (context != null && !Process.isIsolated()) {
final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
if (activityManager != null) {
for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) {
@@ -2576,11 +2578,6 @@ public final class CameraManager {
private boolean shouldHideCamera(int currentDeviceId, int devicePolicy,
DeviceCameraInfo info) {
- if (!android.companion.virtualdevice.flags.Flags.cameraDeviceAwareness()) {
- // Don't hide any cameras if the device-awareness feature flag is disabled.
- return false;
- }
-
if (devicePolicy == DEVICE_POLICY_DEFAULT && info.mDeviceId == DEVICE_ID_DEFAULT) {
// Don't hide default-device cameras for a default-policy virtual device.
return false;
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index fded88212127..d8919160320a 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -641,6 +641,9 @@ public final class DisplayManager {
* is triggered whenever the properties of a {@link android.view.Display}, such as size,
* state, density are modified.
*
+ * This event is not triggered for refresh rate changes as they can change very often.
+ * To monitor refresh rate changes, subscribe to {@link EVENT_TYPE_DISPLAY_REFRESH_RATE}.
+ *
* @see #registerDisplayListener(DisplayListener, Handler, long)
*
*/
@@ -839,6 +842,9 @@ public final class DisplayManager {
* Registers a display listener to receive notifications about when
* displays are added, removed or changed.
*
+ * We encourage to use {@link #registerDisplayListener(Executor, long, DisplayListener)}
+ * instead to subscribe for explicit events of interest
+ *
* @param listener The listener to register.
* @param handler The handler on which the listener should be invoked, or null
* if the listener should be invoked on the calling thread's looper.
@@ -847,7 +853,9 @@ public final class DisplayManager {
*/
public void registerDisplayListener(DisplayListener listener, Handler handler) {
registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED
- | EVENT_TYPE_DISPLAY_CHANGED | EVENT_TYPE_DISPLAY_REMOVED);
+ | EVENT_TYPE_DISPLAY_CHANGED
+ | EVENT_TYPE_DISPLAY_REFRESH_RATE
+ | EVENT_TYPE_DISPLAY_REMOVED);
}
/**
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index b5715ed25bd9..339dbf2c2029 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1766,29 +1766,23 @@ public final class DisplayManagerGlobal {
}
if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_CHANGED) != 0) {
- // For backward compatibility, a client subscribing to
- // DisplayManager.EVENT_FLAG_DISPLAY_CHANGED will be enrolled to both Basic and
- // RR changes
- baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED
- | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE;
+ baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED;
}
- if ((eventFlags
- & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) {
+ if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REMOVED) != 0) {
baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REMOVED;
}
- if (Flags.displayListenerPerformanceImprovements()) {
- if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) {
- baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE;
- }
+ if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_REFRESH_RATE) != 0) {
+ baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE;
+ }
+ if (Flags.displayListenerPerformanceImprovements()) {
if ((eventFlags & DisplayManager.EVENT_TYPE_DISPLAY_STATE) != 0) {
baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_STATE;
}
}
-
return baseEventMask;
}
}
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 23722ed5bb0d..8d58296e5581 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -233,3 +233,12 @@ flag {
description: "Key Event Activity Detection"
bug: "356412905"
}
+
+flag {
+ name: "enable_backup_and_restore_for_input_gestures"
+ namespace: "input"
+ description: "Adds backup and restore support for custom input gestures"
+ bug: "382184249"
+ is_fixed_read_only: true
+}
+
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index e79b2e7becce..26044545b8d1 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -45,7 +45,8 @@ import java.util.function.BiFunction;
* {@link PersistableBundle} subclass.
*/
@android.ravenwood.annotation.RavenwoodKeepWholeClass
-public class BaseBundle {
+@SuppressWarnings("HiddenSuperclass")
+public class BaseBundle implements Parcel.ClassLoaderProvider {
/** @hide */
protected static final String TAG = "Bundle";
static final boolean DEBUG = false;
@@ -311,8 +312,9 @@ public class BaseBundle {
/**
* Return the ClassLoader currently associated with this Bundle.
+ * @hide
*/
- ClassLoader getClassLoader() {
+ public ClassLoader getClassLoader() {
return mClassLoader;
}
@@ -426,6 +428,9 @@ public class BaseBundle {
if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) {
Intent.maybeMarkAsMissingCreatorToken(object);
}
+ } else if (object instanceof Bundle) {
+ Bundle bundle = (Bundle) object;
+ bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
}
return (clazz != null) ? clazz.cast(object) : (T) object;
}
@@ -499,7 +504,7 @@ public class BaseBundle {
int[] numLazyValues = new int[]{0};
try {
parcelledData.readArrayMap(map, count, !parcelledByNative,
- /* lazy */ ownsParcel, mClassLoader, numLazyValues);
+ /* lazy */ ownsParcel, this, numLazyValues);
} catch (BadParcelableException e) {
if (sShouldDefuse) {
Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index a24dc5739b7e..c0591e6899b6 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -141,6 +141,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
STRIPPED.putInt("STRIPPED", 1);
}
+ private boolean isFirstRetrievedFromABundle = false;
+
/**
* Constructs a new, empty Bundle.
*/
@@ -382,7 +384,15 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
bundle.unparcel();
mOwnsLazyValues = false;
bundle.mOwnsLazyValues = false;
- mMap.putAll(bundle.mMap);
+ int N = bundle.mMap.size();
+ for (int i = 0; i < N; i++) {
+ String key = bundle.mMap.keyAt(i);
+ Object value = bundle.mMap.valueAt(i);
+ if (value instanceof Bundle) {
+ ((Bundle) value).isFirstRetrievedFromABundle = true;
+ }
+ mMap.put(key, value);
+ }
// FD and Binders state is now known if and only if both bundles already knew
if ((bundle.mFlags & FLAG_HAS_FDS) != 0) {
@@ -592,6 +602,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
if (intentClass != null && intentClass.isInstance(value)) {
setHasIntent(true);
+ } else if (value instanceof Bundle) {
+ ((Bundle) value).isFirstRetrievedFromABundle = true;
}
}
@@ -793,6 +805,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
*/
public void putBundle(@Nullable String key, @Nullable Bundle value) {
unparcel();
+ if (value != null) {
+ value.isFirstRetrievedFromABundle = true;
+ }
mMap.put(key, value);
}
@@ -1020,7 +1035,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
return null;
}
try {
- return (Bundle) o;
+ Bundle bundle = (Bundle) o;
+ bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
+ return bundle;
} catch (ClassCastException e) {
typeWarning(key, o, "Bundle", e);
return null;
@@ -1028,6 +1045,21 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
}
/**
+ * Set the ClassLoader of a bundle to its container bundle. This is necessary so that when a
+ * bundle's ClassLoader is changed, it can be propagated to its children. Do this only when it
+ * is retrieved from the container bundle first time though. Once it is accessed outside of its
+ * container, its ClassLoader should no longer be changed by its container anymore.
+ *
+ * @param containerBundle the bundle this bundle is retrieved from.
+ */
+ void setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(BaseBundle containerBundle) {
+ if (!isFirstRetrievedFromABundle) {
+ setClassLoader(containerBundle.getClassLoader());
+ isFirstRetrievedFromABundle = true;
+ }
+ }
+
+ /**
* Returns the value associated with the given key, or {@code null} if
* no mapping of the desired type exists for the given key or a {@code null}
* value is explicitly associated with the key.
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index e58934746c14..49d3f06eb80f 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -46,6 +46,7 @@ import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import dalvik.annotation.optimization.CriticalNative;
@@ -4661,7 +4662,7 @@ public final class Parcel {
* @hide
*/
@Nullable
- public Object readLazyValue(@Nullable ClassLoader loader) {
+ private Object readLazyValue(@Nullable ClassLoaderProvider loaderProvider) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
@@ -4672,12 +4673,17 @@ public final class Parcel {
int end = MathUtils.addOrThrow(dataPosition(), objectLength);
int valueLength = end - start;
setDataPosition(end);
- return new LazyValue(this, start, valueLength, type, loader);
+ return new LazyValue(this, start, valueLength, type, loaderProvider);
} else {
- return readValue(type, loader, /* clazz */ null);
+ return readValue(type, getClassLoader(loaderProvider), /* clazz */ null);
}
}
+ @Nullable
+ private static ClassLoader getClassLoader(@Nullable ClassLoaderProvider loaderProvider) {
+ return loaderProvider == null ? null : loaderProvider.getClassLoader();
+ }
+
private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> {
/**
@@ -4691,7 +4697,12 @@ public final class Parcel {
private final int mPosition;
private final int mLength;
private final int mType;
- @Nullable private final ClassLoader mLoader;
+ // this member is set when a bundle that includes a LazyValue is unparceled. But it is used
+ // when apply method is called. Between these 2 events, the bundle's ClassLoader could have
+ // changed. Let the bundle be a ClassLoaderProvider allows the bundle provides its current
+ // ClassLoader at the time apply method is called.
+ @NonNull
+ private final ClassLoaderProvider mLoaderProvider;
@Nullable private Object mObject;
/**
@@ -4702,12 +4713,13 @@ public final class Parcel {
*/
@Nullable private volatile Parcel mSource;
- LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) {
+ LazyValue(Parcel source, int position, int length, int type,
+ @NonNull ClassLoaderProvider loaderProvider) {
mSource = requireNonNull(source);
mPosition = position;
mLength = length;
mType = type;
- mLoader = loader;
+ mLoaderProvider = loaderProvider;
}
@Override
@@ -4720,7 +4732,8 @@ public final class Parcel {
int restore = source.dataPosition();
try {
source.setDataPosition(mPosition);
- mObject = source.readValue(mLoader, clazz, itemTypes);
+ mObject = source.readValue(mLoaderProvider.getClassLoader(), clazz,
+ itemTypes);
} finally {
source.setDataPosition(restore);
}
@@ -4758,6 +4771,12 @@ public final class Parcel {
return Parcel.hasFileDescriptors(mObject);
}
+ /** @hide */
+ @VisibleForTesting
+ public ClassLoader getClassLoader() {
+ return mLoaderProvider.getClassLoader();
+ }
+
@Override
public String toString() {
return (mSource != null)
@@ -4793,7 +4812,8 @@ public final class Parcel {
return Objects.equals(mObject, value.mObject);
}
// Better safely fail here since this could mean we get different objects.
- if (!Objects.equals(mLoader, value.mLoader)) {
+ if (!Objects.equals(mLoaderProvider.getClassLoader(),
+ value.mLoaderProvider.getClassLoader())) {
return false;
}
// Otherwise compare metadata prior to comparing payload.
@@ -4807,10 +4827,24 @@ public final class Parcel {
@Override
public int hashCode() {
// Accessing mSource first to provide memory barrier for mObject
- return Objects.hash(mSource == null, mObject, mLoader, mType, mLength);
+ return Objects.hash(mSource == null, mObject, mLoaderProvider.getClassLoader(), mType,
+ mLength);
}
}
+ /**
+ * Provides a ClassLoader.
+ * @hide
+ */
+ public interface ClassLoaderProvider {
+ /**
+ * Returns a ClassLoader.
+ *
+ * @return ClassLoader
+ */
+ ClassLoader getClassLoader();
+ }
+
/** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */
private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
// Avoids allocating Class[0] array
@@ -5551,8 +5585,8 @@ public final class Parcel {
}
private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal,
- int size, @Nullable ClassLoader loader) {
- readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader, null);
+ int size, @Nullable ClassLoaderProvider loaderProvider) {
+ readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null);
}
/**
@@ -5566,11 +5600,12 @@ public final class Parcel {
* @hide
*/
void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
- boolean lazy, @Nullable ClassLoader loader, int[] lazyValueCount) {
+ boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) {
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, size);
while (size > 0) {
String key = readString();
- Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
+ Object value = (lazy) ? readLazyValue(loaderProvider) : readValue(
+ getClassLoader(loaderProvider));
if (value instanceof LazyValue) {
lazyValueCount[0]++;
}
@@ -5591,12 +5626,12 @@ public final class Parcel {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal,
- @Nullable ClassLoader loader) {
+ @Nullable ClassLoaderProvider loaderProvider) {
final int N = readInt();
if (N < 0) {
return;
}
- readArrayMapInternal(outVal, N, loader);
+ readArrayMapInternal(outVal, N, loaderProvider);
}
/**
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index 9d0e221bd9e7..a8a22f675e08 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -473,17 +473,31 @@ public class SystemHealthManager {
}
}
- final HealthStats[] results = new HealthStats[uids.length];
- if (result.bundle != null) {
- HealthStatsParceler[] parcelers = result.bundle.getParcelableArray(
- IBatteryStats.KEY_UID_SNAPSHOTS, HealthStatsParceler.class);
- if (parcelers != null && parcelers.length == uids.length) {
- for (int i = 0; i < parcelers.length; i++) {
- results[i] = parcelers[i].getHealthStats();
+ switch (result.resultCode) {
+ case IBatteryStats.RESULT_OK: {
+ final HealthStats[] results = new HealthStats[uids.length];
+ if (result.bundle != null) {
+ HealthStatsParceler[] parcelers = result.bundle.getParcelableArray(
+ IBatteryStats.KEY_UID_SNAPSHOTS, HealthStatsParceler.class);
+ if (parcelers != null && parcelers.length == uids.length) {
+ for (int i = 0; i < parcelers.length; i++) {
+ results[i] = parcelers[i].getHealthStats();
+ }
+ }
}
+ return results;
+ }
+ case IBatteryStats.RESULT_SECURITY_EXCEPTION: {
+ throw new SecurityException(result.bundle != null
+ ? result.bundle.getString(IBatteryStats.KEY_EXCEPTION_MESSAGE) : null);
+ }
+ case IBatteryStats.RESULT_RUNTIME_EXCEPTION: {
+ throw new RuntimeException(result.bundle != null
+ ? result.bundle.getString(IBatteryStats.KEY_EXCEPTION_MESSAGE) : null);
}
+ default:
+ throw new RuntimeException("Error code: " + result.resultCode);
}
- return results;
}
/**
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 91ad22f51345..24f8672c1e7c 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -22,6 +22,7 @@ import static android.app.AppOpsManager.OP_LEGACY_STORAGE;
import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.OP_READ_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.OP_READ_MEDIA_IMAGES;
+import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.UserHandle.PER_USER_RANGE;
@@ -44,6 +45,7 @@ import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.PendingIntent;
+import android.app.PropertyInvalidatedCache;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.Context;
@@ -269,14 +271,15 @@ public class StorageManager {
public static final int FLAG_STORAGE_SDK = IInstalld.FLAG_STORAGE_SDK;
/** {@hide} */
- @IntDef(prefix = "FLAG_STORAGE_", value = {
+ @IntDef(prefix = "FLAG_STORAGE_", value = {
FLAG_STORAGE_DE,
FLAG_STORAGE_CE,
FLAG_STORAGE_EXTERNAL,
FLAG_STORAGE_SDK,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface StorageFlags {}
+ public @interface StorageFlags {
+ }
/** {@hide} */
public static final int FLAG_FOR_WRITE = 1 << 8;
@@ -309,6 +312,44 @@ public class StorageManager {
@GuardedBy("mDelegates")
private final ArrayList<StorageEventListenerDelegate> mDelegates = new ArrayList<>();
+ static record VolumeListQuery(int mUserId, String mPackageName, int mFlags) {
+ }
+
+ private static final PropertyInvalidatedCache.QueryHandler<VolumeListQuery, StorageVolume[]>
+ sVolumeListQuery = new PropertyInvalidatedCache.QueryHandler<>() {
+ @androidx.annotation.Nullable
+ @Override
+ public StorageVolume[] apply(@androidx.annotation.NonNull VolumeListQuery query) {
+ final IStorageManager storageManager = IStorageManager.Stub.asInterface(
+ ServiceManager.getService("mount"));
+ if (storageManager == null) {
+ // negative results won't be cached, so we will just try again next time
+ return null;
+ }
+ try {
+ return storageManager.getVolumeList(
+ query.mUserId, query.mPackageName, query.mFlags);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ };
+
+ // Generally, the userId and packageName parameters stay pretty constant, but flags may change
+ // regularly; we have observed some processes hitting 10+ variations.
+ private static final int VOLUME_LIST_CACHE_MAX = 16;
+
+ private static final PropertyInvalidatedCache<VolumeListQuery, StorageVolume[]>
+ sVolumeListCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM).cacheNulls(false)
+ .api("getVolumeList").maxEntries(VOLUME_LIST_CACHE_MAX), "getVolumeList",
+ sVolumeListQuery);
+
+ /** {@hide} */
+ public static void invalidateVolumeListCache() {
+ sVolumeListCache.invalidateCache();
+ }
+
private class StorageEventListenerDelegate extends IStorageEventListener.Stub {
final Executor mExecutor;
final StorageEventListener mListener;
@@ -395,7 +436,8 @@ public class StorageManager {
private class ObbActionListener extends IObbActionListener.Stub {
@SuppressWarnings("hiding")
- private SparseArray<ObbListenerDelegate> mListeners = new SparseArray<ObbListenerDelegate>();
+ private SparseArray<ObbListenerDelegate> mListeners =
+ new SparseArray<ObbListenerDelegate>();
@Override
public void onObbResult(String filename, int nonce, int status) {
@@ -477,10 +519,10 @@ public class StorageManager {
*
* @param looper The {@link android.os.Looper} which events will be received on.
*
- * <p>Applications can get instance of this class by calling
- * {@link android.content.Context#getSystemService(java.lang.String)} with an argument
- * of {@link android.content.Context#STORAGE_SERVICE}.
- *
+ * <p>Applications can get instance of this class by calling
+ * {@link android.content.Context#getSystemService(java.lang.String)} with an
+ * argument
+ * of {@link android.content.Context#STORAGE_SERVICE}.
* @hide
*/
@UnsupportedAppUsage
@@ -488,15 +530,16 @@ public class StorageManager {
mContext = context;
mResolver = context.getContentResolver();
mLooper = looper;
- mStorageManager = IStorageManager.Stub.asInterface(ServiceManager.getServiceOrThrow("mount"));
+ mStorageManager = IStorageManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow("mount"));
mAppOps = mContext.getSystemService(AppOpsManager.class);
}
/**
* Registers a {@link android.os.storage.StorageEventListener StorageEventListener}.
*
- * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object.
- *
+ * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener}
+ * object.
* @hide
*/
@UnsupportedAppUsage
@@ -516,14 +559,14 @@ public class StorageManager {
/**
* Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}.
*
- * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object.
- *
+ * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener}
+ * object.
* @hide
*/
@UnsupportedAppUsage
public void unregisterListener(StorageEventListener listener) {
synchronized (mDelegates) {
- for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) {
+ for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) {
final StorageEventListenerDelegate delegate = i.next();
if (delegate.mListener == listener) {
try {
@@ -558,7 +601,8 @@ public class StorageManager {
* {@link StorageManager#getStorageVolumes()} to observe the latest
* value.
*/
- public void onStateChanged(@NonNull StorageVolume volume) { }
+ public void onStateChanged(@NonNull StorageVolume volume) {
+ }
}
/**
@@ -592,7 +636,7 @@ public class StorageManager {
*/
public void unregisterStorageVolumeCallback(@NonNull StorageVolumeCallback callback) {
synchronized (mDelegates) {
- for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext();) {
+ for (Iterator<StorageEventListenerDelegate> i = mDelegates.iterator(); i.hasNext(); ) {
final StorageEventListenerDelegate delegate = i.next();
if (delegate.mCallback == callback) {
try {
@@ -628,8 +672,8 @@ public class StorageManager {
/**
* Query if a USB Mass Storage (UMS) host is connected.
- * @return true if UMS host is connected.
*
+ * @return true if UMS host is connected.
* @hide
*/
@Deprecated
@@ -640,8 +684,8 @@ public class StorageManager {
/**
* Query if a USB Mass Storage (UMS) is enabled on the device.
- * @return true if UMS host is enabled.
*
+ * @return true if UMS host is enabled.
* @hide
*/
@Deprecated
@@ -663,11 +707,11 @@ public class StorageManager {
* That is, shared UID applications can attempt to mount any other
* application's OBB that shares its UID.
*
- * @param rawPath the path to the OBB file
- * @param key must be <code>null</code>. Previously, some Android device
- * implementations accepted a non-<code>null</code> key to mount
- * an encrypted OBB file. However, this never worked reliably and
- * is no longer supported.
+ * @param rawPath the path to the OBB file
+ * @param key must be <code>null</code>. Previously, some Android device
+ * implementations accepted a non-<code>null</code> key to mount
+ * an encrypted OBB file. However, this never worked reliably and
+ * is no longer supported.
* @param listener will receive the success or failure of the operation
* @return whether the mount call was successfully queued or not
*/
@@ -739,9 +783,9 @@ public class StorageManager {
* application's OBB that shares its UID.
* <p>
*
- * @param rawPath path to the OBB file
- * @param force whether to kill any programs using this in order to unmount
- * it
+ * @param rawPath path to the OBB file
+ * @param force whether to kill any programs using this in order to unmount
+ * it
* @param listener will receive the success or failure of the operation
* @return whether the unmount call was successfully queued or not
*/
@@ -781,7 +825,7 @@ public class StorageManager {
*
* @param rawPath path to OBB image
* @return absolute path to mounted OBB image data or <code>null</code> if
- * not mounted or exception encountered trying to read status
+ * not mounted or exception encountered trying to read status
*/
public String getMountedObbPath(String rawPath) {
Preconditions.checkNotNull(rawPath, "rawPath cannot be null");
@@ -899,7 +943,7 @@ public class StorageManager {
* {@link #UUID_DEFAULT}.
*
* @throws IOException when the storage device hosting the given path isn't
- * present, or when it doesn't have a valid UUID.
+ * present, or when it doesn't have a valid UUID.
*/
public @NonNull UUID getUuidForPath(@NonNull File path) throws IOException {
Preconditions.checkNotNull(path);
@@ -1172,8 +1216,8 @@ public class StorageManager {
/**
* This is not the API you're looking for.
*
- * @see PackageManager#getPrimaryStorageCurrentVolume()
* @hide
+ * @see PackageManager#getPrimaryStorageCurrentVolume()
*/
public String getPrimaryStorageUuid() {
try {
@@ -1186,8 +1230,8 @@ public class StorageManager {
/**
* This is not the API you're looking for.
*
- * @see PackageManager#movePrimaryStorage(VolumeInfo)
* @hide
+ * @see PackageManager#movePrimaryStorage(VolumeInfo)
*/
public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) {
try {
@@ -1216,7 +1260,7 @@ public class StorageManager {
// resolve the actual volume name
if (Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL)) {
try (Cursor c = mContext.getContentResolver().query(uri,
- new String[] { MediaStore.MediaColumns.VOLUME_NAME }, null, null)) {
+ new String[]{MediaStore.MediaColumns.VOLUME_NAME}, null, null)) {
if (c.moveToFirst()) {
volumeName = c.getString(0);
}
@@ -1275,6 +1319,7 @@ public class StorageManager {
/**
* Gets the state of a volume via its mountpoint.
+ *
* @hide
*/
@Deprecated
@@ -1308,7 +1353,7 @@ public class StorageManager {
* Return the list of shared/external storage volumes currently available to
* the calling user and the user it shares media with. Please refer to
* <a href="https://source.android.com/compatibility/12/android-12-cdd#95_multi-user_support">
- * multi-user support</a> for more details.
+ * multi-user support</a> for more details.
*
* <p>
* This is similar to {@link StorageManager#getStorageVolumes()} except that the result also
@@ -1353,7 +1398,7 @@ public class StorageManager {
public static Pair<String, Long> getPrimaryStoragePathAndSize() {
return Pair.create(null,
FileUtils.roundStorageSize(Environment.getDataDirectory().getTotalSpace()
- + Environment.getRootDirectory().getTotalSpace()));
+ + Environment.getRootDirectory().getTotalSpace()));
}
/** {@hide} */
@@ -1389,8 +1434,6 @@ public class StorageManager {
/** {@hide} */
@UnsupportedAppUsage
public static @NonNull StorageVolume[] getVolumeList(int userId, int flags) {
- final IStorageManager storageManager = IStorageManager.Stub.asInterface(
- ServiceManager.getService("mount"));
try {
String packageName = ActivityThread.currentOpPackageName();
if (packageName == null) {
@@ -1406,7 +1449,7 @@ public class StorageManager {
}
packageName = packageNames[0];
}
- return storageManager.getVolumeList(userId, packageName, flags);
+ return sVolumeListCache.query(new VolumeListQuery(userId, packageName, flags));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1414,6 +1457,7 @@ public class StorageManager {
/**
* Returns list of paths for all mountable volumes.
+ *
* @hide
*/
@Deprecated
@@ -1605,7 +1649,7 @@ public class StorageManager {
* <p>
* This is only intended to be called by UserManagerService, as part of creating a user.
*
- * @param userId ID of the user
+ * @param userId ID of the user
* @param ephemeral whether the user is ephemeral
* @throws RuntimeException on error. The user's keys already existing is considered an error.
* @hide
@@ -1711,7 +1755,8 @@ public class StorageManager {
return false;
}
- /** {@hide}
+ /**
+ * {@hide}
* Is this device encrypted?
* <p>
* Note: all devices launching with Android 10 (API level 29) or later are
@@ -1724,8 +1769,10 @@ public class StorageManager {
return RoSystemProperties.CRYPTO_ENCRYPTED;
}
- /** {@hide}
+ /**
+ * {@hide}
* Does this device have file-based encryption (FBE) enabled?
+ *
* @return true if the device has file-based encryption enabled.
*/
public static boolean isFileEncrypted() {
@@ -1759,8 +1806,8 @@ public class StorageManager {
}
/**
- * @deprecated disabled now that FUSE has been replaced by sdcardfs
* @hide
+ * @deprecated disabled now that FUSE has been replaced by sdcardfs
*/
@Deprecated
public static File maybeTranslateEmulatedPathToInternal(File path) {
@@ -1790,6 +1837,7 @@ public class StorageManager {
/**
* Check that given app holds both permission and appop.
+ *
* @hide
*/
public static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid,
@@ -1800,6 +1848,7 @@ public class StorageManager {
/**
* Check that given app holds both permission and appop but do not noteOp.
+ *
* @hide
*/
public static boolean checkPermissionAndCheckOp(Context context, boolean enforce,
@@ -1810,6 +1859,7 @@ public class StorageManager {
/**
* Check that given app holds both permission and appop.
+ *
* @hide
*/
private static boolean checkPermissionAndAppOp(Context context, boolean enforce, int pid,
@@ -1877,7 +1927,9 @@ public class StorageManager {
// Legacy apps technically have the access granted by this op,
// even when the op is denied
if ((mAppOps.checkOpNoThrow(OP_LEGACY_STORAGE, uid,
- packageName) == AppOpsManager.MODE_ALLOWED)) return true;
+ packageName) == AppOpsManager.MODE_ALLOWED)) {
+ return true;
+ }
if (enforce) {
throw new SecurityException("Op " + AppOpsManager.opToName(op) + " "
@@ -1924,7 +1976,7 @@ public class StorageManager {
return true;
}
if (mode == AppOpsManager.MODE_DEFAULT && mContext.checkPermission(
- MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) {
+ MANAGE_EXTERNAL_STORAGE, pid, uid) == PERMISSION_GRANTED) {
return true;
}
// If app doesn't have MANAGE_EXTERNAL_STORAGE, then check if it has requested granular
@@ -1936,7 +1988,7 @@ public class StorageManager {
@VisibleForTesting
public @NonNull ParcelFileDescriptor openProxyFileDescriptor(
int mode, ProxyFileDescriptorCallback callback, Handler handler, ThreadFactory factory)
- throws IOException {
+ throws IOException {
Preconditions.checkNotNull(callback);
MetricsLogger.count(mContext, "storage_open_proxy_file_descriptor", 1);
// Retry is needed because the mount point mFuseAppLoop is using may be unmounted before
@@ -1987,7 +2039,7 @@ public class StorageManager {
/** {@hide} */
public @NonNull ParcelFileDescriptor openProxyFileDescriptor(
int mode, ProxyFileDescriptorCallback callback)
- throws IOException {
+ throws IOException {
return openProxyFileDescriptor(mode, callback, null, null);
}
@@ -2006,19 +2058,18 @@ public class StorageManager {
* you're willing to decrypt on-demand, but where you want to avoid
* persisting the cleartext version.
*
- * @param mode The desired access mode, must be one of
- * {@link ParcelFileDescriptor#MODE_READ_ONLY},
- * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or
- * {@link ParcelFileDescriptor#MODE_READ_WRITE}
+ * @param mode The desired access mode, must be one of
+ * {@link ParcelFileDescriptor#MODE_READ_ONLY},
+ * {@link ParcelFileDescriptor#MODE_WRITE_ONLY}, or
+ * {@link ParcelFileDescriptor#MODE_READ_WRITE}
* @param callback Callback to process file operation requests issued on
- * returned file descriptor.
- * @param handler Handler that invokes callback methods.
+ * returned file descriptor.
+ * @param handler Handler that invokes callback methods.
* @return Seekable ParcelFileDescriptor.
- * @throws IOException
*/
public @NonNull ParcelFileDescriptor openProxyFileDescriptor(
int mode, ProxyFileDescriptorCallback callback, Handler handler)
- throws IOException {
+ throws IOException {
Preconditions.checkNotNull(handler);
return openProxyFileDescriptor(mode, callback, handler, null);
}
@@ -2050,10 +2101,10 @@ public class StorageManager {
* </p>
*
* @param storageUuid the UUID of the storage volume that you're interested
- * in. The UUID for a specific path can be obtained using
- * {@link #getUuidForPath(File)}.
+ * in. The UUID for a specific path can be obtained using
+ * {@link #getUuidForPath(File)}.
* @throws IOException when the storage device isn't present, or when it
- * doesn't support cache quotas.
+ * doesn't support cache quotas.
* @see #getCacheSizeBytes(UUID)
*/
@WorkerThread
@@ -2085,10 +2136,10 @@ public class StorageManager {
* </p>
*
* @param storageUuid the UUID of the storage volume that you're interested
- * in. The UUID for a specific path can be obtained using
- * {@link #getUuidForPath(File)}.
+ * in. The UUID for a specific path can be obtained using
+ * {@link #getUuidForPath(File)}.
* @throws IOException when the storage device isn't present, or when it
- * doesn't support cache quotas.
+ * doesn't support cache quotas.
* @see #getCacheQuotaBytes(UUID)
*/
@WorkerThread
@@ -2106,7 +2157,7 @@ public class StorageManager {
/** @hide */
- @IntDef(prefix = { "MOUNT_MODE_" }, value = {
+ @IntDef(prefix = {"MOUNT_MODE_"}, value = {
MOUNT_MODE_EXTERNAL_NONE,
MOUNT_MODE_EXTERNAL_DEFAULT,
MOUNT_MODE_EXTERNAL_INSTALLER,
@@ -2115,16 +2166,19 @@ public class StorageManager {
})
@Retention(RetentionPolicy.SOURCE)
/** @hide */
- public @interface MountMode {}
+ public @interface MountMode {
+ }
/**
* No external storage should be mounted.
+ *
* @hide
*/
@SystemApi
public static final int MOUNT_MODE_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE;
/**
* Default external storage should be mounted.
+ *
* @hide
*/
@SystemApi
@@ -2132,12 +2186,14 @@ public class StorageManager {
/**
* Mount mode for package installers which should give them access to
* all obb dirs in addition to their package sandboxes
+ *
* @hide
*/
@SystemApi
public static final int MOUNT_MODE_EXTERNAL_INSTALLER = IVold.REMOUNT_MODE_INSTALLER;
/**
* The lower file system should be bind mounted directly on external storage
+ *
* @hide
*/
@SystemApi
@@ -2146,6 +2202,7 @@ public class StorageManager {
/**
* Use the regular scoped storage filesystem, but Android/ should be writable.
* Used to support the applications hosting DownloadManager and the MTP server.
+ *
* @hide
*/
@SystemApi
@@ -2164,10 +2221,10 @@ public class StorageManager {
* this flag to take effect.
* </p>
*
+ * @hide
* @see #getAllocatableBytes(UUID, int)
* @see #allocateBytes(UUID, long, int)
* @see #allocateBytes(FileDescriptor, long, int)
- * @hide
*/
@RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE)
@SystemApi
@@ -2194,6 +2251,7 @@ public class StorageManager {
* freeable cached space when determining allocatable space.
*
* Intended for use with {@link #getAllocatableBytes()}.
+ *
* @hide
*/
public static final int FLAG_ALLOCATE_NON_CACHE_ONLY = 1 << 3;
@@ -2203,12 +2261,13 @@ public class StorageManager {
* cached space when determining allocatable space.
*
* Intended for use with {@link #getAllocatableBytes()}.
+ *
* @hide
*/
public static final int FLAG_ALLOCATE_CACHE_ONLY = 1 << 4;
/** @hide */
- @IntDef(flag = true, prefix = { "FLAG_ALLOCATE_" }, value = {
+ @IntDef(flag = true, prefix = {"FLAG_ALLOCATE_"}, value = {
FLAG_ALLOCATE_AGGRESSIVE,
FLAG_ALLOCATE_DEFY_ALL_RESERVED,
FLAG_ALLOCATE_DEFY_HALF_RESERVED,
@@ -2216,7 +2275,8 @@ public class StorageManager {
FLAG_ALLOCATE_CACHE_ONLY,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface AllocateFlags {}
+ public @interface AllocateFlags {
+ }
/**
* Return the maximum number of new bytes that your app can allocate for
@@ -2246,15 +2306,15 @@ public class StorageManager {
* </p>
*
* @param storageUuid the UUID of the storage volume where you're
- * considering allocating disk space, since allocatable space can
- * vary widely depending on the underlying storage device. The
- * UUID for a specific path can be obtained using
- * {@link #getUuidForPath(File)}.
+ * considering allocating disk space, since allocatable space can
+ * vary widely depending on the underlying storage device. The
+ * UUID for a specific path can be obtained using
+ * {@link #getUuidForPath(File)}.
* @return the maximum number of new bytes that the calling app can allocate
- * using {@link #allocateBytes(UUID, long)} or
- * {@link #allocateBytes(FileDescriptor, long)}.
+ * using {@link #allocateBytes(UUID, long)} or
+ * {@link #allocateBytes(FileDescriptor, long)}.
* @throws IOException when the storage device isn't present, or when it
- * doesn't support allocating space.
+ * doesn't support allocating space.
*/
@WorkerThread
public @BytesLong long getAllocatableBytes(@NonNull UUID storageUuid)
@@ -2297,12 +2357,12 @@ public class StorageManager {
* more than once every 60 seconds.
*
* @param storageUuid the UUID of the storage volume where you'd like to
- * allocate disk space. The UUID for a specific path can be
- * obtained using {@link #getUuidForPath(File)}.
- * @param bytes the number of bytes to allocate.
+ * allocate disk space. The UUID for a specific path can be
+ * obtained using {@link #getUuidForPath(File)}.
+ * @param bytes the number of bytes to allocate.
* @throws IOException when the storage device isn't present, or when it
- * doesn't support allocating space, or if the device had
- * trouble allocating the requested space.
+ * doesn't support allocating space, or if the device had
+ * trouble allocating the requested space.
* @see #getAllocatableBytes(UUID)
*/
@WorkerThread
@@ -2332,10 +2392,9 @@ public class StorageManager {
* These mount modes specify different views and access levels for
* different apps on external storage.
*
+ * @return {@code MountMode} for the given uid and packageName.
* @params uid UID of the application
* @params packageName name of the package
- * @return {@code MountMode} for the given uid and packageName.
- *
* @hide
*/
@RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE)
@@ -2366,15 +2425,15 @@ public class StorageManager {
* (such as when recording a video) you should avoid calling this method
* more than once every 60 seconds.
*
- * @param fd the open file that you'd like to allocate disk space for.
+ * @param fd the open file that you'd like to allocate disk space for.
* @param bytes the number of bytes to allocate. This is the desired final
- * size of the open file. If the open file is smaller than this
- * requested size, it will be extended without modifying any
- * existing contents. If the open file is larger than this
- * requested size, it will be truncated.
+ * size of the open file. If the open file is smaller than this
+ * requested size, it will be extended without modifying any
+ * existing contents. If the open file is larger than this
+ * requested size, it will be truncated.
* @throws IOException when the storage device isn't present, or when it
- * doesn't support allocating space, or if the device had
- * trouble allocating the requested space.
+ * doesn't support allocating space, or if the device had
+ * trouble allocating the requested space.
* @see #isAllocationSupported(FileDescriptor)
* @see Environment#isExternalStorageEmulated(File)
*/
@@ -2499,13 +2558,14 @@ public class StorageManager {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = { "QUOTA_TYPE_" }, value = {
+ @IntDef(prefix = {"QUOTA_TYPE_"}, value = {
QUOTA_TYPE_MEDIA_NONE,
QUOTA_TYPE_MEDIA_AUDIO,
QUOTA_TYPE_MEDIA_VIDEO,
QUOTA_TYPE_MEDIA_IMAGE,
})
- public @interface QuotaType {}
+ public @interface QuotaType {
+ }
private static native boolean setQuotaProjectId(String path, long projectId);
@@ -2532,15 +2592,13 @@ public class StorageManager {
* The default platform user of this API is the MediaProvider process, which is
* responsible for managing all of external storage.
*
- * @param path the path to the file for which we should update the quota type
+ * @param path the path to the file for which we should update the quota type
* @param quotaType the quota type of the file; this is based on the
* {@code QuotaType} constants, eg
* {@code StorageManager.QUOTA_TYPE_MEDIA_AUDIO}
- *
* @throws IllegalArgumentException if {@code quotaType} does not correspond to a valid
* quota type.
* @throws IOException if the quota type could not be updated.
- *
* @hide
*/
@SystemApi
@@ -2616,7 +2674,6 @@ public class StorageManager {
* permissions of a directory to what they should anyway be.
*
* @param path the path for which we should fix up the permissions
- *
* @hide
*/
public void fixupAppDir(@NonNull File path) {
@@ -2822,11 +2879,12 @@ public class StorageManager {
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = { "APP_IO_BLOCKED_REASON_" }, value = {
- APP_IO_BLOCKED_REASON_TRANSCODING,
- APP_IO_BLOCKED_REASON_UNKNOWN,
+ @IntDef(prefix = {"APP_IO_BLOCKED_REASON_"}, value = {
+ APP_IO_BLOCKED_REASON_TRANSCODING,
+ APP_IO_BLOCKED_REASON_UNKNOWN,
})
- public @interface AppIoBlockedReason {}
+ public @interface AppIoBlockedReason {
+ }
/**
* Notify the system that an app with {@code uid} and {@code tid} is blocked on an IO request on
@@ -2839,10 +2897,9 @@ public class StorageManager {
* {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
*
* @param volumeUuid the UUID of the storage volume that the app IO is blocked on
- * @param uid the UID of the app blocked on IO
- * @param tid the tid of the app blocked on IO
- * @param reason the reason the app is blocked on IO
- *
+ * @param uid the UID of the app blocked on IO
+ * @param tid the tid of the app blocked on IO
+ * @param reason the reason the app is blocked on IO
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2866,10 +2923,9 @@ public class StorageManager {
* {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
*
* @param volumeUuid the UUID of the storage volume that the app IO is resumed on
- * @param uid the UID of the app resuming IO
- * @param tid the tid of the app resuming IO
- * @param reason the reason the app is resuming IO
- *
+ * @param uid the UID of the app resuming IO
+ * @param tid the tid of the app resuming IO
+ * @param reason the reason the app is resuming IO
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -2890,10 +2946,9 @@ public class StorageManager {
* {@link android.Manifest.permission#WRITE_MEDIA_STORAGE} permission.
*
* @param volumeUuid the UUID of the storage volume to check IO blocked status
- * @param uid the UID of the app to check IO blocked status
- * @param tid the tid of the app to check IO blocked status
- * @param reason the reason to check IO blocked status for
- *
+ * @param uid the UID of the app to check IO blocked status
+ * @param tid the tid of the app to check IO blocked status
+ * @param reason the reason to check IO blocked status for
* @hide
*/
@TestApi
@@ -2962,7 +3017,6 @@ public class StorageManager {
* information is available, -1 is returned.
*
* @return Percentage of the remaining useful lifetime of the internal storage device.
- *
* @hide
*/
@FlaggedApi(Flags.FLAG_STORAGE_LIFETIME_API)
diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java
index 195896dc8edf..0e78bfdb5069 100644
--- a/core/java/android/view/InputEventConsistencyVerifier.java
+++ b/core/java/android/view/InputEventConsistencyVerifier.java
@@ -180,7 +180,7 @@ public final class InputEventConsistencyVerifier {
final MotionEvent motionEvent = (MotionEvent)event;
if (motionEvent.isTouchEvent()) {
onTouchEvent(motionEvent, nestingLevel);
- } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
+ } else if (motionEvent.isFromSource(InputDevice.SOURCE_TRACKBALL)) {
onTrackballEvent(motionEvent, nestingLevel);
} else {
onGenericMotionEvent(motionEvent, nestingLevel);
diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java
index 1c36eaf99afa..9c1f134bff3e 100644
--- a/core/java/android/view/InputEventReceiver.java
+++ b/core/java/android/view/InputEventReceiver.java
@@ -290,9 +290,15 @@ public abstract class InputEventReceiver {
@SuppressWarnings("unused")
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void dispatchInputEvent(int seq, InputEvent event) {
- Trace.traceBegin(Trace.TRACE_TAG_INPUT, "dispatchInputEvent " + getShortDescription(event));
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_INPUT)) {
+ // This 'if' block is an optimization - without it, 'getShortDescription' will be
+ // called unconditionally, which is expensive.
+ Trace.traceBegin(Trace.TRACE_TAG_INPUT,
+ "dispatchInputEvent " + getShortDescription(event));
+ }
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
+ // If tracing is not enabled, `traceEnd` is a no-op (so we don't need to guard it with 'if')
Trace.traceEnd(Trace.TRACE_TAG_INPUT);
}
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index e8e66210bca6..945975a88cd5 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -32,6 +32,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
@@ -168,8 +169,9 @@ public class InsetsSourceConsumer {
// Reset the applier to the default one which has the most lightweight implementation.
setSurfaceParamsApplier(InsetsAnimationControlRunner.SurfaceParamsApplier.DEFAULT);
} else {
- if (lastControl != null && InsetsSource.getInsetSide(lastControl.getInsetsHint())
- != InsetsSource.getInsetSide(control.getInsetsHint())) {
+ if (lastControl != null && !Insets.NONE.equals(lastControl.getInsetsHint())
+ && InsetsSource.getInsetSide(lastControl.getInsetsHint())
+ != InsetsSource.getInsetSide(control.getInsetsHint())) {
// The source has been moved to a different side. The coordinates are stale.
// Canceling existing animation if there is any.
cancelTypes[0] |= mType;
diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java
index 5e1eadae0953..331e34526ae8 100644
--- a/core/java/android/view/RoundScrollbarRenderer.java
+++ b/core/java/android/view/RoundScrollbarRenderer.java
@@ -20,6 +20,7 @@ import static android.util.MathUtils.acos;
import static java.lang.Math.sin;
+import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
@@ -40,9 +41,9 @@ public class RoundScrollbarRenderer {
// The range of the scrollbar position represented as an angle in degrees.
private static final float SCROLLBAR_ANGLE_RANGE = 28.8f;
- private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90%
- private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 3.1f; // 10%
- private static final float THUMB_WIDTH_DP = 4f;
+ private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 0.7f * SCROLLBAR_ANGLE_RANGE;
+ private static final float MIN_SCROLLBAR_ANGLE_SWIPE = 0.3f * SCROLLBAR_ANGLE_RANGE;
+ private static final float GAP_BETWEEN_TRACK_AND_THUMB_DP = 3f;
private static final float OUTER_PADDING_DP = 2f;
private static final int DEFAULT_THUMB_COLOR = 0xFFFFFFFF;
private static final int DEFAULT_TRACK_COLOR = 0x4CFFFFFF;
@@ -57,14 +58,16 @@ public class RoundScrollbarRenderer {
private final RectF mRect = new RectF();
private final View mParent;
private final float mInset;
+ private final float mGapBetweenThumbAndTrackPx;
+ private final boolean mUseRefactoredRoundScrollbar;
private float mPreviousMaxScroll = 0;
private float mMaxScrollDiff = 0;
private float mPreviousCurrentScroll = 0;
private float mCurrentScrollDiff = 0;
private float mThumbStrokeWidthAsDegrees = 0;
+ private float mGapBetweenTrackAndThumbAsDegrees = 0;
private boolean mDrawToLeft;
- private boolean mUseRefactoredRoundScrollbar;
public RoundScrollbarRenderer(View parent) {
// Paints for the round scrollbar.
@@ -80,16 +83,17 @@ public class RoundScrollbarRenderer {
mParent = parent;
+ Resources resources = parent.getContext().getResources();
// Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same
// way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so
// that it doesn't get clipped.
int maskThickness =
- parent.getContext()
- .getResources()
- .getDimensionPixelSize(
- com.android.internal.R.dimen.circular_display_mask_thickness);
+ resources.getDimensionPixelSize(
+ com.android.internal.R.dimen.circular_display_mask_thickness);
- float thumbWidth = dpToPx(THUMB_WIDTH_DP);
+ float thumbWidth =
+ resources.getDimensionPixelSize(com.android.internal.R.dimen.round_scrollbar_width);
+ mGapBetweenThumbAndTrackPx = dpToPx(GAP_BETWEEN_TRACK_AND_THUMB_DP);
mThumbPaint.setStrokeWidth(thumbWidth);
mTrackPaint.setStrokeWidth(thumbWidth);
mInset = thumbWidth / 2 + maskThickness;
@@ -175,7 +179,6 @@ public class RoundScrollbarRenderer {
}
}
- /** Returns true if horizontal bounds are updated */
private void updateBounds(Rect bounds) {
mRect.set(
bounds.left + mInset,
@@ -184,6 +187,8 @@ public class RoundScrollbarRenderer {
bounds.bottom - mInset);
mThumbStrokeWidthAsDegrees =
getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f);
+ mGapBetweenTrackAndThumbAsDegrees =
+ getVertexAngle((mRect.right - mRect.left) / 2f, mGapBetweenThumbAndTrackPx);
}
private float computeSweepAngle(float scrollExtent, float maxScroll) {
@@ -262,20 +267,22 @@ public class RoundScrollbarRenderer {
// The highest point of the top track on a vertical scale. Here the thumb width is
// reduced to account for the arc formed by ROUND stroke style
-SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees,
- // The lowest point of the top track on a vertical scale. Here the thumb width is
- // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap
- // between thumb and top track
- thumbStartAngle - mThumbStrokeWidthAsDegrees * 2,
+ // The lowest point of the top track on a vertical scale. It's reduced by
+ // (a) angular distance for the arc formed by ROUND stroke style
+ // (b) gap between thumb and top track
+ thumbStartAngle - mThumbStrokeWidthAsDegrees - mGapBetweenTrackAndThumbAsDegrees,
alpha);
// Draws the thumb
drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint);
// Draws the bottom arc
drawTrack(
canvas,
- // The highest point of the bottom track on a vertical scale. Here the thumb width
- // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap
- // between thumb and bottom track
- (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2,
+ // The highest point of the bottom track on a vertical scale. Following added to it
+ // (a) angular distance for the arc formed by ROUND stroke style
+ // (b) gap between thumb and top track
+ (thumbStartAngle + thumbSweepAngle)
+ + mThumbStrokeWidthAsDegrees
+ + mGapBetweenTrackAndThumbAsDegrees,
// The lowest point of the top track on a vertical scale. Here the thumb width is
// added to account for the arc formed by ROUND stroke style
SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees,
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 900f22d2b37b..0d6f82773622 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -133,6 +133,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme;
import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
+import static com.android.window.flags.Flags.enableWindowContextResourcesUpdateOnConfigChange;
import static com.android.window.flags.Flags.predictiveBackSwipeEdgeNoneApi;
import static com.android.window.flags.Flags.setScPropertiesInClient;
@@ -271,7 +272,9 @@ import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import android.window.ScreenCapture;
import android.window.SurfaceSyncGroup;
+import android.window.WindowContext;
import android.window.WindowOnBackInvokedDispatcher;
+import android.window.WindowTokenClient;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -6609,12 +6612,26 @@ public final class ViewRootImpl implements ViewParent,
mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId,
activityWindowInfo);
} else {
- // There is no activity callback - update the configuration right away.
+ if (enableWindowContextResourcesUpdateOnConfigChange()) {
+ // There is no activity callback - update resources for window token, if needed.
+ final WindowTokenClient windowTokenClient = getWindowTokenClient();
+ if (windowTokenClient != null) {
+ windowTokenClient.onConfigurationChanged(
+ mLastReportedMergedConfiguration.getMergedConfiguration(),
+ newDisplayId == INVALID_DISPLAY ? mDisplay.getDisplayId()
+ : newDisplayId);
+ }
+ }
updateConfiguration(newDisplayId);
}
mForceNextConfigUpdate = false;
}
+ private WindowTokenClient getWindowTokenClient() {
+ if (!(mContext instanceof WindowContext)) return null;
+ return (WindowTokenClient) mContext.getWindowContextToken();
+ }
+
/**
* Update display and views if last applied merged configuration changed.
* @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} otherwise.
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 101d5c950b71..edfa1d5aea1f 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -625,6 +625,12 @@ public interface WindowManager extends ViewManager {
int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000
/**
+ * Transition flag: Indicates that aod is showing hidden by entering doze
+ * @hide
+ */
+ int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000
+
+ /**
* @hide
*/
@IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -643,6 +649,7 @@ public interface WindowManager extends ViewManager {
TRANSIT_FLAG_KEYGUARD_OCCLUDING,
TRANSIT_FLAG_KEYGUARD_UNOCCLUDING,
TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH,
+ TRANSIT_FLAG_AOD_APPEARING,
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionFlags {}
@@ -659,7 +666,8 @@ public interface WindowManager extends ViewManager {
(TRANSIT_FLAG_KEYGUARD_GOING_AWAY
| TRANSIT_FLAG_KEYGUARD_APPEARING
| TRANSIT_FLAG_KEYGUARD_OCCLUDING
- | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING);
+ | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING
+ | TRANSIT_FLAG_AOD_APPEARING);
/**
* Remove content mode: Indicates remove content mode is currently not defined.
diff --git a/core/java/android/view/accessibility/OWNERS b/core/java/android/view/accessibility/OWNERS
index f62b33f1f753..799ef0091f71 100644
--- a/core/java/android/view/accessibility/OWNERS
+++ b/core/java/android/view/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
# Android Accessibility Framework owners
include /services/accessibility/OWNERS
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 0fb80422833c..56f0415b40cc 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -3778,8 +3778,32 @@ public final class InputMethodManager {
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
if (Flags.refactorInsetsController()) {
- mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(),
- false /* fromIme */, statsToken);
+ synchronized (mH) {
+ Handler vh = rootView.getHandler();
+ if (vh == null) {
+ // If the view doesn't have a handler, something has changed out from
+ // under us.
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE);
+ return;
+ }
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_CLIENT_VIEW_HANDLER_AVAILABLE);
+
+ if (vh.getLooper() != Looper.myLooper()) {
+ // The view is running on a different thread than our own, so
+ // we need to reschedule our work for over there.
+ if (DEBUG) {
+ Log.v(TAG, "Close current input: reschedule hide to view thread");
+ }
+ final var viewRootImpl = mCurRootView;
+ vh.post(() -> viewRootImpl.getInsetsController().hide(
+ WindowInsets.Type.ime(), false /* fromIme */, statsToken));
+ } else {
+ mCurRootView.getInsetsController().hide(WindowInsets.Type.ime(),
+ false /* fromIme */, statsToken);
+ }
+ }
} else {
IInputMethodManagerGlobalInvoker.hideSoftInput(
mClient,
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 785246074cee..1ce5df7cd137 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -55,6 +55,7 @@ public enum DesktopModeFlags {
Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true),
ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix,
true),
+ ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false),
ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true),
ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX(
Flags::enableDesktopRecentsTransitionsCornersBugfix, false),
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index ddbf9e49bb8d..cf21e50e0a19 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -29,6 +29,7 @@ import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_NONE;
@@ -405,7 +406,8 @@ public final class TransitionInfo implements Parcelable {
*/
public boolean hasChangesOrSideEffects() {
return !mChanges.isEmpty() || isKeyguardGoingAway()
- || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0;
+ || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
+ || (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0;
}
/**
@@ -1289,12 +1291,13 @@ public final class TransitionInfo implements Parcelable {
return options;
}
- /** Make options for a scale-up animation. */
+ /** Make options for a scale-up animation with task override option */
@NonNull
public static AnimationOptions makeScaleUpAnimOptions(int startX, int startY, int width,
- int height) {
+ int height, boolean overrideTaskTransition) {
AnimationOptions options = new AnimationOptions(ANIM_SCALE_UP);
options.mTransitionBounds.set(startX, startY, startX + width, startY + height);
+ options.mOverrideTaskTransition = overrideTaskTransition;
return options;
}
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index a551fe701c5b..f7bee619bc4b 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -106,7 +106,6 @@ public class WindowTokenClient extends Binder {
* @param newConfig the updated {@link Configuration}
* @param newDisplayId the updated {@link android.view.Display} ID
*/
- @VisibleForTesting(visibility = PACKAGE)
@MainThread
public void onConfigurationChanged(Configuration newConfig, int newDisplayId) {
onConfigurationChanged(newConfig, newDisplayId, true /* shouldReportConfigChange */);
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index d413ba0b042c..b805ac560b8d 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -572,6 +572,13 @@ flag {
}
flag {
+ name: "enable_display_reconnect_interaction"
+ namespace: "lse_desktop_experience"
+ description: "Enables new interaction that occurs when a display is reconnected."
+ bug: "365873835"
+}
+
+flag {
name: "show_desktop_experience_dev_option"
namespace: "lse_desktop_experience"
description: "Replace the freeform windowing dev options with a desktop experience one."
@@ -670,6 +677,17 @@ flag {
}
flag {
+ name: "enable_window_context_resources_update_on_config_change"
+ namespace: "lse_desktop_experience"
+ description: "Updates window context resources before the view receives the config change callback."
+ bug: "394527409"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_desktop_tab_tearing_minimize_animation_bugfix"
namespace: "lse_desktop_experience"
description: "Enabling a minimize animation when a new window is opened via tab tearing and the Desktop Windowing open windows limit is reached."
@@ -677,4 +695,14 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-} \ No newline at end of file
+}
+
+flag {
+ name: "enable_desktop_close_shortcut_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Fix the window-close keyboard shortcut in Desktop Mode."
+ bug: "394599430"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/accessibility/OWNERS b/core/java/com/android/internal/accessibility/OWNERS
index 1265dfa2c441..dac64f47ba7e 100644
--- a/core/java/com/android/internal/accessibility/OWNERS
+++ b/core/java/com/android/internal/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
# Android Accessibility Framework owners
include /services/accessibility/OWNERS \ No newline at end of file
diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl
index a5e166b95177..5f1a641945e8 100644
--- a/core/java/com/android/internal/app/IBatteryStats.aidl
+++ b/core/java/com/android/internal/app/IBatteryStats.aidl
@@ -35,8 +35,20 @@ import android.telephony.SignalStrength;
interface IBatteryStats {
/** @hide */
+ const int RESULT_OK = 0;
+
+ /** @hide */
+ const int RESULT_RUNTIME_EXCEPTION = 1;
+
+ /** @hide */
+ const int RESULT_SECURITY_EXCEPTION = 2;
+
+ /** @hide */
const String KEY_UID_SNAPSHOTS = "uid_snapshots";
+ /** @hide */
+ const String KEY_EXCEPTION_MESSAGE = "exception";
+
// These first methods are also called by native code, so must
// be kept in sync with frameworks/native/libs/binder/include_batterystats/batterystats/IBatteryStats.h
@EnforcePermission("UPDATE_DEVICE_STATS")
diff --git a/core/java/com/android/internal/graphics/palette/OWNERS b/core/java/com/android/internal/graphics/palette/OWNERS
index 731dca9b128f..df867252c01c 100644
--- a/core/java/com/android/internal/graphics/palette/OWNERS
+++ b/core/java/com/android/internal/graphics/palette/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 484670
-dupin@google.com
-jamesoleary@google.com \ No newline at end of file
+# Bug component: 484670
+dupin@google.com
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 7c5335cc753c..9085bbec949f 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -306,8 +306,15 @@ public class Cuj {
/** Track work utility view animation shrinking when scrolling down app list. */
public static final int CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK = 127;
+ /**
+ * Track task transitions
+ *
+ * <p>Tracking starts and ends with the animation.</p>
+ */
+ public static final int CUJ_DEFAULT_TASK_TO_TASK_ANIMATION = 128;
+
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
- @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK;
+ @VisibleForTesting static final int LAST_CUJ = CUJ_DEFAULT_TASK_TO_TASK_ANIMATION;
/** @hide */
@IntDef({
@@ -426,7 +433,8 @@ public class Cuj {
CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON,
CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND,
- CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK
+ CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK,
+ CUJ_DEFAULT_TASK_TO_TASK_ANIMATION
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {}
@@ -556,6 +564,7 @@ public class Cuj {
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_EXPAND;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_SHRINK;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DEFAULT_TASK_TO_TASK_ANIMATION] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DEFAULT_TASK_TO_TASK_ANIMATION;
}
private Cuj() {
@@ -806,6 +815,8 @@ public class Cuj {
return "LAUNCHER_WORK_UTILITY_VIEW_EXPAND";
case CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK:
return "LAUNCHER_WORK_UTILITY_VIEW_SHRINK";
+ case CUJ_DEFAULT_TASK_TO_TASK_ANIMATION:
+ return "CUJ_DEFAULT_TASK_TO_TASK_ANIMATION";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 270cf085b06f..e20a52b24485 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -231,6 +231,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
private int mLastRightInset = 0;
@UnsupportedAppUsage
private int mLastLeftInset = 0;
+ private WindowInsets mLastInsets = null;
private boolean mLastHasTopStableInset = false;
private boolean mLastHasBottomStableInset = false;
private boolean mLastHasRightStableInset = false;
@@ -1100,6 +1101,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mLastWindowFlags = attrs.flags;
if (insets != null) {
+ mLastInsets = insets;
mLastForceConsumingTypes = insets.getForceConsumingTypes();
mLastForceConsumingOpaqueCaptionBar = insets.isForceConsumingOpaqueCaptionBar();
@@ -1176,6 +1178,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mForceWindowDrawsBarBackgrounds, requestedVisibleTypes);
}
+ int consumingTypes = 0;
// When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS or
// mForceWindowDrawsBarBackgrounds, we still need to ensure that the rest of the view
// hierarchy doesn't notice it, unless they've explicitly asked for it.
@@ -1186,43 +1189,47 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
//
// Note: Once the app uses the R+ Window.setDecorFitsSystemWindows(false) API we no longer
// consume insets because they might no longer set SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION.
- boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+ final boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
|| (requestedVisibleTypes & WindowInsets.Type.navigationBars()) == 0;
- boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
- boolean forceConsumingNavBar =
+ final boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
+
+ final boolean fitsNavBar =
+ (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
+ && decorFitsSystemWindows
+ && !hideNavigation;
+ final boolean forceConsumingNavBar =
((mForceWindowDrawsBarBackgrounds || mDrawLegacyNavigationBarBackgroundHandled)
&& (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
- && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
- && decorFitsSystemWindows
- && !hideNavigation)
+ && fitsNavBar)
|| ((mLastForceConsumingTypes & WindowInsets.Type.navigationBars()) != 0
&& hideNavigation);
-
- boolean consumingNavBar =
- ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
- && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
- && decorFitsSystemWindows
- && !hideNavigation)
+ final boolean consumingNavBar =
+ ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 && fitsNavBar)
|| forceConsumingNavBar;
+ if (consumingNavBar) {
+ consumingTypes |= WindowInsets.Type.navigationBars();
+ }
- // If we didn't request fullscreen layout, but we still got it because of the
- // mForceWindowDrawsBarBackgrounds flag, also consume top inset.
+ // If the fullscreen layout was not requested, but still received because of the
+ // mForceWindowDrawsBarBackgrounds flag, also consume status bar.
// If we should always consume system bars, only consume that if the app wanted to go to
// fullscreen, as otherwise we can expect the app to handle it.
- boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
+ final boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
|| (attrs.flags & FLAG_FULLSCREEN) != 0;
final boolean hideStatusBar = fullscreen
|| (requestedVisibleTypes & WindowInsets.Type.statusBars()) == 0;
- boolean consumingStatusBar =
- ((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
- && decorFitsSystemWindows
- && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
- && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
- && mForceWindowDrawsBarBackgrounds
- && mLastTopInset != 0)
+ if (((sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
+ && decorFitsSystemWindows
+ && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
+ && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
+ && mForceWindowDrawsBarBackgrounds
+ && mLastTopInset != 0)
|| ((mLastForceConsumingTypes & WindowInsets.Type.statusBars()) != 0
- && hideStatusBar);
+ && hideStatusBar)) {
+ consumingTypes |= WindowInsets.Type.statusBars();
+ }
+ // Decide if caption bar need to be consumed
final boolean hideCaptionBar = fullscreen
|| (requestedVisibleTypes & WindowInsets.Type.captionBar()) == 0;
final boolean consumingCaptionBar =
@@ -1237,22 +1244,23 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
&& mLastForceConsumingOpaqueCaptionBar
&& isOpaqueCaptionBar;
- final int consumedTop =
- (consumingStatusBar || consumingCaptionBar || consumingOpaqueCaptionBar)
- ? mLastTopInset : 0;
- int consumedRight = consumingNavBar ? mLastRightInset : 0;
- int consumedBottom = consumingNavBar ? mLastBottomInset : 0;
- int consumedLeft = consumingNavBar ? mLastLeftInset : 0;
+ if (consumingCaptionBar || consumingOpaqueCaptionBar) {
+ consumingTypes |= WindowInsets.Type.captionBar();
+ }
+
+ final Insets consumedInsets = mLastInsets != null
+ ? mLastInsets.getInsets(consumingTypes) : Insets.NONE;
if (mContentRoot != null
&& mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
- if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
- || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
- lp.topMargin = consumedTop;
- lp.rightMargin = consumedRight;
- lp.bottomMargin = consumedBottom;
- lp.leftMargin = consumedLeft;
+ if (lp.topMargin != consumedInsets.top || lp.rightMargin != consumedInsets.right
+ || lp.bottomMargin != consumedInsets.bottom || lp.leftMargin !=
+ consumedInsets.left) {
+ lp.topMargin = consumedInsets.top;
+ lp.rightMargin = consumedInsets.right;
+ lp.bottomMargin = consumedInsets.bottom;
+ lp.leftMargin = consumedInsets.left;
mContentRoot.setLayoutParams(lp);
if (insets == null) {
@@ -1261,11 +1269,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
requestApplyInsets();
}
}
- if (insets != null && (consumedLeft > 0
- || consumedTop > 0
- || consumedRight > 0
- || consumedBottom > 0)) {
- insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom);
+ if (insets != null && !Insets.NONE.equals(consumedInsets)) {
+ insets = insets.inset(consumedInsets);
}
}
diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
index b7e68bacd143..260619ec0b23 100644
--- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
+++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.os.Handler;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -160,8 +161,13 @@ public class GestureNavigationSettingsObserver extends ContentObserver {
}
public boolean areNavigationButtonForcedVisible() {
- return Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0;
+ String SUWTheme = SystemProperties.get("setupwizard.theme", "");
+ boolean isExpressiveThemeEnabled = SUWTheme.equals("glif_expressive")
+ || SUWTheme.equals("glif_expressive_light");
+ // The back gesture is enabled if using the expressive theme
+ return !isExpressiveThemeEnabled
+ && Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0;
}
private float getUnscaledInset(Resources userRes) {
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 05a33fe830e8..d8cf258e23ba 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -160,19 +160,21 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen
Objects.requireNonNull(mConfigurationService,
"A null ProtoLog Configuration Service was provided!");
- try {
- var args = createConfigurationServiceRegisterClientArgs();
+ mBackgroundLoggingService.execute(() -> {
+ try {
+ var args = createConfigurationServiceRegisterClientArgs();
- final var groupArgs = mLogGroups.values().stream()
- .map(group -> new RegisterClientArgs
- .GroupConfig(group.name(), group.isLogToLogcat()))
- .toArray(RegisterClientArgs.GroupConfig[]::new);
- args.setGroups(groupArgs);
+ final var groupArgs = mLogGroups.values().stream()
+ .map(group -> new RegisterClientArgs
+ .GroupConfig(group.name(), group.isLogToLogcat()))
+ .toArray(RegisterClientArgs.GroupConfig[]::new);
+ args.setGroups(groupArgs);
- mConfigurationService.registerClient(this, args);
- } catch (RemoteException e) {
- throw new RuntimeException("Failed to register ProtoLog client");
- }
+ mConfigurationService.registerClient(this, args);
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed to register ProtoLog client");
+ }
+ });
}
/**
diff --git a/core/java/com/android/internal/protolog/WmProtoLogGroups.java b/core/java/com/android/internal/protolog/WmProtoLogGroups.java
index 4bd5d24b71e2..5edc2fbd4c8f 100644
--- a/core/java/com/android/internal/protolog/WmProtoLogGroups.java
+++ b/core/java/com/android/internal/protolog/WmProtoLogGroups.java
@@ -100,6 +100,8 @@ public enum WmProtoLogGroups implements IProtoLogGroup {
WM_DEBUG_TPL(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM),
WM_DEBUG_EMBEDDED_WINDOWS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM),
+ WM_DEBUG_PRESENTATION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ Consts.TAG_WM),
TEST_GROUP(true, true, false, "WindowManagerProtoLogTest");
private final boolean mEnabled;
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index d8f1b626abf2..31b9fd1ad170 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -284,6 +284,8 @@ void NativeDisplayEventReceiver::dispatchModeRejected(PhysicalDisplayId displayI
displayId.value, modeId);
ALOGV("receiver %p ~ Returned from Mode Rejected handler.", this);
}
+
+ mMessageQueue->raiseAndClearException(env, "dispatchModeRejected");
}
void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
@@ -314,7 +316,7 @@ void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
ALOGV("receiver %p ~ Returned from FrameRateOverride handler.", this);
}
- mMessageQueue->raiseAndClearException(env, "dispatchModeChanged");
+ mMessageQueue->raiseAndClearException(env, "dispatchFrameRateOverrides");
}
void NativeDisplayEventReceiver::dispatchHdcpLevelsChanged(PhysicalDisplayId displayId,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c9f4cdc8e3ce..51049889ecd6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -9385,6 +9385,10 @@
android:permission="android.permission.BIND_JOB_SERVICE">
</service>
+ <service android:name="com.android.server.security.UpdateCertificateRevocationStatusJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
<service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader"
android:exported="false">
<intent-filter>
diff --git a/core/res/res/values-w225dp/dimens.xml b/core/res/res/values-w225dp/dimens.xml
new file mode 100644
index 000000000000..0cd3293f0894
--- /dev/null
+++ b/core/res/res/values-w225dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- The width of the round scrollbar -->
+ <dimen name="round_scrollbar_width">6dp</dimen>
+</resources>
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index 4ff3f8825cc4..ef5875eff06f 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -110,4 +110,8 @@
tap power gesture from triggering the selected target action.
-->
<integer name="config_doubleTapPowerGestureMode">0</integer>
+
+ <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This
+ config enables OEMs to support its usage across tasks.-->
+ <bool name="config_enableCrossTaskScaleUpAnimation">true</bool>
</resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8db94a420e4c..17acf9aed278 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3262,6 +3262,14 @@
as private. {@see android.view.Display#FLAG_PRIVATE} -->
<integer-array translatable="false" name="config_localPrivateDisplayPorts"></integer-array>
+ <!-- Controls if local secondary displays should be able to steal focus and become top display.
+ Value specified in the array represents physical port address of each display and displays
+ in this list due to flag dependencies will be marked with the following flags:
+ {@see android.view.Display#FLAG_STEAL_TOP_FOCUS_DISABLED}
+ {@see android.view.Display#FLAG_OWN_FOCUS} -->
+ <integer-array translatable="false" name="config_localNotStealTopFocusDisplayPorts">
+ </integer-array>
+
<!-- The default mode for the default display. One of the following values (See Display.java):
0 - COLOR_MODE_DEFAULT
7 - COLOR_MODE_SRGB
@@ -7248,6 +7256,9 @@
<!-- Wear devices: An intent action that is used for remote intent. -->
<string name="config_wearRemoteIntentAction" translatable="false" />
+ <!-- Whether the current device's internal display can host desktop sessions. -->
+ <bool name="config_canInternalDisplayHostDesktops">false</bool>
+
<!-- Whether desktop mode is supported on the current device -->
<bool name="config_isDesktopModeSupported">false</bool>
@@ -7340,4 +7351,23 @@
<!-- Array containing the notification assistant service adjustments that are not supported by
default on this device-->
<string-array translatable="false" name="config_notificationDefaultUnsupportedAdjustments" />
+
+ <!-- Preference name of bugreport-->
+ <string name="prefs_bugreport" translatable="false">bugreports</string>
+
+ <!-- key value of warning state stored in bugreport preference-->
+ <string name="key_warning_state" translatable="false">warning-state</string>
+
+ <!-- Bugreport warning dialog state unknown-->
+ <integer name="bugreport_state_unknown">0</integer>
+
+ <!-- Bugreport warning dialog state shows the warning dialog-->
+ <integer name="bugreport_state_show">1</integer>
+
+ <!-- Bugreport warning dialog state skips the warning dialog-->
+ <integer name="bugreport_state_hide">2</integer>
+
+ <!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This
+ config enables OEMs to support its usage across tasks.-->
+ <bool name="config_enableCrossTaskScaleUpAnimation">false</bool>
</resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 484e8ef1e049..595160ec9f66 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -782,6 +782,9 @@
aliasing effects). This is only used on circular displays. -->
<dimen name="circular_display_mask_thickness">1px</dimen>
+ <!-- The width of the round scrollbar -->
+ <dimen name="round_scrollbar_width">5dp</dimen>
+
<dimen name="lock_pattern_dot_line_width">22dp</dimen>
<dimen name="lock_pattern_dot_size">14dp</dimen>
<dimen name="lock_pattern_dot_size_activated">30dp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8c2ca97af493..cc2897a2779e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -430,6 +430,7 @@
<java-symbol type="bool" name="config_enableProximityService" />
<java-symbol type="bool" name="config_enableVirtualDeviceManager" />
<java-symbol type="array" name="config_localPrivateDisplayPorts" />
+ <java-symbol type="array" name="config_localNotStealTopFocusDisplayPorts" />
<java-symbol type="integer" name="config_defaultDisplayDefaultColorMode" />
<java-symbol type="bool" name="config_enableAppWidgetService" />
<java-symbol type="dimen" name="config_pictureInPictureMinAspectRatio" />
@@ -585,6 +586,7 @@
<java-symbol type="dimen" name="accessibility_magnification_indicator_width" />
<java-symbol type="dimen" name="circular_display_mask_thickness" />
<java-symbol type="dimen" name="user_icon_size" />
+ <java-symbol type="dimen" name="round_scrollbar_width" />
<java-symbol type="string" name="add_account_button_label" />
<java-symbol type="string" name="addToDictionary" />
@@ -5752,6 +5754,9 @@
<!-- Whether the developer option for desktop mode is supported on the current device -->
<java-symbol type="bool" name="config_isDesktopModeDevOptionSupported" />
+ <!-- Whether the current device's internal display can host desktop sessions. -->
+ <java-symbol type="bool" name="config_canInternalDisplayHostDesktops" />
+
<!-- Maximum number of active tasks on a given Desktop Windowing session. Set to 0 for unlimited. -->
<java-symbol type="integer" name="config_maxDesktopWindowingActiveTasks"/>
@@ -5900,4 +5905,14 @@
<java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" />
<java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_title" />
<java-symbol type="string" name="usb_apm_usb_suspicious_activity_notification_text" />
+
+ <java-symbol type="string" name="prefs_bugreport" />
+ <java-symbol type="string" name="key_warning_state" />
+ <java-symbol type="integer" name="bugreport_state_unknown" />
+ <java-symbol type="integer" name="bugreport_state_show" />
+ <java-symbol type="integer" name="bugreport_state_hide" />
+
+ <!-- Enable OEMs to support scale up anim across tasks.-->
+ <java-symbol type="bool" name="config_enableCrossTaskScaleUpAnimation" />
+
</resources>
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
index fdfb0c34cdeb..fa1948d9786c 100644
--- a/core/tests/coretests/src/android/content/IntentTest.java
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.os.Binder;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
@@ -238,4 +239,42 @@ public class IntentTest {
// Not all keys from intent are kept - clip data keys are dropped.
assertFalse(intent.getExtraIntentKeys().containsAll(originalIntentKeys));
}
+
+ @Test
+ public void testSetIntentExtrasClassLoaderEffectiveAfterExtraBundleUnparcel() {
+ Intent intent = new Intent();
+ intent.putExtra("bundle", new Bundle());
+
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Intent target = new Intent();
+ target.readFromParcel(parcel);
+ target.collectExtraIntentKeys();
+ ClassLoader cl = new ClassLoader() {
+ };
+ target.setExtrasClassLoader(cl);
+ assertThat(target.getBundleExtra("bundle").getClassLoader()).isEqualTo(cl);
+ }
+
+ @Test
+ public void testBundlePutAllClassLoader() {
+ Intent intent = new Intent();
+ Bundle b1 = new Bundle();
+ b1.putBundle("bundle", new Bundle());
+ intent.putExtra("b1", b1);
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Intent target = new Intent();
+ target.readFromParcel(parcel);
+
+ ClassLoader cl = new ClassLoader() {
+ };
+ target.setExtrasClassLoader(cl);
+ Bundle b2 = new Bundle();
+ b2.putAll(target.getBundleExtra("b1"));
+ assertThat(b2.getBundle("bundle").getClassLoader()).isEqualTo(cl);
+ }
+
}
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 8fa510381060..dc2f0a69375d 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -307,8 +307,10 @@ public class DisplayManagerGlobalTest {
assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED,
mDisplayManagerGlobal
.mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_ADDED, 0));
- assertEquals(DISPLAY_CHANGE_EVENTS, mDisplayManagerGlobal
- .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, 0));
+ assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED,
+ mDisplayManagerGlobal
+ .mapFiltersToInternalEventFlag(DisplayManager.EVENT_TYPE_DISPLAY_CHANGED,
+ 0));
assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED,
mDisplayManagerGlobal.mapFiltersToInternalEventFlag(
DisplayManager.EVENT_TYPE_DISPLAY_REMOVED, 0));
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 45d66e8ee3a9..e6361e10cfa7 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -123,15 +123,21 @@ public class InsetsSourceConsumerTest {
@Test
public void testSetControl_cancelAnimation() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- final InsetsSourceControl newControl = new InsetsSourceControl(mConsumer.getControl());
+ final int[] cancelTypes = {0};
- // Change the side of the insets hint.
- newControl.setInsetsHint(Insets.of(0, 0, 0, 100));
+ // Change the side of the insets hint from NONE to BOTTOM.
+ final InsetsSourceControl newControl1 = new InsetsSourceControl(mConsumer.getControl());
+ newControl1.setInsetsHint(Insets.of(0, 0, 0, 100));
+ mConsumer.setControl(newControl1, new int[1], new int[1], cancelTypes, new int[1]);
- int[] cancelTypes = {0};
- mConsumer.setControl(newControl, new int[1], new int[1], cancelTypes, new int[1]);
+ assertEquals("The animation must not be cancelled", 0, cancelTypes[0]);
- assertEquals(statusBars(), cancelTypes[0]);
+ // Change the side of the insets hint from BOTTOM to TOP.
+ final InsetsSourceControl newControl2 = new InsetsSourceControl(mConsumer.getControl());
+ newControl2.setInsetsHint(Insets.of(0, 100, 0, 0));
+ mConsumer.setControl(newControl2, new int[1], new int[1], cancelTypes, new int[1]);
+
+ assertEquals("The animation must be cancelled", statusBars(), cancelTypes[0]);
});
}
diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java
index dfded7321b2c..0c4ea79dd5be 100644
--- a/graphics/java/android/graphics/Bitmap.java
+++ b/graphics/java/android/graphics/Bitmap.java
@@ -102,6 +102,10 @@ public final class Bitmap implements Parcelable {
private static volatile int sDefaultDensity = -1;
+ /**
+ * This id is not authoritative and can be duplicated if an ashmem bitmap is decoded from a
+ * parcel.
+ */
private long mId;
/**
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index f01e8d665d12..ab2f3ef94eb6 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -1,7 +1,7 @@
-xutan@google.com
+jorgegil@google.com
pbdr@google.com
pragyabajoria@google.com
# Give submodule owners in shell resource approval
-per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com
+per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com
per-file res*/*/tv_*.xml = bronger@google.com
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 13d0169c47c5..a08f88a5b937 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -177,3 +177,10 @@ flag {
description: "Factor task-view state tracking out of taskviewtransitions"
bug: "384976265"
}
+
+flag {
+ name: "enable_bubble_bar_on_phones"
+ namespace: "multitasking"
+ description: "Try out bubble bar on phones"
+ bug: "394869612"
+}
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index b1fedce5597e..50c08732543a 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -46,17 +46,10 @@
android:contentDescription="@string/app_icon_text"
android:importantForAccessibility="no"/>
- <TextView
+ <com.android.wm.shell.windowdecor.MarqueedTextView
android:id="@+id/application_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
tools:text="Gmail"
- android:textColor="@androidprv:color/materialColorOnSurface"
- android:textSize="14sp"
- android:textFontWeight="500"
- android:lineHeight="20dp"
- android:textStyle="normal"
- android:layout_weight="1"/>
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
<com.android.wm.shell.windowdecor.HandleMenuImageButton
android:id="@+id/collapse_menu_button"
@@ -133,37 +126,77 @@
android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
android:background="@drawable/desktop_mode_decor_handle_menu_background">
- <Button
+ <LinearLayout
android:id="@+id/screenshot_button"
android:contentDescription="@string/screenshot_text"
- android:text="@string/screenshot_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton"/>
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_screenshot"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/screenshot_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
- <Button
+ </LinearLayout>
+
+ <LinearLayout
android:id="@+id/new_window_button"
android:contentDescription="@string/new_window_text"
- android:text="@string/new_window_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton" />
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_new_window"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
- <Button
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/new_window_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
+
+ </LinearLayout>
+
+ <LinearLayout
android:id="@+id/manage_windows_button"
android:contentDescription="@string/manage_windows_text"
- android:text="@string/manage_windows_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton" />
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_manage_windows"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/manage_windows_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
- <Button
+ </LinearLayout>
+
+ <LinearLayout
android:id="@+id/change_aspect_ratio_button"
android:contentDescription="@string/change_aspect_ratio_text"
- android:text="@string/change_aspect_ratio_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton" />
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/change_aspect_ratio_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
+
+ </LinearLayout>
</LinearLayout>
<LinearLayout
@@ -176,22 +209,37 @@
android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
android:background="@drawable/desktop_mode_decor_handle_menu_background">
- <Button
+ <LinearLayout
android:id="@+id/open_in_app_or_browser_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
android:layout_weight="1"
+ android:layout_marginEnd="8dp"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
android:contentDescription="@string/open_in_browser_text"
- android:text="@string/open_in_browser_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton"/>
+ android:background="?android:selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_open_in_browser"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/open_in_browser_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
+
+ </LinearLayout>
<ImageButton
android:id="@+id/open_by_default_button"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
+ android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
- android:layout_marginStart="10dp"
android:contentDescription="@string/open_by_default_settings_text"
android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings"
android:tint="@androidprv:color/materialColorOnSurface"/>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 4ebb7dc6ff37..035004bfd322 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -40,19 +40,34 @@
<item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
</style>
- <style name="DesktopModeHandleMenuActionButton">
+ <style name="DesktopModeHandleMenuActionButtonLayout">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">52dp</item>
+ <item name="android:layout_weight">1</item>
<item name="android:gravity">start|center_vertical</item>
- <item name="android:paddingStart">16dp</item>
- <item name="android:paddingEnd">0dp</item>
- <item name="android:textSize">14sp</item>
- <item name="android:textFontWeight">500</item>
- <item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
- <item name="android:drawablePadding">16dp</item>
+ <item name="android:paddingHorizontal">16dp</item>
<item name="android:background">?android:selectableItemBackground</item>
</style>
+ <style name="DesktopModeHandleMenuActionButtonImage">
+ <item name="android:layout_width">20dp</item>
+ <item name="android:layout_height">20dp</item>
+ <item name="android:layout_marginEnd">16dp</item>
+ </style>
+
+ <style name="DesktopModeHandleMenuActionButtonTextView">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textFontWeight">500</item>
+ <item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
+ <item name="android:ellipsize">marquee</item>
+ <item name="android:scrollHorizontally">true</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
<style name="DesktopModeHandleMenuWindowingButton">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index 0b1f76f5ce0e..d280083ae7f5 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -17,4 +17,23 @@
<resources>
<dimen name="floating_dismiss_icon_size">32dp</dimen>
<dimen name="floating_dismiss_background_size">96dp</dimen>
+
+ <!-- Bubble drag zone dimensions -->
+ <dimen name="drag_zone_dismiss_fold">140dp</dimen>
+ <dimen name="drag_zone_dismiss_tablet">200dp</dimen>
+ <dimen name="drag_zone_bubble_fold">140dp</dimen>
+ <dimen name="drag_zone_bubble_tablet">200dp</dimen>
+ <dimen name="drag_zone_full_screen_width">512dp</dimen>
+ <dimen name="drag_zone_full_screen_height">44dp</dimen>
+ <dimen name="drag_zone_desktop_window_width">880dp</dimen>
+ <dimen name="drag_zone_desktop_window_height">300dp</dimen>
+ <dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen>
+ <dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen>
+ <dimen name="drag_zone_split_from_bubble_height">100dp</dimen>
+ <dimen name="drag_zone_split_from_bubble_width">60dp</dimen>
+ <dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen>
+ <dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen>
+ <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen>
+ <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen>
+ <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen>
</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index aa523f57c469..909e9d2c4428 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -16,11 +16,15 @@
package com.android.wm.shell.shared.bubbles
+import android.content.Context
import android.graphics.Rect
+import androidx.annotation.DimenRes
+import com.android.wm.shell.shared.R
import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
/** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */
class DragZoneFactory(
+ private val context: Context,
private val deviceConfig: DeviceConfig,
private val splitScreenModeChecker: SplitScreenModeChecker,
private val desktopWindowModeChecker: DesktopWindowModeChecker,
@@ -29,23 +33,65 @@ class DragZoneFactory(
private val windowBounds: Rect
get() = deviceConfig.windowBounds
- // TODO b/393172431: move these to xml
- private val dismissDragZoneSize = if (deviceConfig.isSmallTablet) 140 else 200
- private val bubbleDragZoneTabletSize = 200
- private val bubbleDragZoneFoldableSize = 140
- private val fullScreenDragZoneWidth = 512
- private val fullScreenDragZoneHeight = 44
- private val desktopWindowDragZoneWidth = 880
- private val desktopWindowDragZoneHeight = 300
- private val desktopWindowFromExpandedViewDragZoneWidth = 200
- private val desktopWindowFromExpandedViewDragZoneHeight = 350
- private val splitFromBubbleDragZoneHeight = 100
- private val splitFromBubbleDragZoneWidth = 60
- private val hSplitFromExpandedViewDragZoneWidth = 60
- private val vSplitFromExpandedViewDragZoneWidth = 200
- private val vSplitFromExpandedViewDragZoneHeightTablet = 285
- private val vSplitFromExpandedViewDragZoneHeightFoldTall = 150
- private val vSplitFromExpandedViewDragZoneHeightFoldShort = 100
+ private var dismissDragZoneSize = 0
+ private var bubbleDragZoneTabletSize = 0
+ private var bubbleDragZoneFoldableSize = 0
+ private var fullScreenDragZoneWidth = 0
+ private var fullScreenDragZoneHeight = 0
+ private var desktopWindowDragZoneWidth = 0
+ private var desktopWindowDragZoneHeight = 0
+ private var desktopWindowFromExpandedViewDragZoneWidth = 0
+ private var desktopWindowFromExpandedViewDragZoneHeight = 0
+ private var splitFromBubbleDragZoneHeight = 0
+ private var splitFromBubbleDragZoneWidth = 0
+ private var hSplitFromExpandedViewDragZoneWidth = 0
+ private var vSplitFromExpandedViewDragZoneWidth = 0
+ private var vSplitFromExpandedViewDragZoneHeightTablet = 0
+ private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0
+ private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0
+
+ init {
+ onConfigurationUpdated()
+ }
+
+ /** Updates all dimensions after a configuration change. */
+ fun onConfigurationUpdated() {
+ dismissDragZoneSize =
+ if (deviceConfig.isSmallTablet) {
+ context.resolveDimension(R.dimen.drag_zone_dismiss_fold)
+ } else {
+ context.resolveDimension(R.dimen.drag_zone_dismiss_tablet)
+ }
+ bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet)
+ bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold)
+ fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width)
+ fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height)
+ desktopWindowDragZoneWidth =
+ context.resolveDimension(R.dimen.drag_zone_desktop_window_width)
+ desktopWindowDragZoneHeight =
+ context.resolveDimension(R.dimen.drag_zone_desktop_window_height)
+ desktopWindowFromExpandedViewDragZoneWidth =
+ context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width)
+ desktopWindowFromExpandedViewDragZoneHeight =
+ context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height)
+ splitFromBubbleDragZoneHeight =
+ context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height)
+ splitFromBubbleDragZoneWidth =
+ context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width)
+ hSplitFromExpandedViewDragZoneWidth =
+ context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width)
+ vSplitFromExpandedViewDragZoneWidth =
+ context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width)
+ vSplitFromExpandedViewDragZoneHeightTablet =
+ context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet)
+ vSplitFromExpandedViewDragZoneHeightFoldTall =
+ context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall)
+ vSplitFromExpandedViewDragZoneHeightFoldShort =
+ context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short)
+ }
+
+ private fun Context.resolveDimension(@DimenRes dimension: Int) =
+ resources.getDimensionPixelSize(dimension)
/**
* Creates the list of drag zones for the dragged object.
@@ -58,11 +104,11 @@ class DragZoneFactory(
when (draggedObject) {
is DraggedObject.BubbleBar -> {
dragZones.add(createDismissDragZone())
- dragZones.addAll(createBubbleDragZones())
+ dragZones.addAll(createBubbleHalfScreenDragZones())
}
is DraggedObject.Bubble -> {
dragZones.add(createDismissDragZone())
- dragZones.addAll(createBubbleDragZones())
+ dragZones.addAll(createBubbleCornerDragZones())
dragZones.add(createFullScreenDragZone())
if (shouldShowDesktopWindowDragZones()) {
dragZones.add(createDesktopWindowDragZoneForBubble())
@@ -80,7 +126,7 @@ class DragZoneFactory(
} else {
dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet())
}
- createBubbleDragZonesForExpandedView()
+ dragZones.addAll(createBubbleHalfScreenDragZones())
}
}
return dragZones
@@ -98,7 +144,7 @@ class DragZoneFactory(
)
}
- private fun createBubbleDragZones(): List<DragZone> {
+ private fun createBubbleCornerDragZones(): List<DragZone> {
val dragZoneSize =
if (deviceConfig.isSmallTablet) {
bubbleDragZoneFoldableSize
@@ -124,7 +170,7 @@ class DragZoneFactory(
)
}
- private fun createBubbleDragZonesForExpandedView(): List<DragZone> {
+ private fun createBubbleHalfScreenDragZones(): List<DragZone> {
return listOf(
DragZone.Bubble.Left(
bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom),
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
new file mode 100644
index 000000000000..29ce8d90e66f
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.wm.shell.shared.bubbles
+
+/**
+ * Manages animating drop targets in response to dragging bubble icons or bubble expanded views
+ * across different drag zones.
+ */
+class DropTargetManager(
+ private val isLayoutRtl: Boolean,
+ private val dragZoneChangedListener: DragZoneChangedListener
+) {
+
+ private var state: DragState? = null
+
+ /** Must be called when a drag gesture is starting. */
+ fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) {
+ val state = DragState(dragZones, draggedObject)
+ dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone)
+ this.state = state
+ }
+
+ /** Called when the user drags to a new location. */
+ fun onDragUpdated(x: Int, y: Int) {
+ val state = state ?: return
+ val oldDragZone = state.currentDragZone
+ val newDragZone = state.getMatchingDragZone(x = x, y = y)
+ state.currentDragZone = newDragZone
+ if (oldDragZone != newDragZone) {
+ dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone)
+ }
+ }
+
+ /** Called when the drag ended. */
+ fun onDragEnded() {
+ state = null
+ }
+
+ /** Stores the current drag state. */
+ private inner class DragState(
+ private val dragZones: List<DragZone>,
+ draggedObject: DraggedObject
+ ) {
+ val initialDragZone =
+ if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) {
+ dragZones.filterIsInstance<DragZone.Bubble.Left>().first()
+ } else {
+ dragZones.filterIsInstance<DragZone.Bubble.Right>().first()
+ }
+ var currentDragZone: DragZone = initialDragZone
+
+ fun getMatchingDragZone(x: Int, y: Int): DragZone {
+ return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone
+ }
+ }
+
+ /** An interface to be notified when drag zones change. */
+ interface DragZoneChangedListener {
+ /** An initial drag zone was set. Called when a drag starts. */
+ fun onInitialDragZoneSet(dragZone: DragZone)
+ /** Called when the object was dragged to a different drag zone. */
+ fun onDragZoneChanged(from: DragZone, to: DragZone)
+ }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 2586bd6d86cb..643c1506e4c2 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -220,6 +220,13 @@ public class DesktopModeStatus {
}
/**
+ * Return {@code true} if the current device can host desktop sessions on its internal display.
+ */
+ public static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
+ }
+
+ /**
* Return {@code true} if desktop mode dev option should be shown on current device
*/
public static boolean canShowDesktopModeDevOption(@NonNull Context context) {
@@ -231,21 +238,24 @@ public class DesktopModeStatus {
* Return {@code true} if desktop mode dev option should be shown on current device
*/
public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
- return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context);
+ return Flags.showDesktopExperienceDevOption()
+ && isInternalDisplayEligibleToHostDesktops(context);
}
/** Returns if desktop mode dev option should be enabled if there is no user override. */
public static boolean shouldDevOptionBeEnabledByDefault(Context context) {
- return isDeviceEligibleForDesktopMode(context) && Flags.enableDesktopWindowingMode();
+ return isInternalDisplayEligibleToHostDesktops(context)
+ && Flags.enableDesktopWindowingMode();
}
/**
* Return {@code true} if desktop mode is enabled and can be entered on the current device.
*/
public static boolean canEnterDesktopMode(@NonNull Context context) {
- return (isDeviceEligibleForDesktopMode(context)
- && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue())
- || isDesktopModeEnabledByDevOption(context);
+ return (isInternalDisplayEligibleToHostDesktops(context)
+ && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()
+ && (isDesktopModeSupported(context) || !enforceDeviceRestrictions())
+ || isDesktopModeEnabledByDevOption(context));
}
/**
@@ -313,10 +323,11 @@ public class DesktopModeStatus {
}
/**
- * Return {@code true} if desktop mode is unrestricted and is supported in the device.
+ * Return {@code true} if desktop sessions is unrestricted and can be host for the device's
+ * internal display.
*/
- public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
- return !enforceDeviceRestrictions() || isDesktopModeSupported(context) || (
+ public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) {
+ return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || (
Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported(
context));
}
@@ -325,7 +336,7 @@ public class DesktopModeStatus {
* Return {@code true} if the developer option for desktop mode is unrestricted and is supported
* in the device.
*
- * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then
+ * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then
* {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true.
*/
private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) {
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
index 4127adc1f901..12938db07ece 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
@@ -24,4 +24,9 @@ public class DragAndDropConstants {
* ignore drag events.
*/
public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION";
+
+ /**
+ * An Intent extra that Launcher can use to specify the {@link android.content.pm.ShortcutInfo}
+ */
+ public static final String EXTRA_SHORTCUT_INFO = "EXTRA_SHORTCUT_INFO";
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index ddcdf9f8c617..d9489287ff42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -54,6 +54,7 @@ import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.Flags;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.common.ComponentUtils;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.bubbles.BubbleInfo;
@@ -282,6 +283,29 @@ public class Bubble implements BubbleViewProvider {
mPackageName = intent.getPackage();
}
+ private Bubble(
+ PendingIntent intent,
+ UserHandle user,
+ String key,
+ @ShellMainThread Executor mainExecutor,
+ @ShellBackgroundThread Executor bgExecutor) {
+ mGroupKey = null;
+ mLocusId = null;
+ mFlags = 0;
+ mUser = user;
+ mIcon = null;
+ mType = BubbleType.TYPE_APP;
+ mKey = key;
+ mShowBubbleUpdateDot = false;
+ mMainExecutor = mainExecutor;
+ mBgExecutor = bgExecutor;
+ mTaskId = INVALID_TASK_ID;
+ mPendingIntent = intent;
+ mIntent = null;
+ mDesiredHeight = Integer.MAX_VALUE;
+ mPackageName = ComponentUtils.getPackageName(intent);
+ }
+
private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor,
@ShellBackgroundThread Executor bgExecutor) {
mGroupKey = null;
@@ -336,6 +360,15 @@ public class Bubble implements BubbleViewProvider {
}
/** Creates an app bubble. */
+ public static Bubble createAppBubble(PendingIntent intent, UserHandle user,
+ @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
+ return new Bubble(intent,
+ user,
+ /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
+ mainExecutor, bgExecutor);
+ }
+
+ /** Creates an app bubble. */
public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon,
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
return new Bubble(intent,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index b93b7b86e661..c7a0401c2b88 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -46,6 +46,7 @@ import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.app.TaskInfo;
import android.content.BroadcastReceiver;
+import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -118,6 +119,7 @@ import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
import com.android.wm.shell.shared.bubbles.DeviceConfig;
+import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -791,15 +793,21 @@ public class BubbleController implements ConfigurationChangeListener,
public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation,
@BubbleBarLocation.UpdateSource int source) {
if (isShowingAsBubbleBar()) {
+ updateExpandedViewForBubbleBarLocation(bubbleBarLocation, source);
+ BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
+ bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
+ mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
+ }
+ }
+
+ private void updateExpandedViewForBubbleBarLocation(BubbleBarLocation bubbleBarLocation,
+ @BubbleBarLocation.UpdateSource int source) {
+ if (isShowingAsBubbleBar()) {
BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation();
mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
if (mLayerView != null && !mLayerView.isExpandedViewDragged()) {
mLayerView.updateExpandedView();
}
- BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
- bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
- mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
-
logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source);
}
}
@@ -872,11 +880,20 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent appIntent,
- UserHandle userHandle) {
- if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
- hideBubbleBarExpandedViewDropTarget();
- expandStackAndSelectBubble(appIntent, userHandle, location);
+ public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location,
+ Intent itemIntent) {
+ hideBubbleBarExpandedViewDropTarget();
+ ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent
+ .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO);
+ if (shortcutInfo != null) {
+ expandStackAndSelectBubble(shortcutInfo, location);
+ return;
+ }
+ UserHandle user = (UserHandle) itemIntent.getExtra(Intent.EXTRA_USER);
+ PendingIntent pendingIntent = (PendingIntent) itemIntent
+ .getExtra(ClipDescription.EXTRA_PENDING_INTENT);
+ if (pendingIntent != null && user != null) {
+ expandStackAndSelectBubble(pendingIntent, user, location);
}
}
@@ -1506,16 +1523,24 @@ public class BubbleController implements ConfigurationChangeListener,
* Expands and selects a bubble created or found via the provided shortcut info.
*
* @param info the shortcut info for the bubble.
+ * @param bubbleBarLocation optional location in case bubble bar should be repositioned.
*/
- public void expandStackAndSelectBubble(ShortcutInfo info) {
+ public void expandStackAndSelectBubble(ShortcutInfo info,
+ @Nullable BubbleBarLocation bubbleBarLocation) {
if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
+ BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null;
+ if (updateLocation != null) {
+ updateExpandedViewForBubbleBarLocation(updateLocation,
+ BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
+ }
Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow
ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info);
if (b.isInflated()) {
- mBubbleData.setSelectedBubbleAndExpandStack(b);
+ mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation);
} else {
b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
- inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false,
+ updateLocation);
}
}
@@ -1524,14 +1549,8 @@ public class BubbleController implements ConfigurationChangeListener,
*
* @param intent the intent for the bubble.
*/
- public void expandStackAndSelectBubble(Intent intent, UserHandle user,
- @Nullable BubbleBarLocation bubbleBarLocation) {
+ public void expandStackAndSelectBubble(Intent intent, UserHandle user) {
if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
- if (bubbleBarLocation != null) {
- //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack &
- // fix bubble bar flicking
- setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
- }
Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow
ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
if (b.isInflated()) {
@@ -1543,6 +1562,31 @@ public class BubbleController implements ConfigurationChangeListener,
}
/**
+ * Expands and selects a bubble created or found for this app.
+ *
+ * @param pendingIntent the intent for the bubble.
+ * @param bubbleBarLocation optional location in case bubble bar should be repositioned.
+ */
+ public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user,
+ @Nullable BubbleBarLocation bubbleBarLocation) {
+ if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
+ BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null;
+ if (updateLocation != null) {
+ updateExpandedViewForBubbleBarLocation(updateLocation,
+ BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
+ }
+ Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user);
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s",
+ pendingIntent);
+ if (b.isInflated()) {
+ mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation);
+ } else {
+ b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, updateLocation);
+ }
+ }
+
+ /**
* Expands and selects a bubble created from a running task in a different mode.
*
* @param taskInfo the task.
@@ -1904,11 +1948,22 @@ public class BubbleController implements ConfigurationChangeListener,
@VisibleForTesting
public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+ inflateAndAdd(bubble, suppressFlyout, showInShade, /* bubbleBarLocation= */ null);
+ }
+
+ /**
+ * Inflates and adds a bubble. Updates Bubble Bar location if bubbles
+ * are shown in the Bubble Bar and the location is not null.
+ */
+ @VisibleForTesting
+ public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade,
+ @Nullable BubbleBarLocation bubbleBarLocation) {
// Lazy init stack view when a bubble is created
ensureBubbleViewsAndWindowCreated();
bubble.setInflateSynchronously(mInflateSynchronously);
bubble.inflate(
- b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
+ b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade,
+ bubbleBarLocation),
mContext,
mExpandedViewManager,
mBubbleTaskViewFactory,
@@ -2242,7 +2297,8 @@ public class BubbleController implements ConfigurationChangeListener,
ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
+ " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
+ " expanded=%b selectionChanged=%b selected=%s"
- + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b",
+ + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b"
+ + " bubbleBarLocation=%s",
update.addedBubble != null ? update.addedBubble.getKey() : "null",
!update.removedBubbles.isEmpty(),
update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
@@ -2251,7 +2307,9 @@ public class BubbleController implements ConfigurationChangeListener,
update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
- update.shouldShowEducation, update.showOverflowChanged);
+ update.shouldShowEducation, update.showOverflowChanged,
+ update.mBubbleBarLocation != null ? update.mBubbleBarLocation.toString()
+ : "null");
ensureBubbleViewsAndWindowCreated();
@@ -2756,13 +2814,13 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void showShortcutBubble(ShortcutInfo info) {
- mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info));
+ mMainExecutor.execute(() -> mController
+ .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null));
}
@Override
public void showAppBubble(Intent intent, UserHandle user) {
- mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent,
- user, /* bubbleBarLocation = */ null));
+ mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, user));
}
@Override
@@ -2983,9 +3041,10 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void expandStackAndSelectBubble(ShortcutInfo info) {
- mMainExecutor.execute(() -> {
- BubbleController.this.expandStackAndSelectBubble(info);
- });
+ mMainExecutor.execute(() ->
+ BubbleController.this
+ .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null)
+ );
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 96d0f6d5654e..abcdb7e70cec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -43,6 +43,7 @@ import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubbles.DismissReason;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
import com.android.wm.shell.shared.bubbles.RemovedBubble;
@@ -91,6 +92,8 @@ public class BubbleData {
@Nullable Bubble suppressedBubble;
@Nullable Bubble unsuppressedBubble;
@Nullable String suppressedSummaryGroup;
+ @Nullable
+ BubbleBarLocation mBubbleBarLocation;
// Pair with Bubble and @DismissReason Integer
final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>();
@@ -116,6 +119,7 @@ public class BubbleData {
|| unsuppressedBubble != null
|| suppressedSummaryChanged
|| suppressedSummaryGroup != null
+ || mBubbleBarLocation != null
|| showOverflowChanged;
}
@@ -169,6 +173,7 @@ public class BubbleData {
}
bubbleBarUpdate.showOverflowChanged = showOverflowChanged;
bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty();
+ bubbleBarUpdate.bubbleBarLocation = mBubbleBarLocation;
return bubbleBarUpdate;
}
@@ -396,8 +401,23 @@ public class BubbleData {
* {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates.
*/
public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) {
+ setSelectedBubbleAndExpandStack(bubble, /* bubbleBarLocation = */ null);
+ }
+
+ /**
+ * Sets the selected bubble and expands it. Also updates bubble bar location if the
+ * bubbleBarLocation is not {@code null}
+ *
+ * <p>This dispatches a single state update for 3 changes and should be used instead of
+ * calling {@link BubbleController#setBubbleBarLocation(BubbleBarLocation, int)} followed by
+ * {@link #setSelectedBubbleAndExpandStack(BubbleViewProvider)} immediately after, which will
+ * generate 2 separate updates.
+ */
+ public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble,
+ @Nullable BubbleBarLocation bubbleBarLocation) {
setSelectedBubbleInternal(bubble);
setExpandedInternal(true);
+ mStateChange.mBubbleBarLocation = bubbleBarLocation;
dispatchPendingChanges();
}
@@ -471,6 +491,16 @@ public class BubbleData {
return bubbleToReturn;
}
+ Bubble getOrCreateBubble(PendingIntent pendingIntent, UserHandle user) {
+ String bubbleKey = Bubble.getAppBubbleKeyForApp(pendingIntent.getCreatorPackage(), user);
+ Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
+ if (bubbleToReturn == null) {
+ bubbleToReturn = Bubble.createAppBubble(pendingIntent, user, mMainExecutor,
+ mBgExecutor);
+ }
+ return bubbleToReturn;
+ }
+
Bubble getOrCreateBubble(TaskInfo taskInfo) {
UserHandle user = UserHandle.of(mCurrentUserId);
String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo);
@@ -503,13 +533,25 @@ public class BubbleData {
}
/**
+ * Calls {@link #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)} passing
+ * {@code null} for bubbleBarLocation.
+ *
+ * @see #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)
+ */
+ void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+ notificationEntryUpdated(bubble, suppressFlyout, showInShade, /* bubbleBarLocation = */
+ null);
+ }
+
+ /**
* When this method is called it is expected that all info in the bubble has completed loading.
* @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager,
* BubbleTaskViewFactory, BubblePositioner, BubbleLogger, BubbleStackView,
* com.android.wm.shell.bubbles.bar.BubbleBarLayerView,
* com.android.launcher3.icons.BubbleIconFactory, boolean)
*/
- void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
+ void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade,
+ @Nullable BubbleBarLocation bubbleBarLocation) {
mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here
Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey());
suppressFlyout |= !bubble.isTextChanged();
@@ -557,6 +599,7 @@ public class BubbleData {
doSuppress(bubble);
}
}
+ mStateChange.mBubbleBarLocation = bubbleBarLocation;
dispatchPendingChanges();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 4a0eee861d21..e47ac61a53dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -117,15 +117,24 @@ public class BubbleTaskViewHelper {
Context context =
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
- PendingIntent pi = PendingIntent.getActivity(
- context,
- /* requestCode= */ 0,
- mBubble.getIntent()
- .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
- PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
- /* options= */ null);
- mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
- launchBounds);
+ Intent fillInIntent = null;
+ //first try get pending intent from the bubble
+ PendingIntent pi = mBubble.getPendingIntent();
+ if (pi == null) {
+ // if null - create new one
+ pi = PendingIntent.getActivity(
+ context,
+ /* requestCode= */ 0,
+ mBubble.getIntent()
+ .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+ PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT,
+ /* options= */ null);
+ } else {
+ fillInIntent = new Intent(pi.getIntent());
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
} else if (isShortcutBubble) {
options.setLaunchedFromBubble(true);
options.setApplyActivityFlagsForBubbles(true);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
index afe5c87604d9..3ff80b5ab8ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.bubbles.bar
import android.content.Intent
import android.graphics.Rect
-import android.os.UserHandle
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
/** Controller that takes care of the bubble bar drag events. */
@@ -31,11 +30,7 @@ interface BubbleBarDragListener {
fun onItemDraggedOutsideBubbleBarDropZone()
/** Called when the drop event happens over the bubble bar drop zone. */
- fun onItemDroppedOverBubbleBarDragZone(
- location: BubbleBarLocation,
- intent: Intent,
- userHandle: UserHandle
- )
+ fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation, itemIntent: Intent)
/**
* Returns mapping of the bubble bar locations to the corresponding
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index aa42de67152a..e3b0872df593 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -524,8 +524,8 @@ public class BubbleBarLayerView extends FrameLayout
* Skips logging if it is {@link BubbleOverflow}.
*/
private void logBubbleEvent(BubbleLogger.Event event) {
- if (mExpandedBubble != null && mExpandedBubble instanceof Bubble bubble) {
- mBubbleLogger.log(bubble, event);
+ if (mExpandedBubble != null && mExpandedBubble instanceof Bubble) {
+ mBubbleLogger.log((Bubble) mExpandedBubble, event);
}
}
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 b2b99d648bf4..b6012378e4d4 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
@@ -914,12 +914,15 @@ public abstract class WMShellModule {
Context context,
Transitions transitions,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ @DynamicOverride DesktopUserRepositories desktopUserRepositories,
InteractionJankMonitor interactionJankMonitor) {
return ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX.isTrue()
? new SpringDragToDesktopTransitionHandler(
- context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor)
+ context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories,
+ interactionJankMonitor)
: new DefaultDragToDesktopTransitionHandler(
- context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor);
+ context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories,
+ interactionJankMonitor);
}
@WMSingleton
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 3c7780711a14..531304d6922a 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
@@ -343,10 +343,22 @@ class DesktopTasksController(
DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
.isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue()
) {
+ logV(
+ "isDesktopModeShowing: hasVisibleTasks=%s hasTopTransparentFullscreenTask=%s hasMinimizedPip=%s",
+ hasVisibleTasks,
+ hasTopTransparentFullscreenTask,
+ hasMinimizedPip,
+ )
return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip
} else if (Flags.enableDesktopWindowingPip()) {
+ logV(
+ "isDesktopModeShowing: hasVisibleTasks=%s hasMinimizedPip=%s",
+ hasVisibleTasks,
+ hasMinimizedPip,
+ )
return hasVisibleTasks || hasMinimizedPip
}
+ logV("isDesktopModeShowing: hasVisibleTasks=%s", hasVisibleTasks)
return hasVisibleTasks
}
@@ -1647,11 +1659,16 @@ class DesktopTasksController(
private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
logV("addWallpaperActivity")
if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) {
+
+ // If the wallpaper activity for this display already exists, let's reorder it to top.
+ val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId)
+ if (wallpaperActivityToken != null) {
+ wct.reorder(wallpaperActivityToken, /* onTop= */ true)
+ return
+ }
+
val intent = Intent(context, DesktopWallpaperActivity::class.java)
- if (
- desktopWallpaperActivityTokenProvider.getToken(displayId) == null &&
- Flags.enablePerDisplayDesktopWallpaperActivity()
- ) {
+ if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
@@ -3074,6 +3091,7 @@ class DesktopTasksController(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
pendingIntentLaunchFlags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
}
if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
dragAndDropFullscreenCookie = Binder()
@@ -3082,7 +3100,12 @@ class DesktopTasksController(
val wct = WindowContainerTransaction()
wct.sendPendingIntent(launchIntent, null, opts.toBundle())
if (windowingMode == WINDOWING_MODE_FREEFORM) {
- desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
+ if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) {
+ // TODO b/376389593: Use a custom tab tearing transition/animation
+ startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+ } else {
+ desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
+ }
} else {
transitions.startTransition(TRANSIT_OPEN, wct, null)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
index a5ba6612bb1a..c10752d36bf9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt
@@ -90,6 +90,11 @@ class DesktopUserRepositories(
return desktopRepoByUserId.getOrCreate(profileId)
}
+ fun getUserIdForProfile(profileId: Int): Int {
+ if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId
+ else return profileId
+ }
+
/** Dumps [DesktopRepository] for each user. */
fun dump(pw: PrintWriter, prefix: String) {
desktopRepoByUserId.forEach { key, value ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 2ac76f319d32..8194d3cab445 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -70,6 +70,7 @@ sealed class DragToDesktopTransitionHandler(
private val context: Context,
private val transitions: Transitions,
private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val desktopUserRepositories: DesktopUserRepositories,
protected val interactionJankMonitor: InteractionJankMonitor,
protected val transactionSupplier: Supplier<SurfaceControl.Transaction>,
) : TransitionHandler {
@@ -127,15 +128,18 @@ sealed class DragToDesktopTransitionHandler(
pendingIntentCreatorBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
}
- val taskUser = UserHandle.of(taskInfo.userId)
+ // If we are launching home for a profile of a user, just use the [userId] of that user
+ // instead of the [profileId] to create the context.
+ val userToLaunchWith =
+ UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId))
val pendingIntent =
PendingIntent.getActivityAsUser(
- context.createContextAsUser(taskUser, /* flags= */ 0),
+ context.createContextAsUser(userToLaunchWith, /* flags= */ 0),
/* requestCode= */ 0,
launchHomeIntent,
FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
options.toBundle(),
- taskUser,
+ userToLaunchWith,
)
val wct = WindowContainerTransaction()
// The app that is being dragged into desktop mode might cause new transitions, make this
@@ -881,6 +885,7 @@ constructor(
context: Context,
transitions: Transitions,
taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ desktopUserRepositories: DesktopUserRepositories,
interactionJankMonitor: InteractionJankMonitor,
transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier {
SurfaceControl.Transaction()
@@ -890,6 +895,7 @@ constructor(
context,
transitions,
taskDisplayAreaOrganizer,
+ desktopUserRepositories,
interactionJankMonitor,
transactionSupplier,
) {
@@ -917,6 +923,7 @@ constructor(
context: Context,
transitions: Transitions,
taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ desktopUserRepositories: DesktopUserRepositories,
interactionJankMonitor: InteractionJankMonitor,
transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier {
SurfaceControl.Transaction()
@@ -926,6 +933,7 @@ constructor(
context,
transitions,
taskDisplayAreaOrganizer,
+ desktopUserRepositories,
interactionJankMonitor,
transactionSupplier,
) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 2571e0e36cd9..b3c1a92f5e1d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -47,7 +47,6 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
import android.view.DragEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -70,6 +69,7 @@ import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;
import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -627,8 +627,7 @@ public class DragLayout extends LinearLayout
@Nullable
private BubbleBarLocation getBubbleBarLocation(int x, int y) {
Intent appData = mSession.appData;
- if (appData == null || appData.getExtra(Intent.EXTRA_INTENT) == null
- || appData.getExtra(Intent.EXTRA_USER) == null) {
+ if (appData == null) {
// there is no app data, so drop event over the bubble bar can not be handled
return null;
}
@@ -686,11 +685,10 @@ public class DragLayout extends LinearLayout
// Process the drop exclusive by DropTarget OR by the BubbleBar
if (mCurrentTarget != null) {
mPolicy.onDropped(mCurrentTarget, hideTaskToken);
- } else if (appData != null && mCurrentBubbleBarTarget != null) {
- Intent appIntent = (Intent) appData.getExtra(Intent.EXTRA_INTENT);
- UserHandle user = (UserHandle) appData.getExtra(Intent.EXTRA_USER);
+ } else if (appData != null && mCurrentBubbleBarTarget != null
+ && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget,
- appIntent, user);
+ appData);
}
// Start animating the drop UI out with the drag surface
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index d666126b91ba..c0a0f469add4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
@@ -200,7 +201,8 @@ public class KeyguardTransitionHandler
transition, info, startTransaction, finishTransaction, finishCallback);
}
- if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) {
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
+ || (info.getFlags() & TRANSIT_FLAG_AOD_APPEARING) != 0) {
return startAnimation(mAppearTransition, "appearing",
transition, info, startTransaction, finishTransaction, finishCallback);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 8ad2e1d3c7c9..7751741ae082 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -29,6 +29,7 @@ import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
@@ -46,6 +47,7 @@ import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.IApplicationThread;
import android.app.PendingIntent;
+import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Rect;
@@ -73,6 +75,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.Flags;
+import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipUtils;
@@ -317,7 +320,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
"RecentsTransitionHandler.mergeAnimation: no controller found");
return;
}
- controller.merge(info, startT, mergeTarget, finishCallback);
+ controller.merge(info, startT, finishT, mergeTarget, finishCallback);
}
@Override
@@ -912,7 +915,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
* before any unhandled transitions.
*/
@SuppressLint("NewApi")
- void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget,
+ void merge(TransitionInfo info, SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT, IBinder mergeTarget,
Transitions.TransitionFinishCallback finishCallback) {
if (mFinishCB == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -1072,8 +1076,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
Slog.e(TAG, "Returning to recents without closing any opening tasks.");
}
// Setup may hide it initially since it doesn't know that overview was still active.
- t.show(recentsOpening.getLeash());
- t.setAlpha(recentsOpening.getLeash(), 1.f);
+ startT.show(recentsOpening.getLeash());
+ startT.setAlpha(recentsOpening.getLeash(), 1.f);
mState = STATE_NORMAL;
}
boolean didMergeThings = false;
@@ -1142,31 +1146,31 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
mOpeningTasks.add(pausingTask);
// Setup hides opening tasks initially, so make it visible again (since we
// are already showing it).
- t.show(change.getLeash());
- t.setAlpha(change.getLeash(), 1.f);
+ startT.show(change.getLeash());
+ startT.setAlpha(change.getLeash(), 1.f);
} else if (isLeaf) {
// We are receiving new opening leaf tasks, so convert to onTasksAppeared.
final RemoteAnimationTarget target = TransitionUtil.newTarget(
- change, layer, info, t, mLeashMap);
+ change, layer, info, startT, mLeashMap);
appearedTargets[nextTargetIdx++] = target;
// reparent into the original `mInfo` since that's where we are animating.
final TransitionInfo.Root root = TransitionUtil.getRootFor(change, mInfo);
final boolean wasClosing = closingIdx >= 0;
- t.reparent(target.leash, root.getLeash());
- t.setPosition(target.leash,
+ startT.reparent(target.leash, root.getLeash());
+ startT.setPosition(target.leash,
change.getStartAbsBounds().left - root.getOffset().x,
change.getStartAbsBounds().top - root.getOffset().y);
- t.setLayer(target.leash, layer);
+ startT.setLayer(target.leash, layer);
if (wasClosing) {
// App was previously visible and is closing
- t.show(target.leash);
- t.setAlpha(target.leash, 1f);
+ startT.show(target.leash);
+ startT.setAlpha(target.leash, 1f);
// Also override the task alpha as it was set earlier when dispatching
// the transition and setting up the leash to hide the
- t.setAlpha(change.getLeash(), 1f);
+ startT.setAlpha(change.getLeash(), 1f);
} else {
// Hide the animation leash, let the listener show it
- t.hide(target.leash);
+ startT.hide(target.leash);
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" opening new leaf taskId=%d wasClosing=%b",
@@ -1175,10 +1179,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
} else {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" opening new taskId=%d", change.getTaskInfo().taskId);
- t.setLayer(change.getLeash(), layer);
+ startT.setLayer(change.getLeash(), layer);
// Setup hides opening tasks initially, so make it visible since recents
// is only animating the leafs.
- t.show(change.getLeash());
+ startT.show(change.getLeash());
mOpeningTasks.add(new TaskState(change, null));
}
}
@@ -1194,7 +1198,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
// Activity only transition, so consume the merge as it doesn't affect the rest of
// recents.
Slog.d(TAG, "Got an activity only transition during recents, so apply directly");
- mergeActivityOnly(info, t);
+ mergeActivityOnly(info, startT);
} else if (!didMergeThings) {
// Didn't recognize anything in incoming transition so don't merge it.
Slog.w(TAG, "Don't know how to merge this transition, foundRecentsClosing="
@@ -1206,7 +1210,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
return;
}
// At this point, we are accepting the merge.
- t.apply();
+ startT.apply();
+ // Since we're accepting the merge, update the finish transaction so that changes via
+ // that transaction will be applied on top of those of the merged transitions
+ mFinishTransaction = finishT;
// not using the incoming anim-only surfaces
info.releaseAnimSurfaces();
if (appearedTargets != null) {
@@ -1349,6 +1356,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */);
t.show(mPausingTasks.get(i).mTaskSurface);
}
+ setCornerRadiusForFreeformTasks(
+ mRecentTasksController.getContext(), t, mPausingTasks);
if (!mKeyguardLocked && mRecentsTask != null) {
wct.restoreTransientOrder(mRecentsTask);
}
@@ -1386,6 +1395,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
for (int i = 0; i < mOpeningTasks.size(); ++i) {
t.show(mOpeningTasks.get(i).mTaskSurface);
}
+ setCornerRadiusForFreeformTasks(
+ mRecentTasksController.getContext(), t, mOpeningTasks);
for (int i = 0; i < mPausingTasks.size(); ++i) {
cleanUpPausingOrClosingTask(mPausingTasks.get(i), wct, t, sendUserLeaveHint);
}
@@ -1446,6 +1457,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
wct.clear();
if (Flags.enableRecentsBookendTransition()) {
+ // Notify the mixers of the pending finish
+ for (int i = 0; i < mMixers.size(); ++i) {
+ mMixers.get(i).handleFinishRecents(returningToApp, wct, t);
+ }
+
// In this case, we've already started the PIP transition, so we can
// clean up immediately
mPendingRunnerFinishCb = runnerFinishCb;
@@ -1505,6 +1521,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
}
}
+ private static void setCornerRadiusForFreeformTasks(
+ Context context,
+ SurfaceControl.Transaction t,
+ ArrayList<TaskState> tasks) {
+ if (!ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) {
+ return;
+ }
+ int cornerRadius = getCornerRadius(context);
+ for (int i = 0; i < tasks.size(); ++i) {
+ TaskState task = tasks.get(i);
+ if (task.mTaskInfo != null && task.mTaskInfo.isFreeform()) {
+ t.setCornerRadius(task.mTaskSurface, cornerRadius);
+ }
+ }
+ }
+
+ private static int getCornerRadius(Context context) {
+ return context.getResources().getDimensionPixelSize(
+ R.dimen.desktop_windowing_freeform_rounded_corner_radius);
+ }
+
private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) {
if (tasks == null) {
return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index aff21cbe0ae6..15ac03ccaf30 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1675,8 +1675,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
void prepareExitSplitScreen(@StageType int stageToTop,
@NonNull WindowContainerTransaction wct, @ExitReason int exitReason) {
if (!isSplitActive()) return;
- ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s",
- stageTypeToString(stageToTop));
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s",
+ stageTypeToString(stageToTop), exitReasonToString(exitReason));
if (enableFlexibleSplit()) {
mStageOrderOperator.getActiveStages().stream()
.filter(stage -> stage.getId() != stageToTop)
@@ -3395,12 +3395,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
TransitionInfo.Change sideChild = null;
StageTaskListener firstAppStage = null;
StageTaskListener secondAppStage = null;
+ boolean foundPausingTask = false;
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo == null || !taskInfo.hasParentTask()) continue;
if (mPausingTasks.contains(taskInfo.taskId)) {
+ foundPausingTask = true;
continue;
}
StageTaskListener stage = getStageOfTask(taskInfo);
@@ -3443,9 +3445,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN);
logExit(EXIT_REASON_UNKNOWN);
});
- Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
- "launched 2 tasks in split, but didn't receive "
- + "2 tasks in transition. Possibly one of them failed to launch"));
+ Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in "
+ + "split, but didn't receive 2 tasks in transition. Possibly one of them "
+ + "failed to launch (foundPausingTask=" + foundPausingTask + ")"));
if (mRecentTasks.isPresent() && mainChild != null) {
mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId);
}
@@ -3800,6 +3802,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
/** Call this when the recents animation canceled during split-screen. */
public void onRecentsInSplitAnimationCanceled() {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled");
mPausingTasks.clear();
setSplitsVisible(false);
@@ -3809,31 +3812,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mTaskOrganizer.applyTransaction(wct);
}
- public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
- @NonNull WindowContainerTransaction finishWct,
- @NonNull SurfaceControl.Transaction finishT) {
- if (!Flags.enableRecentsBookendTransition()) {
- // The non-bookend recents transition case will be handled by
- // RecentsMixedTransition wrapping the finish callback and calling
- // onRecentsInSplitAnimationFinish()
- return;
- }
-
- onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
- }
-
- /** Call this when the recents animation during split-screen finishes. */
- public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct,
- @NonNull SurfaceControl.Transaction finishT) {
- if (Flags.enableRecentsBookendTransition()) {
- // The bookend recents transition case will be handled by
- // onRecentsInSplitAnimationFinishing above
- return;
- }
-
- // Check if the recent transition is finished by returning to the current
- // split, so we can restore the divider bar.
- boolean returnToApp = false;
+ /**
+ * Returns whether the given WCT is reordering any of the split tasks to top.
+ */
+ public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) {
for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
final WindowContainerTransaction.HierarchyOp op =
finishWct.getHierarchyOps().get(i);
@@ -3848,14 +3830,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
&& anyStageContainsContainer) {
- returnToApp = true;
+ return true;
}
}
- onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT);
+ return false;
}
- /** Call this when the recents animation during split-screen finishes. */
- public void onRecentsInSplitAnimationFinishInner(boolean returnToApp,
+ /** Called when the recents animation during split-screen finishes. */
+ public void onRecentsInSplitAnimationFinishing(boolean returnToApp,
@NonNull WindowContainerTransaction finishWct,
@NonNull SurfaceControl.Transaction finishT) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 0689205a1110..a5a5fd73d5c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -39,9 +39,12 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_RELAUNCH;
import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
@@ -55,6 +58,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static com.android.internal.jank.Cuj.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION;
import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -101,6 +105,7 @@ import android.window.WindowContainerTransaction;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.ProtoLog;
@@ -144,6 +149,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private Drawable mEnterpriseThumbnailDrawable;
+ static final InteractionJankMonitor sInteractionJankMonitor =
+ InteractionJankMonitor.getInstance();
+
private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -321,8 +329,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
final ArrayList<Animator> animations = new ArrayList<>();
mAnimations.put(transition, animations);
+ final boolean isTaskTransition = isTaskTransition(info);
+ if (isTaskTransition) {
+ sInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext,
+ mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
+
final Runnable onAnimFinish = () -> {
if (!animations.isEmpty()) return;
+ if (isTaskTransition) {
+ sInteractionJankMonitor.end(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
mAnimations.remove(transition);
finishCallback.onTransitionFinished(null /* wct */);
};
@@ -678,6 +695,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
/**
+ * A task transition is defined as a transition where there is exaclty one open/to_front task
+ * and one close/to_back task. Nothing else is allowed to be included in the transition
+ */
+ public static boolean isTaskTransition(@NonNull TransitionInfo info) {
+ if (info.getChanges().size() != 2) {
+ return false;
+ }
+ boolean hasOpeningTask = false;
+ boolean hasClosingTask = false;
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getTaskInfo() == null) {
+ // A non-task is in the transition
+ return false;
+ }
+ int mode = change.getMode();
+ hasOpeningTask |= mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
+ hasClosingTask |= mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
+ }
+ return hasOpeningTask && hasClosingTask;
+ }
+
+ /**
* Does `info` only contain translucent visibility changes (CHANGEs are ignored). We select
* different animations and z-orders for these
*/
@@ -986,4 +1027,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
|| animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS
|| animType == ANIM_FROM_STYLE;
}
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishTransaction) {
+ sInteractionJankMonitor.cancel(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index f40dc8ad93b5..1e926c57ca61 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -159,9 +159,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
// If pair-to-pair switching, the post-recents clean-up isn't needed.
wct = wct != null ? wct : new WindowContainerTransaction();
if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
- // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove
- // once that rolls out
- mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+ // We've dispatched to the mLeftoversHandler to handle the rest of the transition
+ // and called onRecentsInSplitAnimationStart(), but if the recents handler is not
+ // actually handling the transition, then onRecentsInSplitAnimationFinishing()
+ // won't actually get called by the recents handler. In such cases, we still need
+ // to clean up after the changes from the start call.
+ boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler;
+ if (!splitNotifiedByRecents) {
+ mSplitHandler.onRecentsInSplitAnimationFinishing(
+ mSplitHandler.wctIsReorderingSplitToTop(wct),
+ wct, finishTransaction);
+ }
} else {
// notify pair-to-pair recents animation finish
mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 1d9564948772..c92e67f1a0c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -473,7 +473,7 @@ class HandleMenu(
@VisibleForTesting
val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon)
@VisibleForTesting
- val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name)
+ val appNameView = appInfoPill.requireViewById<MarqueedTextView>(R.id.application_name)
// Windowing Pill.
private val windowingPill = rootView.requireViewById<View>(R.id.windowing_pill)
@@ -486,17 +486,17 @@ class HandleMenu(
// More Actions Pill.
private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill)
- private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button)
- private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button)
+ private val screenshotBtn = moreActionsPill.requireViewById<View>(R.id.screenshot_button)
+ private val newWindowBtn = moreActionsPill.requireViewById<View>(R.id.new_window_button)
private val manageWindowBtn = moreActionsPill
- .requireViewById<Button>(R.id.manage_windows_button)
+ .requireViewById<View>(R.id.manage_windows_button)
private val changeAspectRatioBtn = moreActionsPill
- .requireViewById<Button>(R.id.change_aspect_ratio_button)
+ .requireViewById<View>(R.id.change_aspect_ratio_button)
// Open in Browser/App Pill.
private val openInAppOrBrowserPill = rootView.requireViewById<View>(
R.id.open_in_app_or_browser_pill)
- private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>(
+ private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<View>(
R.id.open_in_app_or_browser_button)
private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>(
R.id.open_by_default_button)
@@ -658,6 +658,7 @@ class HandleMenu(
this.taskInfo = this@HandleMenuView.taskInfo
}
appNameView.setTextColor(style.textColor)
+ appNameView.startMarquee()
}
private fun bindWindowingPill(style: MenuStyle) {
@@ -693,11 +694,15 @@ class HandleMenu(
).forEach {
val button = it.first
val shouldShow = it.second
- button.apply {
- isGone = !shouldShow
+ val label = button.requireViewById<MarqueedTextView>(R.id.label)
+ val image = button.requireViewById<ImageView>(R.id.image)
+
+ button.isGone = !shouldShow
+ label.apply {
setTextColor(style.textColor)
- compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+ startMarquee()
}
+ image.imageTintList = ColorStateList.valueOf(style.textColor)
}
}
@@ -712,12 +717,17 @@ class HandleMenu(
} else {
getString(R.string.open_in_browser_text)
}
- openInAppOrBrowserBtn.apply {
+
+ val label = openInAppOrBrowserBtn.requireViewById<MarqueedTextView>(R.id.label)
+ val image = openInAppOrBrowserBtn.requireViewById<ImageView>(R.id.image)
+ openInAppOrBrowserBtn.contentDescription = btnText
+ label.apply {
text = btnText
- contentDescription = btnText
setTextColor(style.textColor)
- compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+ startMarquee()
}
+ image.imageTintList = ColorStateList.valueOf(style.textColor)
+
openByDefaultBtn.isGone = isBrowserApp
openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 470e5a1d88b4..75f90bb9c38e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -327,7 +327,7 @@ class HandleMenuAnimator(
}
// Open in Browser Button Opacity Animation
- val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button)
+ val button = openInAppOrBrowserPill.requireViewById<View>(R.id.open_in_app_or_browser_button)
animators +=
ObjectAnimator.ofFloat(button, ALPHA, 1f).apply {
startDelay = BODY_ALPHA_OPEN_DELAY
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt
new file mode 100644
index 000000000000..733b6221ac0e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.wm.shell.windowdecor
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextView
+
+/** A custom [TextView] that allows better control over marquee animation used to ellipsize text. */
+class MarqueedTextView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = android.R.attr.textViewStyle
+) : TextView(context, attrs, defStyleAttr) {
+
+ /**
+ * Starts marquee animation if the layout attributes for this object include
+ * `android:ellipsize=marquee`, `android:singleLine=true`, and
+ * `android:scrollHorizontally=true`.
+ */
+ override public fun startMarquee() {
+ super.startMarquee()
+ }
+
+ /**
+ * Must always return [true] since [TextView.startMarquee()] requires view to be selected or
+ * focused in order to start the marquee animation.
+ *
+ * We are not using [TextView.setSelected()] as this would dispatch undesired accessibility
+ * events.
+ */
+ override fun isSelected() : Boolean {
+ return true
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
index 3f828f547920..992402528f4f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
@@ -1,3 +1,2 @@
-jorgegil@google.com
mattsziklay@google.com
mdehaini@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index bc2be901d320..4762bc21d79c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -93,6 +93,9 @@ class AppHeaderViewHolder(
private val lightColors = dynamicLightColorScheme(context)
private val darkColors = dynamicDarkColorScheme(context)
+ private val headerButtonOpenMenuA11yText = context.resources
+ .getString(R.string.desktop_mode_app_header_chip_text)
+
/**
* The corner radius to apply to the app chip, maximize and close button's background drawable.
**/
@@ -228,6 +231,18 @@ class AppHeaderViewHolder(
}
}
+ val a11yActionOpenHeaderMenu = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
+ headerButtonOpenMenuA11yText)
+ openMenuButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(a11yActionOpenHeaderMenu)
+ }
+ }
+
with(context.resources) {
// Update a11y read out to say "double tap to maximize or restore window size"
ViewCompat.replaceAccessibilityAction(
@@ -260,6 +275,7 @@ class AppHeaderViewHolder(
/** Sets the app's name in the header. */
fun setAppName(name: CharSequence) {
appNameTextView.text = name
+ openMenuButton.contentDescription = name
}
/** Sets the app's icon in the header. */
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index 19829e7e5677..bac8e5062128 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -12,7 +12,6 @@ atsjenk@google.com
jorgegil@google.com
vaniadesmonda@google.com
pbdr@google.com
-tkachenkoi@google.com
mpodolian@google.com
jeremysim@google.com
peanutbutter@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index 02b2cec8dbdb..ae73dae99d6f 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -53,10 +53,12 @@
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="teardown-command"
value="settings delete secure show_ime_with_hard_keyboard"/>
<option name="teardown-command" value="settings delete system show_touches"/>
<option name="teardown-command" value="settings delete system pointer_location"/>
+ <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/>
<option name="teardown-command"
value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
</target_preparer>
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index ffcc3446d436..7a7d88b80ce3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -572,6 +572,22 @@ public class BubbleDataTest extends ShellTestCase {
assertThat(update.shouldShowEducation).isTrue();
}
+ /** Verifies that the update should contain the bubble bar location. */
+ @Test
+ public void test_shouldUpdateBubbleBarLocation() {
+ // Setup
+ mBubbleData.setListener(mListener);
+
+ // Test
+ mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */
+ true, BubbleBarLocation.LEFT);
+
+ // Verify
+ verifyUpdateReceived();
+ BubbleData.Update update = mUpdateCaptor.getValue();
+ assertThat(update.mBubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT);
+ }
+
/**
* Verifies that the update shouldn't show the user education, if the education is required but
* the bubble should auto-expand
@@ -1367,6 +1383,20 @@ public class BubbleDataTest extends ShellTestCase {
}
@Test
+ public void setSelectedBubbleAndExpandStackWithLocation() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 2000);
+ mBubbleData.setListener(mListener);
+
+ mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1, BubbleBarLocation.LEFT);
+
+ verifyUpdateReceived();
+ assertSelectionChangedTo(mBubbleA1);
+ assertExpandedChangedTo(true);
+ assertLocationChangedTo(BubbleBarLocation.LEFT);
+ }
+
+ @Test
public void testShowOverflowChanged_hasOverflowBubbles() {
assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
sendUpdatedEntryAtTime(mEntryA1, 1000);
@@ -1450,6 +1480,12 @@ public class BubbleDataTest extends ShellTestCase {
assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble);
}
+ private void assertLocationChangedTo(BubbleBarLocation location) {
+ BubbleData.Update update = mUpdateCaptor.getValue();
+ assertWithMessage("locationChanged").that(update.mBubbleBarLocation)
+ .isEqualTo(location);
+ }
+
private void assertExpandedChangedTo(boolean expected) {
BubbleData.Update update = mUpdateCaptor.getValue();
assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
index 33e8d78d6a15..7b7d96c4294c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.common
import android.testing.AndroidTestingRunner
import android.view.IWindowSession
import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,10 +31,10 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidTestingRunner::class)
@SmallTest
-class WindowSessionSupplierTest {
+class WindowSessionSupplierTest : ShellTestCase() {
@Test
- fun `InputChannelSupplier supplies an InputChannel`() {
+ fun `WindowSessionSupplierTest supplies an IWindowSession`() {
val supplier = WindowSessionSupplier()
SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
it is IWindowSession
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index d6b13610c9c1..70a30a3ca7a9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -113,7 +113,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
.strictness(Strictness.LENIENT)
.spyStatic(DesktopModeStatus::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+ doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
index 403d468a7034..d510570e8839 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -30,7 +30,6 @@ import android.view.KeyEvent
import android.window.DisplayAreaInfo
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer
-import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
@@ -48,7 +47,6 @@ import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
-import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel
@@ -107,12 +105,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
@Before
fun setUp() {
Dispatchers.setMain(StandardTestDispatcher())
- mockitoSession =
- mockitoSession()
- .strictness(Strictness.LENIENT)
- .spyStatic(DesktopModeStatus::class.java)
- .startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+ mockitoSession = mockitoSession().strictness(Strictness.LENIENT).startMocking()
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
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 edb9b2d2fede..718bf322f6a9 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
@@ -171,7 +171,6 @@ import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.isA
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
@@ -292,7 +291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
.spyStatic(DesktopModeStatus::class.java)
.spyStatic(Toast::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+ doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
@@ -363,9 +362,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
shellInit.init()
- val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
+ val captor = argumentCaptor<RecentsTransitionStateListener>()
verify(recentsTransitionHandler).addTransitionStateListener(captor.capture())
- recentsTransitionStateListener = captor.value
+ recentsTransitionStateListener = captor.firstValue
controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener
@@ -441,7 +440,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() {
val task1 = setUpFreeformTask()
- val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+ val argumentCaptor = argumentCaptor<Boolean>()
controller.toggleDesktopTaskSize(
task1,
ToggleTaskSizeInteraction(
@@ -461,7 +460,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
STABLE_BOUNDS.height(),
displayController,
)
- assertThat(argumentCaptor.value).isTrue()
+ assertThat(argumentCaptor.firstValue).isTrue()
}
@Test
@@ -476,7 +475,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
val task1 = setUpFreeformTask(bounds = stableBounds, active = true)
- val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+ val argumentCaptor = argumentCaptor<Boolean>()
controller.toggleDesktopTaskSize(
task1,
ToggleTaskSizeInteraction(
@@ -497,7 +496,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
eq(displayController),
anyOrNull(),
)
- assertThat(argumentCaptor.value).isFalse()
+ assertThat(argumentCaptor.firstValue).isFalse()
}
@Test
@@ -547,6 +546,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskHidden(task1)
@@ -581,7 +581,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val wct =
getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
// Wallpaper is moved to front.
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ wct.assertReorderAt(index = 0, wallpaperToken)
// Desk is activated.
verify(desksOrganizer).activateDesk(wct, deskId)
}
@@ -783,6 +783,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskVisible(task1)
@@ -825,7 +826,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
+ fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskHidden(task1)
@@ -842,6 +844,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
wct.assertReorderAt(index = 2, task2)
}
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskHidden(task1)
+ markTaskVisible(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: wallpaper intent, task1, task2
+ wct.assertReorderAt(index = 0, wallpaperToken)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
@Test
@DisableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
@@ -860,9 +880,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
- whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
- .thenReturn(Binder())
+ fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+
controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
val wct =
@@ -871,10 +891,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
- )
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() {
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ wct.assertReorderAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
@@ -899,6 +927,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
.thenReturn(Binder())
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
@@ -991,6 +1020,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
/** TODO: b/362720497 - add multi-desk version when minimization is implemented. */
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
val minimizedTask = setUpFreeformTask()
@@ -1569,6 +1599,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task = createTaskInfo(1)
whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
@@ -1736,7 +1767,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() {
- val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+ val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
.thenReturn(Binder())
@@ -1751,12 +1782,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
verify(desktopModeEnterExitTransitionListener)
.onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
- assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+ assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue)
}
@Test
fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() {
- val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+ val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
.thenReturn(Binder())
@@ -1768,7 +1799,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
verify(desktopModeEnterExitTransitionListener)
.onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
- assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+ assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue)
}
@Test
@@ -1802,6 +1833,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTask = setUpFreeformTask()
val fullscreenTask = setUpFullscreenTask()
markTaskHidden(freeformTask)
@@ -1828,6 +1860,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
)
fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() {
val freeformTask = setUpFreeformTask()
@@ -1840,7 +1873,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
)
val wct = getLatestEnterDesktopWct()
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ wct.assertReorderAt(index = 0, wallpaperToken)
verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask)
verify(desksOrganizer).activateDesk(wct, deskId = 0)
verify(desktopModeEnterExitTransitionListener)
@@ -1967,6 +2000,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
val newTask = setUpFullscreenTask()
val homeTask = setUpHomeTask()
@@ -2224,26 +2258,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun moveTaskToFront_remoteTransition_usesOneshotHandler() {
setUpHomeTask()
val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() }
- val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+ val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
.thenReturn(Binder())
controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
- assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value)
+ assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue)
}
@Test
fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() {
setUpHomeTask()
val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() }
- val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
+ val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>()
whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
.thenReturn(Binder())
controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition()))
- assertThat(transitionHandlerArgCaptor.value)
+ assertThat(transitionHandlerArgCaptor.firstValue)
.isInstanceOf(DesktopWindowLimitRemoteHandler::class.java)
}
@@ -2718,9 +2752,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
- captor.value.hierarchyOps.none { hop ->
+ captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
}
@@ -2759,9 +2793,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startPipTransition(captor.capture())
- captor.value.hierarchyOps.none { hop ->
+ captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
}
@@ -2775,9 +2809,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
- captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK }
+ captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK }
}
@Test
@@ -2791,10 +2825,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
// The only active task is being minimized.
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
// Adds remove wallpaper operation
- captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+ captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
}
@Test
@@ -2808,9 +2842,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
// The only active task is already minimized.
controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
- captor.value.hierarchyOps.none { hop ->
+ captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
}
@@ -2825,9 +2859,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON)
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
- captor.value.hierarchyOps.none { hop ->
+ captor.firstValue.hierarchyOps.none { hop ->
hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
}
}
@@ -2845,10 +2879,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
// task1 is the only visible task as task2 is minimized.
controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON)
// Adds remove wallpaper operation
- val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val captor = argumentCaptor<WindowContainerTransaction>()
verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
// Adds remove wallpaper operation
- captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+ captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
}
@Test
@@ -2987,6 +3021,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -3118,6 +3153,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTask1 = setUpFreeformTask()
val freeformTask2 = createFreeformTask()
@@ -3152,7 +3188,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task = createFreeformTask()
+
val result = controller.handleRequest(Binder(), createTransition(task))
assertNotNull(result, "Should handle request")
@@ -3180,6 +3218,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
// Second display task
createFreeformTask(displayId = SECOND_DISPLAY)
@@ -4635,7 +4674,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
- val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val wctArgument = argumentCaptor<WindowContainerTransaction>()
verify(splitScreenController)
.requestEnterSplitSelect(
eq(task2),
@@ -4643,9 +4682,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT),
eq(task2.configuration.windowConfiguration.bounds),
)
- assertThat(wctArgument.value.hierarchyOps).hasSize(1)
+ assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1)
// Removes wallpaper activity when leaving desktop
- wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false)
+ wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false)
}
@Test
@@ -4660,7 +4699,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false)
- val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val wctArgument = argumentCaptor<WindowContainerTransaction>()
verify(splitScreenController)
.requestEnterSplitSelect(
eq(task2),
@@ -4669,7 +4708,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
eq(task2.configuration.windowConfiguration.bounds),
)
// Does not remove wallpaper activity, as desktop still has visible desktop tasks
- assertThat(wctArgument.value.hierarchyOps).isEmpty()
+ assertThat(wctArgument.firstValue.hierarchyOps).isEmpty()
}
@Test
@@ -4677,7 +4716,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun newWindow_fromFullscreenOpensInSplit() {
setUpLandscapeDisplay()
val task = setUpFullscreenTask()
- val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+ val optionsCaptor = argumentCaptor<Bundle>()
runOpenNewWindow(task)
verify(splitScreenController)
.startIntent(
@@ -4690,7 +4729,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
eq(true),
eq(SPLIT_INDEX_UNDEFINED),
)
- assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+ assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
.isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
}
@@ -4699,7 +4738,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun newWindow_fromSplitOpensInSplit() {
setUpLandscapeDisplay()
val task = setUpSplitScreenTask()
- val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+ val optionsCaptor = argumentCaptor<Bundle>()
runOpenNewWindow(task)
verify(splitScreenController)
.startIntent(
@@ -4712,7 +4751,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
eq(true),
eq(SPLIT_INDEX_UNDEFINED),
)
- assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+ assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
.isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
}
@@ -4807,11 +4846,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
setUpLandscapeDisplay()
val task = setUpFullscreenTask()
val taskToRequest = setUpFreeformTask()
- val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+ val optionsCaptor = argumentCaptor<Bundle>()
runOpenInstance(task, taskToRequest.taskId)
verify(splitScreenController)
.startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
- assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+ assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
.isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
}
@@ -4821,11 +4860,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
setUpLandscapeDisplay()
val task = setUpSplitScreenTask()
val taskToRequest = setUpFreeformTask()
- val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java)
+ val optionsCaptor = argumentCaptor<Bundle>()
runOpenInstance(task, taskToRequest.taskId)
verify(splitScreenController)
.startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull())
- assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode)
+ assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode)
.isEqualTo(WINDOWING_MODE_MULTI_WINDOW)
}
@@ -5427,38 +5466,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun onUnhandledDrag_newFreeformIntent() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagEnabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
+ PointF(1200f, 700f),
+ Rect(240, 700, 2160, 1900),
+ tabTearingAnimationFlagEnabled = true,
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagDisabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
PointF(1200f, 700f),
Rect(240, 700, 2160, 1900),
+ tabTearingAnimationFlagEnabled = false,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagEnabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
+ PointF(50f, 700f),
+ Rect(0, 0, 500, 1000),
+ tabTearingAnimationFlagEnabled = true,
)
}
@Test
- fun onUnhandledDrag_newFreeformIntentSplitLeft() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagDisabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
PointF(50f, 700f),
Rect(0, 0, 500, 1000),
+ tabTearingAnimationFlagEnabled = false,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagEnabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
+ PointF(2500f, 700f),
+ Rect(500, 0, 1000, 1000),
+ tabTearingAnimationFlagEnabled = true,
)
}
@Test
- fun onUnhandledDrag_newFreeformIntentSplitRight() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagDisabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
PointF(2500f, 700f),
Rect(500, 0, 1000, 1000),
+ tabTearingAnimationFlagEnabled = false,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagEnabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+ PointF(1200f, 50f),
+ Rect(),
+ tabTearingAnimationFlagEnabled = true,
)
}
@Test
- fun onUnhandledDrag_newFullscreenIntent() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagDisabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
PointF(1200f, 50f),
Rect(),
+ tabTearingAnimationFlagEnabled = false,
)
}
@@ -5812,6 +5903,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
indicatorType: DesktopModeVisualIndicator.IndicatorType,
inputCoordinate: PointF,
expectedBounds: Rect,
+ tabTearingAnimationFlagEnabled: Boolean,
) {
setUpLandscapeDisplay()
val task = setUpFreeformTask()
@@ -5842,6 +5934,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
anyOrNull(),
eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT),
)
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ any(),
+ anyOrNull(),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
spyController.onUnhandledDrag(
mockPendingIntent,
@@ -5849,24 +5951,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
mockDragEvent,
mockCallback as Consumer<Boolean>,
)
- val arg: ArgumentCaptor<WindowContainerTransaction> =
- ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val arg = argumentCaptor<WindowContainerTransaction>()
var expectedWindowingMode: Int
if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) {
expectedWindowingMode = WINDOWING_MODE_FULLSCREEN
// Fullscreen launches currently use default transitions
- verify(transitions).startTransition(any(), capture(arg), anyOrNull())
+ verify(transitions).startTransition(any(), arg.capture(), anyOrNull())
} else {
expectedWindowingMode = WINDOWING_MODE_FREEFORM
- // All other launches use a special handler.
- verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+ if (tabTearingAnimationFlagEnabled) {
+ verify(desktopMixedTransitionHandler)
+ .startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ arg.capture(),
+ anyOrNull(),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ } else {
+ // All other launches use a special handler.
+ verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture())
+ }
}
assertThat(
- ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
+ ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions)
.launchWindowingMode
)
.isEqualTo(expectedWindowingMode)
- assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds)
+ assertThat(
+ ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions)
+ .launchBounds
+ )
.isEqualTo(expectedBounds)
}
@@ -6048,52 +6163,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@WindowManager.TransitionType type: Int = TRANSIT_OPEN,
handlerClass: Class<out TransitionHandler>? = null,
): WindowContainerTransaction {
- val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val arg = argumentCaptor<WindowContainerTransaction>()
if (handlerClass == null) {
verify(transitions).startTransition(eq(type), arg.capture(), isNull())
} else {
verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
}
- return arg.value
+ return arg.lastValue
}
private fun getLatestToggleResizeDesktopTaskWct(
currentBounds: Rect? = null
): WindowContainerTransaction {
- val arg: ArgumentCaptor<WindowContainerTransaction> =
- ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val arg = argumentCaptor<WindowContainerTransaction>()
verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
- .startTransition(capture(arg), eq(currentBounds))
- return arg.value
+ .startTransition(arg.capture(), eq(currentBounds))
+ return arg.lastValue
}
private fun getLatestDesktopMixedTaskWct(
@WindowManager.TransitionType type: Int = TRANSIT_OPEN
): WindowContainerTransaction {
- val arg: ArgumentCaptor<WindowContainerTransaction> =
- ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val arg = argumentCaptor<WindowContainerTransaction>()
verify(desktopMixedTransitionHandler)
- .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull())
- return arg.value
+ .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull())
+ return arg.lastValue
}
private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
- val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val arg = argumentCaptor<WindowContainerTransaction>()
verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
- return arg.value
+ return arg.lastValue
}
private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
- val arg: ArgumentCaptor<WindowContainerTransaction> =
- ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
- return arg.value
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture())
+ return arg.lastValue
}
private fun getLatestExitDesktopWct(): WindowContainerTransaction {
- val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ val arg = argumentCaptor<WindowContainerTransaction>()
verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
- return arg.value
+ return arg.lastValue
}
private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
index 83e48728c4f2..030bb1ace49d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt
@@ -123,8 +123,26 @@ class DesktopUserRepositoriesTest : ShellTestCase() {
assertThat(desktopRepository.userId).isEqualTo(PROFILE_ID_2)
}
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_HSUM)
+ fun getUserForProfile_flagEnabled_returnsUserIdForProfile() {
+ userRepositories.onUserChanged(USER_ID_2, mock())
+ val profiles: MutableList<UserInfo> =
+ mutableListOf(
+ UserInfo(USER_ID_2, "User profile", 0),
+ UserInfo(PROFILE_ID_1, "Work profile", 0),
+ )
+ userRepositories.onUserProfilesChanged(profiles)
+
+ val userIdForProfile = userRepositories.getUserIdForProfile(PROFILE_ID_1)
+
+ assertThat(userIdForProfile).isEqualTo(USER_ID_2)
+ }
+
private companion object {
const val USER_ID_1 = 7
+ const val USER_ID_2 = 8
+ const val PROFILE_ID_1 = 4
const val PROFILE_ID_2 = 5
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 25246d9984c3..1732875f1d57 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -70,6 +70,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
@Mock private lateinit var draggedTaskLeash: SurfaceControl
@Mock private lateinit var homeTaskLeash: SurfaceControl
+ @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories
private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
@@ -84,6 +85,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
context,
transitions,
taskDisplayAreaOrganizer,
+ desktopUserRepositories,
mockInteractionJankMonitor,
transactionSupplier,
)
@@ -93,6 +95,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
context,
transitions,
taskDisplayAreaOrganizer,
+ desktopUserRepositories,
mockInteractionJankMonitor,
transactionSupplier,
)
@@ -484,17 +487,22 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
val mergedFinishTransaction = mock<SurfaceControl.Transaction>()
val finishCallback = mock<Transitions.TransitionFinishCallback>()
val task = createTask()
- val startTransition = startDrag(
- springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null)
+ val startTransition =
+ startDrag(
+ springHandler,
+ task,
+ finishTransaction = playingFinishTransaction,
+ homeChange = null,
+ )
springHandler.onTaskResizeAnimationListener = mock()
springHandler.mergeAnimation(
transition = mock<IBinder>(),
info =
- createTransitionInfo(
- type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
- draggedTask = task,
- ),
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task,
+ ),
startT = mergedStartTransaction,
finishT = mergedFinishTransaction,
mergeTarget = startTransition,
@@ -723,7 +731,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
private fun createTransitionInfo(
type: Int,
draggedTask: RunningTaskInfo,
- homeChange: TransitionInfo.Change? = createHomeChange()) =
+ homeChange: TransitionInfo.Change? = createHomeChange(),
+ ) =
TransitionInfo(type, /* flags= */ 0).apply {
homeChange?.let { addChange(it) }
addChange( // Dragged Task.
@@ -741,11 +750,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
)
}
- private fun createHomeChange() = TransitionInfo.Change(mock(), homeTaskLeash).apply {
- parent = null
- taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
- flags = flags or FLAG_IS_WALLPAPER
- }
+ private fun createHomeChange() =
+ TransitionInfo.Change(mock(), homeTaskLeash).apply {
+ parent = null
+ taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+ flags = flags or FLAG_IS_WALLPAPER
+ }
private fun systemPropertiesKey(name: String) =
"${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index b50af741b2a6..439be9155b26 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -17,9 +17,13 @@
package com.android.wm.shell.recents;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX;
import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING;
import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING;
import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED;
@@ -44,9 +48,11 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
@@ -57,6 +63,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.os.IResultReceiver;
+import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
@@ -92,9 +99,13 @@ import java.util.Optional;
@SmallTest
public class RecentsTransitionHandlerTest extends ShellTestCase {
+ private static final int FREEFORM_TASK_CORNER_RADIUS = 32;
+
@Mock
private Context mContext;
@Mock
+ private Resources mResources;
+ @Mock
private TaskStackListenerImpl mTaskStackListener;
@Mock
private ShellCommandHandler mShellCommandHandler;
@@ -134,6 +145,10 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
when(mContext.getSystemService(KeyguardManager.class))
.thenReturn(mock(KeyguardManager.class));
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mResources.getDimensionPixelSize(
+ R.dimen.desktop_windowing_freeform_rounded_corner_radius)
+ ).thenReturn(FREEFORM_TASK_CORNER_RADIUS);
mShellInit = spy(new ShellInit(mMainExecutor));
mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
mDisplayInsetsController, mMainExecutor));
@@ -276,6 +291,57 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING);
}
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() {
+ ActivityManager.RunningTaskInfo freeformTask =
+ new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, freeformTask)
+ .build();
+ SurfaceControl leash = mergeTransitionInfo.getChanges().get(0).getLeash();
+ final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mRecentsTransitionHandler.startAnimation(
+ transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+
+ mRecentsTransitionHandler.findController(transition).merge(
+ mergeTransitionInfo,
+ new StubTransaction(),
+ finishT,
+ transition,
+ mock(Transitions.TransitionFinishCallback.class));
+ mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
+ false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+ mMainExecutor.flushAll();
+
+ verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS);
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ public void testFinish_returningToFreeformTasks_setsCornerRadius() {
+ ActivityManager.RunningTaskInfo freeformTask =
+ new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ TransitionInfo transitionInfo = new TransitionInfoBuilder(TRANSIT_CLOSE)
+ .addChange(TRANSIT_CLOSE, freeformTask)
+ .build();
+ SurfaceControl leash = transitionInfo.getChanges().get(0).getLeash();
+ final IBinder transition = startRecentsTransition(/* synthetic= */ false);
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ mRecentsTransitionHandler.startAnimation(
+ transition, transitionInfo, new StubTransaction(), finishT,
+ mock(Transitions.TransitionFinishCallback.class));
+
+ mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
+ false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+ mMainExecutor.flushAll();
+
+
+ verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS);
+ }
+
private IBinder startRecentsTransition(boolean synthetic) {
return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class));
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
index e28d6ff8bf7f..fd22a84dee5d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt
@@ -16,8 +16,10 @@
package com.android.wm.shell.shared.bubbles
+import android.content.Context
import android.graphics.Insets
import android.graphics.Rect
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker
@@ -27,11 +29,14 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
+private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit
+
@SmallTest
@RunWith(AndroidJUnit4::class)
/** Unit tests for [DragZoneFactory]. */
class DragZoneFactoryTest {
+ private val context = getApplicationContext<Context>()
private lateinit var dragZoneFactory: DragZoneFactory
private val tabletPortrait =
DeviceConfig(
@@ -55,184 +60,238 @@ class DragZoneFactoryTest {
@Test
fun dragZonesForBubbleBar_tablet() {
dragZoneFactory =
- DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ DragZoneFactory(
+ context,
+ tabletPortrait,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.Bubble::class.java,
- DragZone.Bubble::class.java,
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
)
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForBubble_tablet_portrait() {
dragZoneFactory =
- DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ DragZoneFactory(
+ context,
+ tabletPortrait,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- DragZone.FullScreen::class.java,
- DragZone.DesktopWindow::class.java,
- DragZone.Split.Top::class.java,
- DragZone.Split.Bottom::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForBubble_tablet_landscape() {
- dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ tabletLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- DragZone.FullScreen::class.java,
- DragZone.DesktopWindow::class.java,
- DragZone.Split.Left::class.java,
- DragZone.Split.Right::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForBubble_foldable_portrait() {
- dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldablePortrait,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- DragZone.FullScreen::class.java,
- DragZone.Split.Left::class.java,
- DragZone.Split.Right::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForBubble_foldable_landscape() {
- dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldableLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- DragZone.FullScreen::class.java,
- DragZone.Split.Top::class.java,
- DragZone.Split.Bottom::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForExpandedView_tablet_portrait() {
dragZoneFactory =
- DragZoneFactory(tabletPortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ DragZoneFactory(
+ context,
+ tabletPortrait,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(
DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
)
- val expectedZones: List<Class<out DragZone>> =
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.FullScreen::class.java,
- DragZone.DesktopWindow::class.java,
- DragZone.Split.Top::class.java,
- DragZone.Split.Bottom::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForExpandedView_tablet_landscape() {
- dragZoneFactory = DragZoneFactory(tabletLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ tabletLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
- dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ dragZoneFactory.createSortedDragZones(
+ DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+ )
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.FullScreen::class.java,
- DragZone.DesktopWindow::class.java,
- DragZone.Split.Left::class.java,
- DragZone.Split.Right::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.DesktopWindow>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForExpandedView_foldable_portrait() {
- dragZoneFactory = DragZoneFactory(foldablePortrait, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldablePortrait,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
- dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ dragZoneFactory.createSortedDragZones(
+ DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+ )
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.FullScreen::class.java,
- DragZone.Split.Left::class.java,
- DragZone.Split.Right::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Left>(),
+ verifyInstance<DragZone.Split.Right>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForExpandedView_foldable_landscape() {
- dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldableLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
- dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
- val expectedZones: List<Class<out DragZone>> =
+ dragZoneFactory.createSortedDragZones(
+ DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+ )
+ val expectedZones: List<DragZoneVerifier> =
listOf(
- DragZone.Dismiss::class.java,
- DragZone.FullScreen::class.java,
- DragZone.Split.Top::class.java,
- DragZone.Split.Bottom::class.java,
- DragZone.Bubble.Left::class.java,
- DragZone.Bubble.Right::class.java,
- )
- dragZones.zip(expectedZones).forEach { (zone, expectedType) ->
- assertThat(zone).isInstanceOf(expectedType)
- }
+ verifyInstance<DragZone.Dismiss>(),
+ verifyInstance<DragZone.FullScreen>(),
+ verifyInstance<DragZone.Split.Top>(),
+ verifyInstance<DragZone.Split.Bottom>(),
+ verifyInstance<DragZone.Bubble.Left>(),
+ verifyInstance<DragZone.Bubble.Right>(),
+ )
+ assertThat(dragZones).hasSize(expectedZones.size)
+ dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) }
}
@Test
fun dragZonesForBubble_tablet_desktopModeDisabled() {
isDesktopWindowModeSupported = false
- dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldableLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT))
assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
@@ -241,9 +300,21 @@ class DragZoneFactoryTest {
@Test
fun dragZonesForExpandedView_tablet_desktopModeDisabled() {
isDesktopWindowModeSupported = false
- dragZoneFactory = DragZoneFactory(foldableLandscape, splitScreenModeChecker, desktopWindowModeChecker)
+ dragZoneFactory =
+ DragZoneFactory(
+ context,
+ foldableLandscape,
+ splitScreenModeChecker,
+ desktopWindowModeChecker
+ )
val dragZones =
- dragZoneFactory.createSortedDragZones(DraggedObject.ExpandedView(BubbleBarLocation.LEFT))
+ dragZoneFactory.createSortedDragZones(
+ DraggedObject.ExpandedView(BubbleBarLocation.LEFT)
+ )
assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty()
}
+
+ private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone ->
+ assertThat(dragZone).isInstanceOf(T::class.java)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
new file mode 100644
index 000000000000..efb91c5fbfda
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.wm.shell.shared.bubbles
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFails
+
+/** Unit tests for [DropTargetManager]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DropTargetManagerTest {
+
+ private lateinit var dropTargetManager: DropTargetManager
+ private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener
+ private val dropTarget = Rect(0, 0, 0, 0)
+
+ // create 3 drop zones that are horizontally next to each other
+ // -------------------------------------------------
+ // | | | |
+ // | bubble | | bubble |
+ // | | dismiss | |
+ // | left | | right |
+ // | | | |
+ // -------------------------------------------------
+ private val bubbleLeftDragZone =
+ DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = dropTarget)
+ private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100))
+ private val bubbleRightDragZone =
+ DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = dropTarget)
+
+ @Before
+ fun setUp() {
+ dragZoneChangedListener = FakeDragZoneChangedListener()
+ dropTargetManager = DropTargetManager(isLayoutRtl = false, dragZoneChangedListener)
+ }
+
+ @Test
+ fun onDragStarted_notifiesInitialDragZone() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone)
+ )
+ assertThat(dragZoneChangedListener.initialDragZone).isEqualTo(bubbleLeftDragZone)
+ }
+
+ @Test
+ fun onDragStarted_missingExpectedDragZone_fails() {
+ assertFails {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.RIGHT),
+ listOf(bubbleLeftDragZone)
+ )
+ }
+ }
+
+ @Test
+ fun onDragUpdated_notifiesDragZoneChanged() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+ )
+ dropTargetManager.onDragUpdated(
+ bubbleRightDragZone.bounds.centerX(),
+ bubbleRightDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
+
+ dropTargetManager.onDragUpdated(
+ dismissDragZone.bounds.centerX(),
+ dismissDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
+ }
+
+ @Test
+ fun onDragUpdated_withinSameZone_doesNotNotify() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+ )
+ dropTargetManager.onDragUpdated(
+ bubbleLeftDragZone.bounds.centerX(),
+ bubbleLeftDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isNull()
+ assertThat(dragZoneChangedListener.toDragZone).isNull()
+ }
+
+ @Test
+ fun onDragUpdated_outsideAllZones_doesNotNotify() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone)
+ )
+ val pointX = 200
+ val pointY = 200
+ assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse()
+ assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse()
+ dropTargetManager.onDragUpdated(pointX, pointY)
+ assertThat(dragZoneChangedListener.fromDragZone).isNull()
+ assertThat(dragZoneChangedListener.toDragZone).isNull()
+ }
+
+ @Test
+ fun onDragUpdated_hasOverlappingZones_notifiesFirstDragZoneChanged() {
+ // create a drag zone that spans across the width of all 3 drag zones, but extends below
+ // them
+ val splitDragZone = DragZone.Split.Left(bounds = Rect(0, 0, 300, 200))
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone, splitDragZone)
+ )
+
+ // drag to a point that is within both the bubble right zone and split zone
+ val (pointX, pointY) =
+ Pair(
+ bubbleRightDragZone.bounds.centerX(),
+ bubbleRightDragZone.bounds.centerY()
+ )
+ assertThat(splitDragZone.contains(pointX, pointY)).isTrue()
+ dropTargetManager.onDragUpdated(pointX, pointY)
+ // verify we dragged to the bubble right zone because that has higher priority than split
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone)
+
+ dropTargetManager.onDragUpdated(
+ bubbleRightDragZone.bounds.centerX(),
+ 150 // below the bubble and dismiss drag zones but within split
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone)
+
+ val (dismissPointX, dismissPointY) =
+ Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY())
+ assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue()
+ dropTargetManager.onDragUpdated(dismissPointX, dismissPointY)
+ assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone)
+ assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone)
+ }
+
+ @Test
+ fun onDragUpdated_afterDragEnded_doesNotNotify() {
+ dropTargetManager.onDragStarted(
+ DraggedObject.Bubble(BubbleBarLocation.LEFT),
+ listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone)
+ )
+ dropTargetManager.onDragEnded()
+ dropTargetManager.onDragUpdated(
+ bubbleRightDragZone.bounds.centerX(),
+ bubbleRightDragZone.bounds.centerY()
+ )
+ assertThat(dragZoneChangedListener.fromDragZone).isNull()
+ assertThat(dragZoneChangedListener.toDragZone).isNull()
+ }
+
+ private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener {
+ var initialDragZone: DragZone? = null
+ var fromDragZone: DragZone? = null
+ var toDragZone: DragZone? = null
+
+ override fun onInitialDragZoneSet(dragZone: DragZone) {
+ initialDragZone = dragZone
+ }
+ override fun onDragZoneChanged(from: DragZone, to: DragZone) {
+ fromDragZone = from
+ toDragZone = to
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index 33f14acd0f02..391d46287498 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -157,33 +157,33 @@ class DesktopModeStatusTest : ShellTestCase() {
}
@Test
- fun isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() {
- doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+ fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() {
+ doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
- assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+ assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue()
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
- fun isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() {
- assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
+ fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() {
+ assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse()
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
- fun isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() {
- assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
+ fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() {
+ assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse()
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
- fun isDeviceEligibleForDesktopMode_supportFlagOn_configDevOptModeOn_returnsTrue() {
+ fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_configDevOptModeOn_returnsTrue() {
doReturn(true).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
- assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+ assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue()
}
@DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index b9d6a454694d..e5a6a6d258dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -360,7 +360,8 @@ public class SplitTransitionTests extends ShellTestCase {
mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT,
mock(SurfaceControl.Transaction.class));
} else {
- mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT,
+ mStageCoordinator.onRecentsInSplitAnimationFinishing(
+ mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT,
mock(SurfaceControl.Transaction.class));
}
assertFalse(mStageCoordinator.isSplitScreenVisible());
@@ -430,7 +431,8 @@ public class SplitTransitionTests extends ShellTestCase {
mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT,
mock(SurfaceControl.Transaction.class));
} else {
- mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT,
+ mStageCoordinator.onRecentsInSplitAnimationFinishing(
+ mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT,
mock(SurfaceControl.Transaction.class));
}
assertTrue(mStageCoordinator.isSplitScreenVisible());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
index 53ae967e7bbf..067dcec5d65d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
@@ -73,7 +73,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest :
.spyStatic(DesktopModeStatus::class.java)
.spyStatic(DragPositioningCallbackUtility::class.java)
.startMocking()
- doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+ doReturn(false).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())}
setUpCommon()
whenever(mockDisplayController.getDisplay(anyInt())).thenReturn(mockDisplay)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index f15418adf1e3..49812d381178 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -116,7 +116,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
.spyStatic(DragPositioningCallbackUtility::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) }
+ doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) }
+ doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) }
doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) }
setUpCommon()
@@ -384,7 +385,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
- doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
+ doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) }
setUpMockDecorationsForTasks(task)
onTaskOpening(task)
diff --git a/libs/hwui/OWNERS b/libs/hwui/OWNERS
index bc174599a4d3..70d13ab8b3e5 100644
--- a/libs/hwui/OWNERS
+++ b/libs/hwui/OWNERS
@@ -4,7 +4,6 @@ alecmouri@google.com
djsollen@google.com
jreck@google.com
njawad@google.com
-scroggo@google.com
sumir@google.com
# For text, e.g. Typeface, Font, Minikin, etc.
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index b1550b0b6888..63a024b8e780 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -260,7 +260,7 @@ sk_sp<Bitmap> Bitmap::createFrom(AHardwareBuffer* hardwareBuffer, const SkImageI
#endif
sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr,
- size_t size, bool readOnly) {
+ size_t size, bool readOnly, int64_t id) {
#ifdef _WIN32 // ashmem not implemented on Windows
return nullptr;
#else
@@ -279,7 +279,7 @@ sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int f
}
}
- sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes));
+ sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes, id));
if (readOnly) {
bitmap->setImmutable();
}
@@ -334,7 +334,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info
: SkPixelRef(info.width(), info.height(), address, rowBytes)
, mInfo(validateAlpha(info))
, mPixelStorageType(PixelStorageType::Ashmem)
- , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) {
+ , mId(id != UNDEFINED_BITMAP_ID ? id : getId(mPixelStorageType)) {
mPixelStorage.ashmem.address = address;
mPixelStorage.ashmem.fd = fd;
mPixelStorage.ashmem.size = mappedSize;
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index 8abe6a8c445a..4e9bcf27c0ef 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -97,7 +97,7 @@ public:
BitmapPalette palette);
#endif
static sk_sp<Bitmap> createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr,
- size_t size, bool readOnly);
+ size_t size, bool readOnly, int64_t id);
static sk_sp<Bitmap> createFrom(const SkImageInfo&, SkPixelRef&);
int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); }
@@ -183,15 +183,15 @@ public:
static bool compress(const SkBitmap& bitmap, JavaCompressFormat format,
int32_t quality, SkWStream* stream);
-private:
- static constexpr uint64_t INVALID_BITMAP_ID = 0u;
+ static constexpr uint64_t UNDEFINED_BITMAP_ID = 0u;
+private:
static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes);
Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes);
Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info);
Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes,
- uint64_t id = INVALID_BITMAP_ID);
+ uint64_t id = UNDEFINED_BITMAP_ID);
#ifdef __ANDROID__ // Layoutlib does not support hardware acceleration
Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes,
BitmapPalette palette);
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index 29efd98b41d0..cfde0b28c0d5 100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -191,9 +191,8 @@ void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info,
info.width(), info.height(), isPremultiplied);
}
-jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
- int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
- int density) {
+jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk,
+ jobject ninePatchInsets, int density, int64_t id) {
static jmethodID gBitmap_constructorMethodID =
GetMethodIDOrDie(env, gBitmap_class,
"<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V");
@@ -208,10 +207,12 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
if (!isMutable) {
bitmapWrapper->bitmap().setImmutable();
}
+ int64_t bitmapId = id != Bitmap::UNDEFINED_BITMAP_ID ? id : bitmap->getId();
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
- static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper),
- bitmap->width(), bitmap->height(), density,
- isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc);
+ static_cast<jlong>(bitmapId),
+ reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(),
+ bitmap->height(), density, isPremultiplied, ninePatchChunk,
+ ninePatchInsets, fromMalloc);
if (env->ExceptionCheck() != 0) {
ALOGE("*** Uncaught exception returned from Java call!\n");
@@ -759,6 +760,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
const int32_t height = p.readInt32();
const int32_t rowBytes = p.readInt32();
const int32_t density = p.readInt32();
+ const int64_t sourceId = p.readInt64();
if (kN32_SkColorType != colorType &&
kRGBA_F16_SkColorType != colorType &&
@@ -815,7 +817,8 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
return STATUS_NO_MEMORY;
}
nativeBitmap =
- Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable);
+ Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size,
+ !isMutable, sourceId);
return STATUS_OK;
});
@@ -831,15 +834,15 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
}
return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr,
- nullptr, density);
+ nullptr, density, sourceId);
#else
jniThrowRuntimeException(env, "Cannot use parcels outside of Android");
return NULL;
#endif
}
-static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
- jlong bitmapHandle, jint density, jobject parcel) {
+static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density,
+ jobject parcel) {
#ifdef __ANDROID__ // Layoutlib does not support parcel
if (parcel == NULL) {
ALOGD("------- writeToParcel null parcel\n");
@@ -870,6 +873,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
binder_status_t status;
int fd = bitmapWrapper->bitmap().getAshmemFd();
if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) {
+ p.writeInt64(bitmapWrapper->bitmap().getId());
#if DEBUG_PARCEL
ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as "
"immutable blob (fds %s)",
@@ -889,7 +893,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)",
p.allowFds() ? "allowed" : "forbidden");
#endif
-
+ p.writeInt64(Bitmap::UNDEFINED_BITMAP_ID);
status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap);
if (status) {
doThrowRE(env, "Could not copy bitmap to parcel blob.");
diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h
index 21a93f066d9b..c93246a972b6 100644
--- a/libs/hwui/jni/Bitmap.h
+++ b/libs/hwui/jni/Bitmap.h
@@ -18,6 +18,7 @@
#include <jni.h>
#include <android/bitmap.h>
+#include <hwui/Bitmap.h>
struct SkImageInfo;
@@ -33,9 +34,9 @@ enum BitmapCreateFlags {
kBitmapCreateFlag_Premultiplied = 0x2,
};
-jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
- int bitmapCreateFlags, jbyteArray ninePatchChunk = nullptr,
- jobject ninePatchInsets = nullptr, int density = -1);
+jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags,
+ jbyteArray ninePatchChunk = nullptr, jobject ninePatchInsets = nullptr,
+ int density = -1, int64_t id = Bitmap::UNDEFINED_BITMAP_ID);
Bitmap& toBitmap(jlong bitmapHandle);
diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp
index b0f5423813b7..95e4e01d8df8 100644
--- a/libs/hwui/jni/ScopedParcel.cpp
+++ b/libs/hwui/jni/ScopedParcel.cpp
@@ -39,6 +39,16 @@ uint32_t ScopedParcel::readUint32() {
return temp;
}
+int64_t ScopedParcel::readInt64() {
+ int64_t temp = 0;
+ // TODO: This behavior-matches what android::Parcel does
+ // but this should probably be better
+ if (AParcel_readInt64(mParcel, &temp) != STATUS_OK) {
+ temp = 0;
+ }
+ return temp;
+}
+
float ScopedParcel::readFloat() {
float temp = 0.;
if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) {
diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h
index fd8d6a210f0f..f2f138fda43c 100644
--- a/libs/hwui/jni/ScopedParcel.h
+++ b/libs/hwui/jni/ScopedParcel.h
@@ -35,12 +35,16 @@ public:
uint32_t readUint32();
+ int64_t readInt64();
+
float readFloat();
void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); }
void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); }
+ void writeInt64(int64_t value) { AParcel_writeInt64(mParcel, value); }
+
void writeFloat(float value) { AParcel_writeFloat(mParcel, value); }
bool allowFds() const { return AParcel_getAllowFds(mParcel); }
diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp
index 747eb8e5ad1b..5406de8602d6 100644
--- a/libs/input/PointerControllerContext.cpp
+++ b/libs/input/PointerControllerContext.cpp
@@ -15,6 +15,7 @@
*/
#include "PointerControllerContext.h"
+
#include "PointerController.h"
namespace {
@@ -184,7 +185,7 @@ void PointerControllerContext::PointerAnimator::handleVsyncEvents() {
DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE];
while ((n = mDisplayEventReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) {
for (size_t i = 0; i < static_cast<size_t>(n); ++i) {
- if (buf[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) {
+ if (buf[i].header.type == DisplayEventType::DISPLAY_EVENT_VSYNC) {
timestamp = buf[i].header.timestamp;
gotVsync = true;
}
diff --git a/media/java/android/media/audiofx/HapticGenerator.java b/media/java/android/media/audiofx/HapticGenerator.java
index d2523ef43b9e..7f94ddea9b84 100644
--- a/media/java/android/media/audiofx/HapticGenerator.java
+++ b/media/java/android/media/audiofx/HapticGenerator.java
@@ -36,6 +36,20 @@ import java.util.UUID;
* <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions.
* <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio
* effects.
+ *
+ * <pre>{@code
+ * AudioManager audioManager = context.getSystemService(AudioManager.class);
+ * player = MediaPlayer.create(
+ * context,
+ * audioUri,
+ * new AudioAttributes.Builder().setHapticChannelsMuted(false).build(),
+ * audioManager.generateAudioSessionId()
+ * );
+ * if (HapticGenerator.isAvailable()) {
+ * HapticGenerator.create(player.getAudioSessionId()).setEnabled(true);
+ * }
+ * player.start();
+ * }</pre>
*/
public class HapticGenerator extends AudioEffect implements AutoCloseable {
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 2a6919c5e03d..0deed3982d9b 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -145,6 +145,16 @@ flag {
}
flag {
+ name: "enable_output_switcher_device_grouping"
+ namespace: "media_better_together"
+ description: "Enables selected items in Output Switcher to be grouped together."
+ bug: "388347018"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_prevention_of_keep_alive_route_providers"
namespace: "media_solutions"
description: "Enables mechanisms to prevent route providers from keeping malicious apps alive."
diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp
index 48cd53dc44d7..d50acd837bf9 100644
--- a/media/jni/android_media_MediaDrm.cpp
+++ b/media/jni/android_media_MediaDrm.cpp
@@ -27,7 +27,6 @@
#include "jni.h"
#include <nativehelper/JNIHelp.h>
-#include <android_companion_virtualdevice_flags.h>
#include <android/companion/virtualnative/IVirtualDeviceManagerNative.h>
#include <android/hardware/drm/1.3/IDrmFactory.h>
#include <binder/Parcel.h>
@@ -46,7 +45,6 @@
using ::android::companion::virtualnative::IVirtualDeviceManagerNative;
using ::android::os::PersistableBundle;
namespace drm = ::android::hardware::drm;
-namespace virtualdevice_flags = android::companion::virtualdevice::flags;
namespace android {
@@ -1050,11 +1048,6 @@ DrmPlugin::SecurityLevel jintToSecurityLevel(jint jlevel) {
}
std::vector<int> getVirtualDeviceIds() {
- if (!virtualdevice_flags::device_aware_drm()) {
- ALOGW("Device-aware DRM flag disabled.");
- return std::vector<int>();
- }
-
sp<IBinder> binder =
defaultServiceManager()->checkService(String16("virtualdevice_native"));
if (binder != nullptr) {
diff --git a/packages/EasterEgg/AndroidManifest.xml b/packages/EasterEgg/AndroidManifest.xml
index 96e5892f4d1d..bcc10ddde228 100644
--- a/packages/EasterEgg/AndroidManifest.xml
+++ b/packages/EasterEgg/AndroidManifest.xml
@@ -64,7 +64,7 @@
android:label="@string/u_egg_name"
android:icon="@drawable/android16_patch_adaptive"
android:configChanges="orientation|screenLayout|screenSize|density"
- android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
+ android:theme="@style/Theme.Landroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/EasterEgg/res/drawable/ic_planet_large.xml b/packages/EasterEgg/res/drawable/ic_planet_large.xml
new file mode 100644
index 000000000000..7ac7c38153f2
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_large.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_medium.xml b/packages/EasterEgg/res/drawable/ic_planet_medium.xml
new file mode 100644
index 000000000000..e997b45eb6e5
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_medium.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_small.xml b/packages/EasterEgg/res/drawable/ic_planet_small.xml
new file mode 100644
index 000000000000..43339573207b
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_small.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_planet_tiny.xml b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml
new file mode 100644
index 000000000000..c666765113da
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_planet_tiny.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
+ android:strokeWidth="2"
+ android:fillColor="#16161D"
+ android:strokeColor="#ffffff"/>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft.xml b/packages/EasterEgg/res/drawable/ic_spacecraft.xml
new file mode 100644
index 000000000000..3cef4ab29192
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ >
+ <group android:translateX="10" android:translateY="12">
+ <path
+ android:strokeColor="#FFFFFF"
+ android:strokeWidth="2"
+ android:pathData="
+M11.853 0
+C11.853 -4.418 8.374 -8 4.083 -8
+L-5.5 -8
+C-6.328 -8 -7 -7.328 -7 -6.5
+C-7 -5.672 -6.328 -5 -5.5 -5
+L-2.917 -5
+C-1.26 -5 0.083 -3.657 0.083 -2
+L0.083 2
+C0.083 3.657 -1.26 5 -2.917 5
+L-5.5 5
+C-6.328 5 -7 5.672 -7 6.5
+C-7 7.328 -6.328 8 -5.5 8
+L4.083 8
+C8.374 8 11.853 4.418 11.853 0
+Z
+ "/>
+ </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml
new file mode 100644
index 000000000000..7a0c70379f20
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft_filled.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:width="24dp"
+ android:viewportHeight="24" android:viewportWidth="24"
+ >
+ <group android:translateX="10" android:translateY="12">
+ <path
+ android:strokeColor="#FFFFFF"
+ android:fillColor="#000000"
+ android:strokeWidth="2"
+ android:pathData="
+M11.853 0
+C11.853 -4.418 8.374 -8 4.083 -8
+L-5.5 -8
+C-6.328 -8 -7 -7.328 -7 -6.5
+C-7 -5.672 -6.328 -5 -5.5 -5
+L-2.917 -5
+C-1.26 -5 0.083 -3.657 0.083 -2
+L0.083 2
+C0.083 3.657 -1.26 5 -2.917 5
+L-5.5 5
+C-6.328 5 -7 5.672 -7 6.5
+C-7 7.328 -6.328 8 -5.5 8
+L4.083 8
+C8.374 8 11.853 4.418 11.853 0
+Z
+ "/>
+ </group>
+</vector>
diff --git a/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml
new file mode 100644
index 000000000000..2d4ce106ef38
--- /dev/null
+++ b/packages/EasterEgg/res/drawable/ic_spacecraft_rotated.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/ic_spacecraft"
+ android:fromDegrees="0"
+ android:toDegrees="360"
+ /> \ No newline at end of file
diff --git a/packages/EasterEgg/res/values/themes.xml b/packages/EasterEgg/res/values/themes.xml
index 5b163043a356..3a87e456fc3b 100644
--- a/packages/EasterEgg/res/values/themes.xml
+++ b/packages/EasterEgg/res/values/themes.xml
@@ -1,7 +1,26 @@
-<resources>
+<?xml version="1.0" encoding="utf-8"?><!--
+Copyright (C) 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
<style name="ThemeOverlay.EasterEgg.AppWidgetContainer" parent="">
<item name="appWidgetBackgroundColor">@color/light_blue_600</item>
<item name="appWidgetTextColor">@color/light_blue_50</item>
</style>
-</resources> \ No newline at end of file
+
+ <style name="Theme.Landroid" parent="android:Theme.Material.NoActionBar">
+ <item name="android:windowLightStatusBar">false</item>
+ <item name="android:windowLightNavigationBar">false</item>
+ </style>
+</resources>
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
index fb5954ec9736..8214c540304e 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Autopilot.kt
@@ -41,14 +41,16 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
val telemetry: String
get() =
- listOf(
- "---- AUTOPILOT ENGAGED ----",
- "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
- "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
- )
- .joinToString("\n")
-
- private var strategy: String = "NONE"
+ if (enabled)
+ listOf(
+ "---- AUTOPILOT ENGAGED ----",
+ "TGT: " + (target?.name?.toUpperCase() ?: "SELECTING..."),
+ "EXE: $strategy" + if (debug.isNotEmpty()) " ($debug)" else "",
+ )
+ .joinToString("\n")
+ else ""
+
+ var strategy: String = "NONE"
private var debug: String = ""
override fun update(sim: Simulator, dt: Float) {
@@ -119,7 +121,7 @@ class Autopilot(val ship: Spacecraft, val universe: Universe) : Entity {
target.pos +
Vec2.makeWithAngleMag(
target.velocity.angle(),
- min(altitude / 2, target.velocity.mag())
+ min(altitude / 2, target.velocity.mag()),
)
leadingVector = leadingPos - ship.pos
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
index d040fba49fdf..e74863849efa 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/ComposeTools.kt
@@ -20,9 +20,19 @@ import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import kotlin.random.Random
@Composable fun Dp.toLocalPx() = with(LocalDensity.current) { this@toLocalPx.toPx() }
@@ -36,6 +46,40 @@ val flickerFadeIn =
animationSpec =
tween(
durationMillis = 1000,
- easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random)
+ easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random),
)
)
+
+fun flickerFadeInAfterDelay(delay: Int = 0) =
+ fadeIn(
+ animationSpec =
+ tween(
+ durationMillis = 1000,
+ delayMillis = delay,
+ easing = CubicBezierEasing(0f, 1f, 1f, 0f) * flickerFadeEasing(Random),
+ )
+ )
+
+@Composable
+fun ConsoleButton(
+ modifier: Modifier = Modifier,
+ textStyle: TextStyle = TextStyle.Default,
+ color: Color,
+ bgColor: Color,
+ borderColor: Color,
+ text: String,
+ onClick: () -> Unit,
+) {
+ Text(
+ style = textStyle,
+ color = color,
+ modifier =
+ modifier
+ .clickable { onClick() }
+ .background(color = bgColor)
+ .border(width = 1.dp, color = borderColor)
+ .padding(6.dp)
+ .minimumInteractiveComponentSize(),
+ text = text,
+ )
+}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
index d56e8b9e8d0e..8d4adf638bb3 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/DreamUniverse.kt
@@ -56,6 +56,8 @@ class DreamUniverse : DreamService() {
}
}
+ private var notifier: UniverseProgressNotifier? = null
+
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@@ -76,8 +78,8 @@ class DreamUniverse : DreamService() {
Random.nextFloat() * PI2f,
Random.nextFloatInRange(
PLANET_ORBIT_RANGE.start,
- PLANET_ORBIT_RANGE.endInclusive
- )
+ PLANET_ORBIT_RANGE.endInclusive,
+ ),
)
}
@@ -94,9 +96,11 @@ class DreamUniverse : DreamService() {
composeView.setContent {
Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
DebugText(DEBUG_TEXT)
- Telemetry(universe)
+ Telemetry(universe, showControls = false)
}
+ notifier = UniverseProgressNotifier(this, universe)
+
composeView.setViewTreeLifecycleOwner(lifecycleOwner)
composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
index 4f77b00b7570..95a60c7a5292 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/MainActivity.kt
@@ -21,6 +21,7 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
@@ -34,6 +35,7 @@ import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -46,6 +48,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -59,6 +62,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextStyle
@@ -74,9 +78,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import java.lang.Float.max
import java.lang.Float.min
import java.util.Calendar
@@ -85,11 +86,14 @@ import kotlin.math.absoluteValue
import kotlin.math.floor
import kotlin.math.sqrt
import kotlin.random.Random
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
enum class RandomSeedType {
Fixed,
Daily,
- Evergreen
+ Evergreen,
}
const val TEST_UNIVERSE = false
@@ -138,6 +142,10 @@ fun getDessertCode(): String =
else -> Build.VERSION.RELEASE_OR_CODENAME.replace(Regex("[a-z]*"), "")
}
+fun getSystemDesignation(universe: Universe): String {
+ return "${getDessertCode()}-${universe.randomSeed % 100_000}"
+}
+
val DEBUG_TEXT = mutableStateOf("Hello Universe")
const val SHOW_DEBUG_TEXT = false
@@ -150,13 +158,13 @@ fun DebugText(text: MutableState<String>) {
fontWeight = FontWeight.Medium,
fontSize = 9.sp,
color = Color.Yellow,
- text = text.value
+ text = text.value,
)
}
}
@Composable
-fun Telemetry(universe: Universe) {
+fun Telemetry(universe: Universe, showControls: Boolean) {
var topVisible by remember { mutableStateOf(false) }
var bottomVisible by remember { mutableStateOf(false) }
@@ -174,7 +182,6 @@ fun Telemetry(universe: Universe) {
LaunchedEffect("blah") {
delay(1000)
bottomVisible = true
- delay(1000)
topVisible = true
}
@@ -183,13 +190,11 @@ fun Telemetry(universe: Universe) {
// TODO: Narrow the scope of invalidation here to the specific data needed;
// the behavior below mimics the previous implementation of a snapshot ticker value
val recomposeScope = currentRecomposeScope
- Telescope(universe) {
- recomposeScope.invalidate()
- }
+ Telescope(universe) { recomposeScope.invalidate() }
BoxWithConstraints(
modifier =
- Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent),
+ Modifier.fillMaxSize().padding(6.dp).windowInsetsPadding(WindowInsets.safeContent)
) {
val wide = maxWidth > maxHeight
Column(
@@ -197,57 +202,82 @@ fun Telemetry(universe: Universe) {
Modifier.align(if (wide) Alignment.BottomEnd else Alignment.BottomStart)
.fillMaxWidth(if (wide) 0.45f else 1.0f)
) {
- universe.ship.autopilot?.let { autopilot ->
- if (autopilot.enabled) {
+ val autopilotEnabled = universe.ship.autopilot?.enabled == true
+ if (autopilotEnabled) {
+ universe.ship.autopilot?.let { autopilot ->
AnimatedVisibility(
modifier = Modifier,
visible = bottomVisible,
- enter = flickerFadeIn
+ enter = flickerFadeIn,
) {
Text(
style = textStyle,
color = Colors.Autopilot,
modifier = Modifier.align(Left),
- text = autopilot.telemetry
+ text = autopilot.telemetry,
)
}
}
}
- AnimatedVisibility(
- modifier = Modifier,
- visible = bottomVisible,
- enter = flickerFadeIn
- ) {
- Text(
- style = textStyle,
- color = Colors.Console,
- modifier = Modifier.align(Left),
- text =
- with(universe.ship) {
- val closest = universe.closestPlanet()
- val distToClosest = ((closest.pos - pos).mag() - closest.radius).toInt()
- listOfNotNull(
- landing?.let {
- "LND: ${it.planet.name.toUpperCase()}\nJOB: ${it.text}"
- }
- ?: if (distToClosest < 10_000) {
- "ALT: $distToClosest"
- } else null,
- "THR: %.0f%%".format(thrust.mag() * 100f),
- "POS: %s".format(pos.str("%+7.0f")),
- "VEL: %.0f".format(velocity.mag())
- )
- .joinToString("\n")
+ Row(modifier = Modifier.padding(top = 6.dp)) {
+ AnimatedVisibility(
+ modifier = Modifier.weight(1f),
+ visible = bottomVisible,
+ enter = flickerFadeIn,
+ ) {
+ Text(
+ style = textStyle,
+ color = Colors.Console,
+ text =
+ with(universe.ship) {
+ val closest = universe.closestPlanet()
+ val distToClosest =
+ ((closest.pos - pos).mag() - closest.radius).toInt()
+ listOfNotNull(
+ landing?.let {
+ "LND: ${it.planet.name.toUpperCase()}\n" +
+ "JOB: ${it.text.toUpperCase()}"
+ }
+ ?: if (distToClosest < 10_000) {
+ "ALT: $distToClosest"
+ } else null,
+ "THR: %.0f%%".format(thrust.mag() * 100f),
+ "POS: %s".format(pos.str("%+7.0f")),
+ "VEL: %.0f".format(velocity.mag()),
+ )
+ .joinToString("\n")
+ },
+ )
+ }
+
+ if (showControls) {
+ AnimatedVisibility(
+ visible = bottomVisible,
+ enter = flickerFadeInAfterDelay(500),
+ ) {
+ ConsoleButton(
+ textStyle = textStyle,
+ color = Colors.Console,
+ bgColor = if (autopilotEnabled) Colors.Autopilot else Color.Transparent,
+ borderColor = Colors.Console,
+ text = "AUTO",
+ ) {
+ universe.ship.autopilot?.let {
+ it.enabled = !it.enabled
+ DYNAMIC_ZOOM = it.enabled
+ if (!it.enabled) universe.ship.thrust = Vec2.Zero
+ }
}
- )
+ }
+ }
}
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.TopStart),
visible = topVisible,
- enter = flickerFadeIn
+ enter = flickerFadeInAfterDelay(1000),
) {
Text(
style = textStyle,
@@ -263,13 +293,12 @@ fun Telemetry(universe: Universe) {
text =
(with(universe.star) {
listOf(
- " STAR: $name (${getDessertCode()}-" +
- "${universe.randomSeed % 100_000})",
+ " STAR: $name (${getSystemDesignation(universe)})",
" CLASS: ${cls.name}",
"RADIUS: ${radius.toInt()}",
" MASS: %.3g".format(mass),
"BODIES: ${explored.size} / ${universe.planets.size}",
- ""
+ "",
)
} +
explored
@@ -280,11 +309,11 @@ fun Telemetry(universe: Universe) {
" ATMO: ${it.atmosphere.capitalize()}",
" FAUNA: ${it.fauna.capitalize()}",
" FLORA: ${it.flora.capitalize()}",
- ""
+ "",
)
}
.flatten())
- .joinToString("\n")
+ .joinToString("\n"),
// TODO: different colors, highlight latest discovery
)
@@ -293,6 +322,7 @@ fun Telemetry(universe: Universe) {
}
class MainActivity : ComponentActivity() {
+ private var notifier: UniverseProgressNotifier? = null
private var foldState = mutableStateOf<FoldingFeature?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -300,7 +330,7 @@ class MainActivity : ComponentActivity() {
onWindowLayoutInfoChange()
- enableEdgeToEdge()
+ enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Red.toArgb()))
val universe = Universe(namer = Namer(resources), randomSeed = randomSeed())
@@ -312,12 +342,13 @@ class MainActivity : ComponentActivity() {
com.android.egg.ComponentActivationActivity.lockUnlockComponents(applicationContext)
- // for autopilot testing in the activity
- // val autopilot = Autopilot(universe.ship, universe)
- // universe.ship.autopilot = autopilot
- // universe.add(autopilot)
- // autopilot.enabled = true
- // DYNAMIC_ZOOM = autopilot.enabled
+ // set up the autopilot in case we need it
+ val autopilot = Autopilot(universe.ship, universe)
+ universe.ship.autopilot = autopilot
+ universe.add(autopilot)
+ autopilot.enabled = false
+
+ notifier = UniverseProgressNotifier(this, universe)
setContent {
Spaaaace(modifier = Modifier.fillMaxSize(), u = universe, foldState = foldState)
@@ -329,7 +360,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
minRadius = minRadius,
maxRadius = maxRadius,
- color = Color.Green
+ color = Color.Green,
) { vec ->
(universe.follow as? Spacecraft)?.let { ship ->
if (vec == Vec2.Zero) {
@@ -346,13 +377,13 @@ class MainActivity : ComponentActivity() {
ship.thrust =
Vec2.makeWithAngleMag(
a,
- lexp(minRadius, maxRadius, m).coerceIn(0f, 1f)
+ lexp(minRadius, maxRadius, m).coerceIn(0f, 1f),
)
}
}
}
}
- Telemetry(universe)
+ Telemetry(universe, true)
}
}
@@ -382,7 +413,7 @@ fun MainActivityPreview() {
Spaaaace(modifier = Modifier.fillMaxSize(), universe)
DebugText(DEBUG_TEXT)
- Telemetry(universe)
+ Telemetry(universe, true)
}
@Composable
@@ -391,7 +422,7 @@ fun FlightStick(
minRadius: Float = 0f,
maxRadius: Float = 1000f,
color: Color = Color.Green,
- onStickChanged: (vector: Vec2) -> Unit
+ onStickChanged: (vector: Vec2) -> Unit,
) {
val origin = remember { mutableStateOf(Vec2.Zero) }
val target = remember { mutableStateOf(Vec2.Zero) }
@@ -444,14 +475,14 @@ fun FlightStick(
PathEffect.dashPathEffect(
floatArrayOf(this.density * 1f, this.density * 2f)
)
- else null
- )
+ else null,
+ ),
)
drawLine(
color = color,
start = origin.value,
end = origin.value + Vec2.makeWithAngleMag(a, mag),
- strokeWidth = 2f
+ strokeWidth = 2f,
)
}
}
@@ -462,15 +493,13 @@ fun FlightStick(
fun Spaaaace(
modifier: Modifier,
u: Universe,
- foldState: MutableState<FoldingFeature?> = mutableStateOf(null)
+ foldState: MutableState<FoldingFeature?> = mutableStateOf(null),
) {
LaunchedEffect(u) {
- while (true) withInfiniteAnimationFrameNanos { frameTimeNanos ->
- u.step(frameTimeNanos)
- }
+ while (true) withInfiniteAnimationFrameNanos { frameTimeNanos -> u.step(frameTimeNanos) }
}
- var cameraZoom by remember { mutableStateOf(1f) }
+ var cameraZoom by remember { mutableFloatStateOf(DEFAULT_CAMERA_ZOOM) }
var cameraOffset by remember { mutableStateOf(Offset.Zero) }
val transformableState =
@@ -501,15 +530,16 @@ fun Spaaaace(
val closest = u.closestPlanet()
val distToNearestSurf = max(0f, (u.ship.pos - closest.pos).mag() - closest.radius * 1.2f)
// val normalizedDist = clamp(distToNearestSurf, 50f, 50_000f) / 50_000f
- if (DYNAMIC_ZOOM) {
- cameraZoom =
- expSmooth(
- cameraZoom,
- clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM),
- dt = u.dt,
- speed = 1.5f
- )
- } else if (!TOUCH_CAMERA_ZOOM) cameraZoom = DEFAULT_CAMERA_ZOOM
+ val targetZoom =
+ if (DYNAMIC_ZOOM) {
+ clamp(500f / distToNearestSurf, MIN_CAMERA_ZOOM, MAX_CAMERA_ZOOM)
+ } else {
+ DEFAULT_CAMERA_ZOOM
+ }
+ if (!TOUCH_CAMERA_ZOOM) {
+ cameraZoom = expSmooth(cameraZoom, targetZoom, dt = u.dt, speed = 1.5f)
+ }
+
if (!TOUCH_CAMERA_PAN) cameraOffset = (u.follow?.pos ?: Vec2.Zero) * -1f
// cameraZoom: metersToPixels
@@ -521,9 +551,9 @@ fun Spaaaace(
-cameraOffset -
Offset(
visibleSpaceSizeMeters.width * centerFracX,
- visibleSpaceSizeMeters.height * centerFracY
+ visibleSpaceSizeMeters.height * centerFracY,
),
- visibleSpaceSizeMeters
+ visibleSpaceSizeMeters,
)
var gridStep = 1000f
@@ -537,14 +567,14 @@ fun Spaaaace(
"fps: ${"%3.0f".format(1f / u.dt)} " +
"dt: ${u.dt}\n" +
((u.follow as? Spacecraft)?.let {
- "ship: p=%s v=%7.2f a=%6.3f t=%s\n".format(
- it.pos.str("%+7.1f"),
- it.velocity.mag(),
- it.angle,
- it.thrust.str("%+5.2f")
- )
- }
- ?: "") +
+ "ship: p=%s v=%7.2f a=%6.3f t=%s\n"
+ .format(
+ it.pos.str("%+7.1f"),
+ it.velocity.mag(),
+ it.angle,
+ it.thrust.str("%+5.2f"),
+ )
+ } ?: "") +
"star: '${u.star.name}' designation=UDC-${u.randomSeed % 100_000} " +
"class=${u.star.cls.name} r=${u.star.radius.toInt()} m=${u.star.mass}\n" +
"planets: ${u.planets.size}\n" +
@@ -574,7 +604,7 @@ fun Spaaaace(
translate(
-visibleSpaceRectMeters.center.x + size.width * 0.5f,
- -visibleSpaceRectMeters.center.y + size.height * 0.5f
+ -visibleSpaceRectMeters.center.y + size.height * 0.5f,
) {
// debug outer frame
// drawRect(
@@ -590,7 +620,7 @@ fun Spaaaace(
color = Colors.Eigengrau2,
start = Offset(x, visibleSpaceRectMeters.top),
end = Offset(x, visibleSpaceRectMeters.bottom),
- strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+ strokeWidth = (if ((x % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom,
)
x += gridStep
}
@@ -601,7 +631,7 @@ fun Spaaaace(
color = Colors.Eigengrau2,
start = Offset(visibleSpaceRectMeters.left, y),
end = Offset(visibleSpaceRectMeters.right, y),
- strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom
+ strokeWidth = (if ((y % (gridStep * 10) == 0f)) 3f else 1.5f) / cameraZoom,
)
y += gridStep
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
index 73318077f47a..babf1328c7d4 100644
--- a/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
+++ b/packages/EasterEgg/src/com/android/egg/landroid/Namer.kt
@@ -16,8 +16,8 @@
package com.android.egg.landroid
-import android.content.res.Resources
import com.android.egg.R
+import android.content.res.Resources
import kotlin.random.Random
const val SUFFIX_PROB = 0.75f
@@ -58,7 +58,7 @@ class Namer(resources: Resources) {
1f to "*",
1f to "^",
1f to "#",
- 0.1f to "(^*!%@##!!"
+ 0.1f to "(^*!%@##!!",
)
private var activities = Bag(resources.getStringArray(R.array.activities))
@@ -101,26 +101,26 @@ class Namer(resources: Resources) {
fun floraPlural(rng: Random): String {
return floraGenericPlurals.pull(rng)
}
+
fun faunaPlural(rng: Random): String {
return faunaGenericPlurals.pull(rng)
}
+
fun atmoPlural(rng: Random): String {
return atmoGenericPlurals.pull(rng)
}
val TEMPLATE_REGEX = Regex("""\{(flora|fauna|planet|atmo)\}""")
+
fun describeActivity(rng: Random, target: Planet?): String {
- return activities
- .pull(rng)
- .replace(TEMPLATE_REGEX) {
- when (it.groupValues[1]) {
- "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
- "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
- "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
- "planet" -> (target?.description ?: "SOME BODY") // once told me
- else -> "unknown template tag: ${it.groupValues[0]}"
- }
+ return activities.pull(rng).replace(TEMPLATE_REGEX) {
+ when (it.groupValues[1]) {
+ "flora" -> (target?.flora ?: "SOME") + " " + floraPlural(rng)
+ "fauna" -> (target?.fauna ?: "SOME") + " " + faunaPlural(rng)
+ "atmo" -> (target?.atmosphere ?: "SOME") + " " + atmoPlural(rng)
+ "planet" -> (target?.description ?: "SOME BODY") // once told me
+ else -> "unknown template tag: ${it.groupValues[0]}"
}
- .toUpperCase()
+ }
}
}
diff --git a/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt
new file mode 100644
index 000000000000..bb3a04df6f36
--- /dev/null
+++ b/packages/EasterEgg/src/com/android/egg/landroid/UniverseProgressNotifier.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.egg.landroid
+
+import com.android.egg.R
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.util.lerp
+import kotlinx.coroutines.DisposableHandle
+
+const val CHANNEL_ID = "progress"
+const val CHANNEL_NAME = "Spacecraft progress"
+const val UPDATE_FREQUENCY_SEC = 1f
+
+fun lerpRange(range: ClosedFloatingPointRange<Float>, x: Float): Float =
+ lerp(range.start, range.endInclusive, x)
+
+class UniverseProgressNotifier(val context: Context, val universe: Universe) {
+ private val notificationId = universe.randomSeed.toInt()
+ private val chan =
+ NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
+ .apply { lockscreenVisibility = Notification.VISIBILITY_PUBLIC }
+ private val noman =
+ context.getSystemService(NotificationManager::class.java)?.apply {
+ createNotificationChannel(chan)
+ }
+
+ private val registration: DisposableHandle =
+ universe.addSimulationStepListener(this::onSimulationStep)
+
+ private val spacecraftIcon = Icon.createWithResource(context, R.drawable.ic_spacecraft_filled)
+ private val planetIcons =
+ listOf(
+ (lerpRange(PLANET_RADIUS_RANGE, 0.75f)) to
+ Icon.createWithResource(context, R.drawable.ic_planet_large),
+ (lerpRange(PLANET_RADIUS_RANGE, 0.5f)) to
+ Icon.createWithResource(context, R.drawable.ic_planet_medium),
+ (lerpRange(PLANET_RADIUS_RANGE, 0.25f)) to
+ Icon.createWithResource(context, R.drawable.ic_planet_small),
+ (PLANET_RADIUS_RANGE.start to
+ Icon.createWithResource(context, R.drawable.ic_planet_tiny)),
+ )
+
+ private fun getPlanetIcon(planet: Planet): Icon {
+ for ((radius, icon) in planetIcons) {
+ if (planet.radius > radius) return icon
+ }
+ return planetIcons.last().second
+ }
+
+ private val progress = Notification.ProgressStyle().setProgressTrackerIcon(spacecraftIcon)
+
+ private val builder =
+ Notification.Builder(context, CHANNEL_ID)
+ .setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
+ },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
+ )
+ )
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setColorized(true)
+ .setOngoing(true)
+ .setColor(Colors.Eigengrau2.toArgb())
+ .setStyle(progress)
+
+ private var lastUpdate = 0f
+ private var initialDistToTarget = 0
+
+ private fun onSimulationStep() {
+ if (universe.now - lastUpdate >= UPDATE_FREQUENCY_SEC) {
+ lastUpdate = universe.now
+ // android.util.Log.v("Landroid", "posting notification at time ${universe.now}")
+
+ var distToTarget = 0
+ val autopilot = universe.ship.autopilot
+ val autopilotEnabled: Boolean = autopilot?.enabled == true
+ val target = autopilot?.target
+ val landing = universe.ship.landing
+ val speed = universe.ship.velocity.mag()
+
+ if (landing != null) {
+ // landed
+ builder.setContentTitle("landed: ${landing.planet.name}")
+ builder.setContentText("currently: ${landing.text}")
+ builder.setShortCriticalText("landed")
+
+ progress.setProgress(progress.progressMax)
+ progress.setProgressIndeterminate(false)
+
+ builder.setStyle(progress)
+ } else if (autopilotEnabled) {
+ if (target != null) {
+ // autopilot en route
+ distToTarget = ((target.pos - universe.ship.pos).mag() - target.radius).toInt()
+ if (initialDistToTarget == 0) {
+ // we have a new target!
+ initialDistToTarget = distToTarget
+ progress.progressEndIcon = getPlanetIcon(target)
+ }
+
+ val eta = if (speed > 0) "%1.0fs".format(distToTarget / speed) else "???"
+ builder.setContentTitle("headed to: ${target.name}")
+ builder.setContentText(
+ "autopilot is ${autopilot.strategy.toLowerCase()}" +
+ "\ndist: ${distToTarget}u // eta: $eta"
+ )
+ // fun fact: ProgressStyle was originally EnRouteStyle
+ builder.setShortCriticalText("en route")
+
+ progress
+ .setProgressSegments(
+ listOf(
+ Notification.ProgressStyle.Segment(initialDistToTarget)
+ .setColor(Colors.Track.toArgb())
+ )
+ )
+ .setProgress(initialDistToTarget - distToTarget)
+ .setProgressIndeterminate(false)
+ builder.setStyle(progress)
+ } else {
+ // no target
+ if (initialDistToTarget != 0) {
+ // just launched
+ initialDistToTarget = 0
+ progress.progressStartIcon = progress.progressEndIcon
+ progress.progressEndIcon = null
+ }
+
+ builder.setContentTitle("in space")
+ builder.setContentText("selecting new target...")
+ builder.setShortCriticalText("launched")
+
+ progress.setProgressIndeterminate(true)
+
+ builder.setStyle(progress)
+ }
+ } else {
+ // under user control
+
+ initialDistToTarget = 0
+
+ builder.setContentTitle("in space")
+ builder.setContentText("under manual control")
+ builder.setShortCriticalText("adrift")
+
+ builder.setStyle(null)
+ }
+
+ builder
+ .setSubText(getSystemDesignation(universe))
+ .setSmallIcon(R.drawable.ic_spacecraft_rotated)
+
+ val notification = builder.build()
+
+ // one of the silliest things about Android is that icon levels go from 0 to 10000
+ notification.iconLevel = (((universe.ship.angle + PI2f) / PI2f) * 10_000f).toInt()
+
+ noman?.notify(notificationId, notification)
+ }
+ }
+}
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto
index ec287c1b65b7..52a2160cdd74 100644
--- a/packages/SettingsLib/Graph/graph.proto
+++ b/packages/SettingsLib/Graph/graph.proto
@@ -95,6 +95,8 @@ message PreferenceProto {
optional PermissionsProto write_permissions = 18;
// Tag constants associated with the preference.
repeated string tags = 19;
+ // Permit to read and write preference value (the lower 15 bits is reserved for read permit).
+ optional int32 read_write_permit = 20;
// Target of an Intent
message ActionTarget {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index e511bf1c175d..13541b1ebc9a 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -56,6 +56,8 @@ import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.PreferenceTitleProvider
import com.android.settingslib.metadata.ReadWritePermit
+import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY
+import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
import com.android.settingslib.preference.PreferenceScreenFactory
import com.android.settingslib.preference.PreferenceScreenProvider
import java.util.Locale
@@ -415,52 +417,46 @@ fun PreferenceMetadata.toProto(
for (tag in metadata.tags(context)) addTags(tag)
}
persistent = metadata.isPersistent(context)
- if (persistent) {
- if (metadata is PersistentPreference<*>) {
- sensitivityLevel = metadata.sensitivityLevel
- metadata.getReadPermissions(context)?.let {
- if (it.size > 0) readPermissions = it.toProto()
- }
- metadata.getWritePermissions(context)?.let {
- if (it.size > 0) writePermissions = it.toProto()
+ if (metadata !is PersistentPreference<*>) return@preferenceProto
+ sensitivityLevel = metadata.sensitivityLevel
+ metadata.getReadPermissions(context)?.let { if (it.size > 0) readPermissions = it.toProto() }
+ metadata.getWritePermissions(context)?.let { if (it.size > 0) writePermissions = it.toProto() }
+ val readPermit = metadata.evalReadPermit(context, callingPid, callingUid)
+ val writePermit =
+ metadata.evalWritePermit(context, callingPid, callingUid) ?: ReadWritePermit.ALLOW
+ readWritePermit = ReadWritePermit.make(readPermit, writePermit)
+ if (
+ flags.includeValue() &&
+ enabled &&
+ (!hasAvailable() || available) &&
+ (!hasRestricted() || !restricted) &&
+ readPermit == ReadWritePermit.ALLOW
+ ) {
+ val storage = metadata.storage(context)
+ value = preferenceValueProto {
+ when (metadata.valueType) {
+ Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it }
+ Boolean::class.javaObjectType ->
+ storage.getBoolean(metadata.key)?.let { booleanValue = it }
+ Float::class.javaObjectType ->
+ storage.getFloat(metadata.key)?.let { floatValue = it }
+ else -> {}
}
}
- if (
- flags.includeValue() &&
- enabled &&
- (!hasAvailable() || available) &&
- (!hasRestricted() || !restricted) &&
- metadata is PersistentPreference<*> &&
- metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW
- ) {
- val storage = metadata.storage(context)
- value = preferenceValueProto {
- when (metadata.valueType) {
- Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it }
- Boolean::class.javaObjectType ->
- storage.getBoolean(metadata.key)?.let { booleanValue = it }
- Float::class.javaObjectType ->
- storage.getFloat(metadata.key)?.let { floatValue = it }
- else -> {}
- }
- }
- }
- if (flags.includeValueDescriptor()) {
- valueDescriptor = preferenceValueDescriptorProto {
- when (metadata) {
- is IntRangeValuePreference -> rangeValue = rangeValueProto {
- min = metadata.getMinValue(context)
- max = metadata.getMaxValue(context)
- step = metadata.getIncrementStep(context)
- }
- else -> {}
- }
- if (metadata is PersistentPreference<*>) {
- when (metadata.valueType) {
- Boolean::class.javaObjectType -> booleanType = true
- Float::class.javaObjectType -> floatType = true
+ }
+ if (flags.includeValueDescriptor()) {
+ valueDescriptor = preferenceValueDescriptorProto {
+ when (metadata) {
+ is IntRangeValuePreference -> rangeValue = rangeValueProto {
+ min = metadata.getMinValue(context)
+ max = metadata.getMaxValue(context)
+ step = metadata.getIncrementStep(context)
}
- }
+ else -> {}
+ }
+ when (metadata.valueType) {
+ Boolean::class.javaObjectType -> booleanType = true
+ Float::class.javaObjectType -> floatType = true
}
}
}
@@ -478,6 +474,20 @@ fun <T> PersistentPreference<T>.evalReadPermit(
else -> getReadPermit(context, callingPid, callingUid)
}
+/** Evaluates the write permit of a persistent preference. */
+fun <T> PersistentPreference<T>.evalWritePermit(
+ context: Context,
+ callingPid: Int,
+ callingUid: Int,
+): Int? =
+ when {
+ sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY ->
+ ReadWritePermit.DISALLOW
+ getWritePermissions(context)?.check(context, callingPid, callingUid) == false ->
+ ReadWritePermit.REQUIRE_APP_PERMISSION
+ else -> getWritePermit(context, callingPid, callingUid)
+ }
+
private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? {
if (isRoot && this is PreferenceScreenMetadata) {
val titleRes = screenTitle
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
index 60f9c6bb92a3..72f6934b5f35 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -36,8 +36,6 @@ import com.android.settingslib.metadata.PreferenceRemoteOpMetricsLogger
import com.android.settingslib.metadata.PreferenceRestrictionProvider
import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.metadata.ReadWritePermit
-import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY
-import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
/** Request to set preference value. */
class PreferenceSetterRequest(
@@ -223,13 +221,8 @@ fun <T> PersistentPreference<T>.evalWritePermit(
callingPid: Int,
callingUid: Int,
): Int =
- when {
- sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY ->
- ReadWritePermit.DISALLOW
- getWritePermissions(context)?.check(context, callingPid, callingUid) == false ->
- ReadWritePermit.REQUIRE_APP_PERMISSION
- else -> getWritePermit(context, value, callingPid, callingUid)
- }
+ evalWritePermit(context, callingPid, callingUid)
+ ?: getWritePermit(context, value, callingPid, callingUid)
/** Message codec for [PreferenceSetterRequest]. */
object PreferenceSetterRequestCodec : MessageCodec<PreferenceSetterRequest> {
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
index e456a7f1aa1c..c723dce82b5a 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -41,6 +41,19 @@ annotation class ReadWritePermit {
const val REQUIRE_APP_PERMISSION = 2
/** Require explicit user agreement (e.g. terms of service). */
const val REQUIRE_USER_AGREEMENT = 3
+
+ private const val READ_PERMIT_BITS = 15
+ private const val READ_PERMIT_MASK = (1 shl 16) - 1
+
+ /** Wraps given read and write permit into an integer. */
+ fun make(readPermit: @ReadWritePermit Int, writePermit: @ReadWritePermit Int): Int =
+ (writePermit shl READ_PERMIT_BITS) or readPermit
+
+ /** Extracts the read permit from given integer generated by [make]. */
+ fun getReadPermit(readWritePermit: Int): Int = readWritePermit and READ_PERMIT_MASK
+
+ /** Extracts the write permit from given integer generated by [make]. */
+ fun getWritePermit(readWritePermit: Int): Int = readWritePermit shr READ_PERMIT_BITS
}
}
@@ -81,6 +94,12 @@ interface PersistentPreference<T> : PreferenceMetadata {
/** The value type the preference is associated with. */
val valueType: Class<T>
+ /** The sensitivity level of the preference. */
+ val sensitivityLevel: @SensitivityLevel Int
+ get() = SensitivityLevel.UNKNOWN_SENSITIVITY
+
+ override fun isPersistent(context: Context) = true
+
/**
* Returns the key-value storage of the preference.
*
@@ -102,19 +121,27 @@ interface PersistentPreference<T> : PreferenceMetadata {
* behind the scene.
*/
fun getReadPermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int =
- PreferenceScreenRegistry.getReadPermit(
- context,
- callingPid,
- callingUid,
- this,
- )
+ PreferenceScreenRegistry.defaultReadPermit
/** Returns the required permissions to write preference value. */
fun getWritePermissions(context: Context): Permissions? = null
/**
* Returns if the external application (identified by [callingPid] and [callingUid]) is
- * permitted to write preference with given [value].
+ * permitted to write preference value. If the write permit depends on certain value, implement
+ * the overloading [getWritePermit] instead.
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it
+ * behind the scene.
+ */
+ fun getWritePermit(context: Context, callingPid: Int, callingUid: Int): @ReadWritePermit Int? =
+ null
+
+ /**
+ * Returns if the external application (identified by [callingPid] and [callingUid]) is
+ * permitted to write preference with given [value]. Note that if the overloading
+ * [getWritePermit] returns non null value, this method will be ignored!
*
* The underlying implementation does NOT need to check common states like isEnabled,
* isRestricted, isAvailable or permissions in [getWritePermissions]. The framework will do it
@@ -125,18 +152,7 @@ interface PersistentPreference<T> : PreferenceMetadata {
value: T?,
callingPid: Int,
callingUid: Int,
- ): @ReadWritePermit Int =
- PreferenceScreenRegistry.getWritePermit(
- context,
- value,
- callingPid,
- callingUid,
- this,
- )
-
- /** The sensitivity level of the preference. */
- val sensitivityLevel: @SensitivityLevel Int
- get() = SensitivityLevel.UNKNOWN_SENSITIVITY
+ ): @ReadWritePermit Int = PreferenceScreenRegistry.defaultWritePermit
}
/** Descriptor of values. */
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
index a8939ab0d902..7f2a61081fbb 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -127,7 +127,7 @@ interface PreferenceMetadata {
fun dependencies(context: Context): Array<String> = arrayOf()
/** Returns if the preference is persistent in datastore. */
- fun isPersistent(context: Context): Boolean = this is PersistentPreference<*>
+ fun isPersistent(context: Context): Boolean = false
/**
* Returns if preference value backup is allowed (by default returns `true` if preference is
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
index 246310984db9..8d4bfffb1fdb 100644
--- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -22,12 +22,18 @@ import android.util.Log
import com.android.settingslib.datastore.KeyValueStore
/** Registry of all available preference screens in the app. */
-object PreferenceScreenRegistry : ReadWritePermitProvider {
+object PreferenceScreenRegistry {
private const val TAG = "ScreenRegistry"
/** Provider of key-value store. */
private lateinit var keyValueStoreProvider: KeyValueStoreProvider
+ /** The default permit for external application to read preference values. */
+ var defaultReadPermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW
+
+ /** The default permit for external application to write preference values. */
+ var defaultWritePermit: @ReadWritePermit Int = ReadWritePermit.DISALLOW
+
/**
* Factories of all available [PreferenceScreenMetadata]s.
*
@@ -38,9 +44,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider {
/** Metrics logger for preference actions triggered by user interaction. */
var preferenceUiActionMetricsLogger: PreferenceUiActionMetricsLogger? = null
- private var readWritePermitProvider: ReadWritePermitProvider =
- object : ReadWritePermitProvider {}
-
/** Sets the [KeyValueStoreProvider]. */
fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) {
this.keyValueStoreProvider = keyValueStoreProvider
@@ -77,28 +80,6 @@ object PreferenceScreenRegistry : ReadWritePermitProvider {
return null
}
}
-
- /**
- * Sets the provider to check read write permit. Read and write requests are denied by default.
- */
- fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider) {
- this.readWritePermitProvider = readWritePermitProvider
- }
-
- override fun getReadPermit(
- context: Context,
- callingPid: Int,
- callingUid: Int,
- preference: PreferenceMetadata,
- ) = readWritePermitProvider.getReadPermit(context, callingPid, callingUid, preference)
-
- override fun getWritePermit(
- context: Context,
- value: Any?,
- callingPid: Int,
- callingUid: Int,
- preference: PreferenceMetadata,
- ) = readWritePermitProvider.getWritePermit(context, value, callingPid, callingUid, preference)
}
/** Provider of [KeyValueStore]. */
@@ -113,25 +94,3 @@ fun interface KeyValueStoreProvider {
*/
fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore?
}
-
-/** Provider of read and write permit. */
-interface ReadWritePermitProvider {
-
- val defaultReadWritePermit: @ReadWritePermit Int
- get() = ReadWritePermit.DISALLOW
-
- fun getReadPermit(
- context: Context,
- callingPid: Int,
- callingUid: Int,
- preference: PreferenceMetadata,
- ): @ReadWritePermit Int = defaultReadWritePermit
-
- fun getWritePermit(
- context: Context,
- value: Any?,
- callingPid: Int,
- callingUid: Int,
- preference: PreferenceMetadata,
- ): @ReadWritePermit Int = defaultReadWritePermit
-}
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index e173c5e996df..0f6a2a082e0c 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -118,6 +118,7 @@ public class SettingsSpinnerPreference extends Preference
spinner.setAdapter(mAdapter);
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
+ spinner.setLongClickable(false);
if (mShouldPerformClick) {
mShouldPerformClick = false;
// To show dropdown view.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
index 0c7d6f093289..b173db0a0505 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -37,7 +37,7 @@ object BluetoothLeBroadcastMetadataExt {
private const val KEY_BT_ADVERTISER_ADDRESS = "AD"
private const val KEY_BT_BROADCAST_ID = "BI"
private const val KEY_BT_BROADCAST_CODE = "BC"
- private const val KEY_BT_STREAM_METADATA = "MD"
+ private const val KEY_BT_PUBLIC_METADATA = "PM"
private const val KEY_BT_STANDARD_QUALITY = "SQ"
private const val KEY_BT_HIGH_QUALITY = "HQ"
@@ -84,7 +84,7 @@ object BluetoothLeBroadcastMetadataExt {
}
if (this.publicBroadcastMetadata != null &&
this.publicBroadcastMetadata?.rawMetadata?.size != 0) {
- entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString(
+ entries.add(Pair(KEY_BT_PUBLIC_METADATA, Base64.encodeToString(
this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
}
if ((this.audioConfigQuality and
@@ -160,7 +160,7 @@ object BluetoothLeBroadcastMetadataExt {
var sourceAdvertiserSid = -1
var broadcastId = -1
var broadcastName: String? = null
- var streamMetadata: BluetoothLeAudioContentMetadata? = null
+ var publicMetadata: BluetoothLeAudioContentMetadata? = null
var paSyncInterval = -1
var broadcastCode: ByteArray? = null
var audioConfigQualityStandard = -1
@@ -207,11 +207,11 @@ object BluetoothLeBroadcastMetadataExt {
broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) }
.toByteArray(), Base64.NO_WRAP)
}
- KEY_BT_STREAM_METADATA -> {
- require(streamMetadata == null) {
- "Duplicate streamMetadata $input"
+ KEY_BT_PUBLIC_METADATA -> {
+ require(publicMetadata == null) {
+ "Duplicate publicMetadata $input"
}
- streamMetadata = BluetoothLeAudioContentMetadata
+ publicMetadata = BluetoothLeAudioContentMetadata
.fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
}
KEY_BT_STANDARD_QUALITY -> {
@@ -256,7 +256,7 @@ object BluetoothLeBroadcastMetadataExt {
Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " +
"sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
"broadcastId=$broadcastId, broadcastName=$broadcastName, " +
- "streamMetadata=${streamMetadata != null}, " +
+ "publicMetadata=${publicMetadata != null}, " +
"paSyncInterval=$paSyncInterval, " +
"broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " +
"audioConfigQualityStandard=$audioConfigQualityStandard, " +
@@ -317,7 +317,7 @@ object BluetoothLeBroadcastMetadataExt {
setBroadcastName(broadcastName)
// QR code should set PBP(public broadcast profile) for auracast
setPublicBroadcast(true)
- setPublicBroadcastMetadata(streamMetadata)
+ setPublicBroadcastMetadata(publicMetadata)
setPaSyncInterval(paSyncInterval)
setEncrypted(broadcastCode != null)
setBroadcastCode(broadcastCode)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index bf86911ee683..572444edea29 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -30,11 +30,13 @@ import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.flags.Flags;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -385,7 +387,7 @@ public class CsipDeviceManager {
preferredMainDevice.refresh();
hasChanged = true;
}
- syncAudioSharingSourceIfNeeded(preferredMainDevice);
+ syncAudioSharingStatusIfNeeded(preferredMainDevice);
}
if (hasChanged) {
log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
@@ -399,13 +401,16 @@ public class CsipDeviceManager {
return userManager != null && userManager.isManagedProfile();
}
- private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) {
+ private void syncAudioSharingStatusIfNeeded(CachedBluetoothDevice mainDevice) {
boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingUIAvailable(mContext);
- if (isAudioSharingEnabled) {
+ if (isAudioSharingEnabled && mainDevice != null) {
if (isWorkProfile()) {
- log("addMemberDevicesIntoMainDevice: skip sync source for work profile");
+ log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, work profile");
return;
}
+ Set<CachedBluetoothDevice> deviceSet = new HashSet<>();
+ deviceSet.add(mainDevice);
+ deviceSet.addAll(mainDevice.getMemberDevice());
boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager)
&& BluetoothUtils.hasConnectedBroadcastSource(
mainDevice, mBtManager);
@@ -419,9 +424,6 @@ public class CsipDeviceManager {
if (metadata != null && assistant != null) {
log("addMemberDevicesIntoMainDevice: sync audio sharing source after "
+ "combining the top level devices.");
- Set<CachedBluetoothDevice> deviceSet = new HashSet<>();
- deviceSet.add(mainDevice);
- deviceSet.addAll(mainDevice.getMemberDevice());
Set<BluetoothDevice> sinksToSync = deviceSet.stream()
.map(CachedBluetoothDevice::getDevice)
.filter(device ->
@@ -435,8 +437,24 @@ public class CsipDeviceManager {
}
}
}
+ if (Flags.enableTemporaryBondDevicesUi()) {
+ log("addMemberDevicesIntoMainDevice: sync temp bond metadata for audio sharing "
+ + "sinks after combining the top level devices.");
+ Set<BluetoothDevice> sinksToSync = deviceSet.stream()
+ .map(CachedBluetoothDevice::getDevice).filter(Objects::nonNull).collect(
+ Collectors.toSet());
+ if (sinksToSync.stream().anyMatch(BluetoothUtils::isTemporaryBondDevice)) {
+ for (BluetoothDevice device : sinksToSync) {
+ if (!BluetoothUtils.isTemporaryBondDevice(device)) {
+ log("addMemberDevicesIntoMainDevice: sync temp bond metadata for "
+ + device.getAnonymizedAddress());
+ BluetoothUtils.setTemporaryBondMetadata(device);
+ }
+ }
+ }
+ }
} else {
- log("addMemberDevicesIntoMainDevice: skip sync source, flag disabled");
+ log("addMemberDevicesIntoMainDevice: skip sync audio sharing status, flag disabled");
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 69e41a36f48f..aa84571c73d0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -779,7 +779,7 @@ public abstract class InfoMediaManager {
static List<RouteListingPreference.Item> composePreferenceRouteListing(
RouteListingPreference routeListingPreference) {
boolean preferRouteListingOrdering =
- com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping()
&& preferRouteListingOrdering(routeListingPreference);
List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>();
List<RouteListingPreference.Item> itemList = routeListingPreference.getItems();
@@ -814,7 +814,7 @@ public abstract class InfoMediaManager {
* Returns an ordered list of available devices based on the provided {@code
* routeListingPreferenceItems}.
*
- * <p>The resulting order if enableOutputSwitcherSessionGrouping is disabled is:
+ * <p>The resulting order if enableOutputSwitcherDeviceGrouping is disabled is:
*
* <ol>
* <li>Selected routes.
@@ -822,7 +822,7 @@ public abstract class InfoMediaManager {
* <li>Not-selected, non-system, available routes sorted by route listing preference.
* </ol>
*
- * <p>The resulting order if enableOutputSwitcherSessionGrouping is enabled is:
+ * <p>The resulting order if enableOutputSwitcherDeviceGrouping is enabled is:
*
* <ol>
* <li>Selected routes sorted by route listing preference.
@@ -848,7 +848,7 @@ public abstract class InfoMediaManager {
Set<String> sortedRouteIds = new LinkedHashSet<>();
boolean addSelectedRlpItemsFirst =
- com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping()
&& preferRouteListingOrdering(routeListingPreference);
Set<String> selectedRouteIds = new HashSet<>();
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt
index 10156c404ebf..bac564c7d0f4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/MediaSessions.kt
@@ -20,6 +20,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaMetadata
import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
import android.media.session.MediaSession
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
@@ -98,16 +99,22 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
}
/** Set volume `level` to remote media `token` */
- fun setVolume(token: MediaSession.Token, level: Int) {
+ fun setVolume(sessionId: SessionId, volumeLevel: Int) {
+ when (sessionId) {
+ is SessionId.Media -> setMediaSessionVolume(sessionId.token, volumeLevel)
+ }
+ }
+
+ private fun setMediaSessionVolume(token: MediaSession.Token, volumeLevel: Int) {
val record = mRecords[token]
if (record == null) {
Log.w(TAG, "setVolume: No record found for token $token")
return
}
if (D.BUG) {
- Log.d(TAG, "Setting level to $level")
+ Log.d(TAG, "Setting level to $volumeLevel")
}
- record.controller.setVolumeTo(level, 0)
+ record.controller.setVolumeTo(volumeLevel, 0)
}
private fun onRemoteVolumeChangedH(sessionToken: MediaSession.Token, flags: Int) {
@@ -122,7 +129,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
)
}
val token = controller.sessionToken
- mCallbacks.onRemoteVolumeChanged(token, flags)
+ mCallbacks.onRemoteVolumeChanged(SessionId.from(token), flags)
}
private fun onUpdateRemoteSessionListH(sessionToken: MediaSession.Token?) {
@@ -158,7 +165,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
controller.registerCallback(record, mHandler)
}
val record = mRecords[token]
- val remote = isRemote(playbackInfo)
+ val remote = playbackInfo.isRemote()
if (remote) {
updateRemoteH(token, record!!.name, playbackInfo)
record.sentRemote = true
@@ -172,7 +179,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
Log.d(TAG, "Removing " + record.name + " sentRemote=" + record.sentRemote)
}
if (record.sentRemote) {
- mCallbacks.onRemoteRemoved(token)
+ mCallbacks.onRemoteRemoved(SessionId.from(token))
record.sentRemote = false
}
}
@@ -213,8 +220,8 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
private fun updateRemoteH(
token: MediaSession.Token,
name: String?,
- pi: MediaController.PlaybackInfo,
- ) = mCallbacks.onRemoteUpdate(token, name, pi)
+ playbackInfo: PlaybackInfo,
+ ) = mCallbacks.onRemoteUpdate(SessionId.from(token), name, VolumeInfo.from(playbackInfo))
private inner class MediaControllerRecord(val controller: MediaController) :
MediaController.Callback() {
@@ -225,7 +232,7 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
return method + " " + controller.packageName + " "
}
- override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) {
+ override fun onAudioInfoChanged(info: PlaybackInfo) {
if (D.BUG) {
Log.d(
TAG,
@@ -235,9 +242,9 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
sentRemote),
)
}
- val remote = isRemote(info)
+ val remote = info.isRemote()
if (!remote && sentRemote) {
- mCallbacks.onRemoteRemoved(controller.sessionToken)
+ mCallbacks.onRemoteRemoved(SessionId.from(controller.sessionToken))
sentRemote = false
} else if (remote) {
updateRemoteH(controller.sessionToken, name, info)
@@ -301,20 +308,36 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
}
}
+ /** Opaque id for ongoing sessions that support volume adjustment. */
+ sealed interface SessionId {
+
+ companion object {
+ fun from(token: MediaSession.Token) = Media(token)
+ }
+
+ data class Media(val token: MediaSession.Token) : SessionId
+ }
+
+ /** Holds session volume information. */
+ data class VolumeInfo(val currentVolume: Int, val maxVolume: Int) {
+
+ companion object {
+
+ fun from(playbackInfo: PlaybackInfo) =
+ VolumeInfo(playbackInfo.currentVolume, playbackInfo.maxVolume)
+ }
+ }
+
/** Callback for remote media sessions */
interface Callbacks {
/** Invoked when remote media session is updated */
- fun onRemoteUpdate(
- token: MediaSession.Token?,
- name: String?,
- pi: MediaController.PlaybackInfo?,
- )
+ fun onRemoteUpdate(token: SessionId?, name: String?, volumeInfo: VolumeInfo?)
/** Invoked when remote media session is removed */
- fun onRemoteRemoved(token: MediaSession.Token?)
+ fun onRemoteRemoved(token: SessionId?)
/** Invoked when remote volume is changed */
- fun onRemoteVolumeChanged(token: MediaSession.Token?, flags: Int)
+ fun onRemoteVolumeChanged(token: SessionId?, flags: Int)
}
companion object {
@@ -325,12 +348,11 @@ class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
const val UPDATE_REMOTE_SESSION_LIST: Int = 3
private const val USE_SERVICE_LABEL = false
-
- private fun isRemote(pi: MediaController.PlaybackInfo?): Boolean =
- pi != null && pi.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE
}
}
+private fun PlaybackInfo?.isRemote() = this?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
+
private fun MediaController.dump(n: Int, writer: PrintWriter) {
writer.println(" Controller $n: $packageName")
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
index fd14d1ff6786..2eccaa626f3b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
@@ -40,6 +40,8 @@ import android.content.Context;
import android.os.Looper;
import android.os.Parcel;
import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import com.android.settingslib.flags.Flags;
@@ -74,6 +76,9 @@ public class CsipDeviceManagerTest {
private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11";
private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22";
private final static String DEVICE_ADDRESS_3 = "AA:BB:CC:DD:EE:33";
+ private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
+ private static final String TEMP_BOND_METADATA =
+ "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>";
private final static int GROUP1 = 1;
private final BluetoothClass DEVICE_CLASS_1 =
createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES);
@@ -337,6 +342,7 @@ public class CsipDeviceManagerTest {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_returnTrue() {
// Condition: The preferredDevice is main and there is another main device in top list
// Expected Result: return true and there is the preferredDevice in top list
@@ -346,7 +352,6 @@ public class CsipDeviceManagerTest {
mCachedDevices.add(preferredDevice);
mCachedDevices.add(mCachedDevice2);
mCachedDevices.add(mCachedDevice3);
- mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
.isTrue();
@@ -359,6 +364,7 @@ public class CsipDeviceManagerTest {
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
public void
addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() {
// Condition: The preferredDevice is main and there is another main device in top list
@@ -369,7 +375,6 @@ public class CsipDeviceManagerTest {
mCachedDevices.add(preferredDevice);
mCachedDevices.add(mCachedDevice2);
mCachedDevices.add(mCachedDevice3);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mBroadcast.isEnabled(null)).thenReturn(true);
BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
@@ -377,6 +382,8 @@ public class CsipDeviceManagerTest {
BluetoothLeBroadcastReceiveState.class);
when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+ when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
when(mUserManager.isManagedProfile()).thenReturn(true);
@@ -387,10 +394,13 @@ public class CsipDeviceManagerTest {
assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false);
+ verify(mDevice1, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
}
@Test
- public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() {
+ @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
+ public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncState() {
// Condition: The preferredDevice is main and there is another main device in top list
// Expected Result: return true and there is the preferredDevice in top list
CachedBluetoothDevice preferredDevice = mCachedDevice1;
@@ -399,7 +409,6 @@ public class CsipDeviceManagerTest {
mCachedDevices.add(preferredDevice);
mCachedDevices.add(mCachedDevice2);
mCachedDevices.add(mCachedDevice3);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mBroadcast.isEnabled(null)).thenReturn(true);
BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
@@ -407,6 +416,8 @@ public class CsipDeviceManagerTest {
BluetoothLeBroadcastReceiveState.class);
when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+ when(mDevice2.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
.isTrue();
@@ -415,6 +426,8 @@ public class CsipDeviceManagerTest {
assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false);
+ verify(mDevice1).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
}
@Test
@@ -436,13 +449,13 @@ public class CsipDeviceManagerTest {
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_returnTrue() {
// Condition: The preferredDevice is member and there are two main device in top list
// Expected Result: return true and there is the preferredDevice in top list
CachedBluetoothDevice preferredDevice = mCachedDevice2;
BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice();
mCachedDevice3.setGroupId(GROUP1);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mBroadcast.isEnabled(null)).thenReturn(false);
assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
@@ -457,16 +470,20 @@ public class CsipDeviceManagerTest {
assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
verify(mAssistant, never()).addSource(any(BluetoothDevice.class),
any(BluetoothLeBroadcastMetadata.class), anyBoolean());
+ verify(mDevice2, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
+ verify(mDevice3, never()).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
}
@Test
- public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() {
+ @EnableFlags({Flags.FLAG_ENABLE_LE_AUDIO_SHARING, Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI})
+ public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncState() {
// Condition: The preferredDevice is member and there are two main device in top list
// Expected Result: return true and there is the preferredDevice in top list
CachedBluetoothDevice preferredDevice = mCachedDevice2;
BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice();
mCachedDevice3.setGroupId(GROUP1);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
when(mBroadcast.isEnabled(null)).thenReturn(true);
BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
@@ -474,6 +491,8 @@ public class CsipDeviceManagerTest {
BluetoothLeBroadcastReceiveState.class);
when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state));
+ when(mDevice1.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ .thenReturn(TEMP_BOND_METADATA.getBytes());
assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
.isTrue();
@@ -488,6 +507,10 @@ public class CsipDeviceManagerTest {
assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false);
verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false);
+ verify(mDevice2).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
+ verify(mDevice3).setMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS,
+ TEMP_BOND_METADATA.getBytes());
}
@Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index 93ebc84374b2..7b6604b3f1c6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -942,7 +942,7 @@ public class InfoMediaManagerTest {
assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void composePreferenceRouteListing_useSystemOrderingIsFalse() {
RouteListingPreference routeListingPreference =
@@ -955,7 +955,7 @@ public class InfoMediaManagerTest {
assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_4);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void composePreferenceRouteListing_useSystemOrderingIsTrue() {
RouteListingPreference routeListingPreference =
@@ -968,7 +968,7 @@ public class InfoMediaManagerTest {
assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_3);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void arrangeRouteListByPreference_useSystemOrderingIsFalse() {
RouteListingPreference routeListingPreference =
@@ -986,7 +986,7 @@ public class InfoMediaManagerTest {
assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void arrangeRouteListByPreference_useSystemOrderingIsTrue() {
RouteListingPreference routeListingPreference =
diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
index 1ad20dc02042..5f6eb5e49573 100644
--- a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
+++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
@@ -233,7 +233,7 @@ class BluetoothLeBroadcastMetadataExtTest {
const val QR_CODE_STRING =
"BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" +
- "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"
+ "PM:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"
const val QR_CODE_STRING_NON_ENCRYPTED =
"BLUETOOTH:UUID:184F;BN:SG9ja2V5;AT:0;AD:AABBCC001122;BI:DE51E9;SQ:1;AS:1;PI:FFFF;" +
"NS:1;BS:1;NB:1;;"
diff --git a/packages/Shell/src/com/android/shell/BugreportPrefs.java b/packages/Shell/src/com/android/shell/BugreportPrefs.java
index 93690d48cd04..b0fd925daec3 100644
--- a/packages/Shell/src/com/android/shell/BugreportPrefs.java
+++ b/packages/Shell/src/com/android/shell/BugreportPrefs.java
@@ -23,25 +23,24 @@ import android.content.SharedPreferences;
* Preferences related to bug reports.
*/
final class BugreportPrefs {
- static final String PREFS_BUGREPORT = "bugreports";
-
- private static final String KEY_WARNING_STATE = "warning-state";
-
- static final int STATE_UNKNOWN = 0;
- // Shows the warning dialog.
- static final int STATE_SHOW = 1;
- // Skips the warning dialog.
- static final int STATE_HIDE = 2;
static int getWarningState(Context context, int def) {
- final SharedPreferences prefs = context.getSharedPreferences(
- PREFS_BUGREPORT, Context.MODE_PRIVATE);
- return prefs.getInt(KEY_WARNING_STATE, def);
+ String prefsBugreport = context.getResources().getString(
+ com.android.internal.R.string.prefs_bugreport);
+ String keyWarningState = context.getResources().getString(
+ com.android.internal.R.string.key_warning_state);
+ final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport,
+ Context.MODE_PRIVATE);
+ return prefs.getInt(keyWarningState, def);
}
static void setWarningState(Context context, int value) {
- final SharedPreferences prefs = context.getSharedPreferences(
- PREFS_BUGREPORT, Context.MODE_PRIVATE);
- prefs.edit().putInt(KEY_WARNING_STATE, value).apply();
+ String prefsBugreport = context.getResources().getString(
+ com.android.internal.R.string.prefs_bugreport);
+ String keyWarningState = context.getResources().getString(
+ com.android.internal.R.string.key_warning_state);
+ final SharedPreferences prefs = context.getSharedPreferences(prefsBugreport,
+ Context.MODE_PRIVATE);
+ prefs.edit().putInt(keyWarningState, value).apply();
}
}
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 61f49db07abc..fb0678fedb56 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -21,8 +21,6 @@ import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_TELEVISION;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
-import static com.android.shell.BugreportPrefs.STATE_HIDE;
-import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
import static com.android.shell.BugreportPrefs.getWarningState;
import static com.android.shell.flags.Flags.handleBugreportsForWear;
@@ -1347,7 +1345,11 @@ public class BugreportProgressService extends Service {
}
private boolean hasUserDecidedNotToGetWarningMessage() {
- return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE;
+ int bugreportStateUnknown = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_unknown);
+ int bugreportStateHide = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_hide);
+ return getWarningState(mContext, bugreportStateUnknown) == bugreportStateHide;
}
private void maybeShowWarningMessageAndCloseNotification(int id) {
diff --git a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
index a44e23603f52..0e835f91aca6 100644
--- a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
+++ b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
@@ -16,9 +16,6 @@
package com.android.shell;
-import static com.android.shell.BugreportPrefs.STATE_HIDE;
-import static com.android.shell.BugreportPrefs.STATE_SHOW;
-import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
import static com.android.shell.BugreportPrefs.getWarningState;
import static com.android.shell.BugreportPrefs.setWarningState;
import static com.android.shell.BugreportProgressService.sendShareIntent;
@@ -69,12 +66,19 @@ public class BugreportWarningActivity extends AlertActivity
mConfirmRepeat = (CheckBox) ap.mView.findViewById(android.R.id.checkbox);
- final int state = getWarningState(this, STATE_UNKNOWN);
+ int bugreportStateUnknown = getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_unknown);
+ int bugreportStateHide = getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_hide);
+ int bugreportStateShow = getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_show);
+
+ final int state = getWarningState(this, bugreportStateUnknown);
final boolean checked;
if (Build.IS_USER) {
- checked = state == STATE_HIDE; // Only checks if specifically set to.
+ checked = state == bugreportStateHide; // Only checks if specifically set to.
} else {
- checked = state != STATE_SHOW; // Checks by default.
+ checked = state != bugreportStateShow; // Checks by default.
}
mConfirmRepeat.setChecked(checked);
@@ -83,9 +87,14 @@ public class BugreportWarningActivity extends AlertActivity
@Override
public void onClick(DialogInterface dialog, int which) {
+ int bugreportStateHide = getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_hide);
+ int bugreportStateShow = getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_show);
if (which == AlertDialog.BUTTON_POSITIVE) {
// Remember confirm state, and launch target
- setWarningState(this, mConfirmRepeat.isChecked() ? STATE_HIDE : STATE_SHOW);
+ setWarningState(this, mConfirmRepeat.isChecked() ? bugreportStateHide
+ : bugreportStateShow);
if (mSendIntent != null) {
sendShareIntent(this, mSendIntent);
}
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index 7bda2ea790b0..2d6abe6cdc93 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -19,10 +19,6 @@ package com.android.shell;
import static android.test.MoreAsserts.assertContainsRegex;
import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME;
-import static com.android.shell.BugreportPrefs.PREFS_BUGREPORT;
-import static com.android.shell.BugreportPrefs.STATE_HIDE;
-import static com.android.shell.BugreportPrefs.STATE_SHOW;
-import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
import static com.android.shell.BugreportPrefs.getWarningState;
import static com.android.shell.BugreportPrefs.setWarningState;
import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_REQUESTED;
@@ -201,8 +197,9 @@ public class BugreportReceiverTest {
return null;
}).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
any(), anyBoolean(), anyBoolean());
-
- setWarningState(mContext, STATE_HIDE);
+ int bugreportStateHide = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_hide);
+ setWarningState(mContext, bugreportStateHide);
mUiBot.turnScreenOn();
}
@@ -469,22 +466,31 @@ public class BugreportReceiverTest {
@Test
public void testBugreportFinished_withWarningUnknownState() throws Exception {
- bugreportFinishedWithWarningTest(STATE_UNKNOWN);
+ int bugreportStateUnknown = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_unknown);
+ bugreportFinishedWithWarningTest(bugreportStateUnknown);
}
@Test
public void testBugreportFinished_withWarningShowAgain() throws Exception {
- bugreportFinishedWithWarningTest(STATE_SHOW);
+ int bugreportStateShow = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_show);
+ bugreportFinishedWithWarningTest(bugreportStateShow);
}
private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception {
+ int bugreportStateUnknown = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_unknown);
+ int bugreportStateHide = mContext.getResources().getInteger(
+ com.android.internal.R.integer.bugreport_state_hide);
if (propertyState == null) {
// Clear properties
- mContext.getSharedPreferences(PREFS_BUGREPORT, Context.MODE_PRIVATE)
- .edit().clear().commit();
+ mContext.getSharedPreferences(
+ mContext.getResources().getString(com.android.internal.R.string.prefs_bugreport)
+ , Context.MODE_PRIVATE).edit().clear().commit();
// Confidence check...
- assertEquals("Did not reset properties", STATE_UNKNOWN,
- getWarningState(mContext, STATE_UNKNOWN));
+ assertEquals("Did not reset properties", bugreportStateUnknown,
+ getWarningState(mContext, bugreportStateUnknown));
} else {
setWarningState(mContext, propertyState);
}
@@ -501,7 +507,8 @@ public class BugreportReceiverTest {
// TODO: get ok and dontShowAgain from the dialog reference above
UiObject dontShowAgain =
mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat));
- final boolean firstTime = propertyState == null || propertyState == STATE_UNKNOWN;
+ final boolean firstTime =
+ propertyState == null || propertyState == bugreportStateUnknown;
if (firstTime) {
if (Build.IS_USER) {
assertFalse("Checkbox should NOT be checked by default on user builds",
@@ -524,8 +531,8 @@ public class BugreportReceiverTest {
assertActionSendMultiple(extras);
// Make sure it's hidden now.
- int newState = getWarningState(mContext, STATE_UNKNOWN);
- assertEquals("Didn't change state", STATE_HIDE, newState);
+ int newState = getWarningState(mContext, bugreportStateUnknown);
+ assertEquals("Didn't change state", bugreportStateHide, newState);
}
@Test
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 744388f47d0e..19806e7cdf64 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -207,6 +207,8 @@ filegroup {
"tests/src/**/systemui/statusbar/notification/row/NotificationConversationInfoTest.java",
"tests/src/**/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt",
"tests/src/**/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt",
+ "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java",
+ "tests/src/**/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierDisabledTest.java",
"tests/src/**/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java",
"tests/src/**/systemui/statusbar/phone/CentralSurfacesImplTest.java",
"tests/src/**/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java",
@@ -538,6 +540,7 @@ android_library {
kotlincflags: [
"-Xjvm-default=all",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
],
plugins: [
@@ -552,6 +555,11 @@ android_library {
},
}
+platform_compat_config {
+ name: "SystemUI-core-compat-config",
+ src: ":SystemUI-core",
+}
+
filegroup {
name: "AAA-src",
srcs: ["tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java"],
@@ -754,6 +762,7 @@ android_library {
"kosmos",
"testables",
"androidx.test.rules",
+ "platform-compat-test-rules",
],
libs: [
"android.test.runner.stubs.system",
@@ -888,6 +897,7 @@ android_robolectric_test {
static_libs: [
"RoboTestLibraries",
"androidx.compose.runtime_runtime",
+ "platform-compat-test-rules",
],
libs: [
"android.test.runner.impl",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 5b989cb6abc4..910f71276376 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1869,20 +1869,6 @@ flag {
bug: "385194612"
}
-flag{
- name: "gsf_bouncer"
- namespace: "systemui"
- description: "Applies GSF font styles to Bouncer surfaces."
- bug: "379364381"
-}
-
-flag {
- name: "gsf_quick_settings"
- namespace: "systemui"
- description: "Applies GSF font styles to Quick Settings surfaces."
- bug: "379364381"
-}
-
flag {
name: "spatial_model_launcher_pushback"
namespace: "systemui"
@@ -1960,6 +1946,16 @@ flag {
}
flag {
+ name: "unfold_latency_tracking_fix"
+ namespace: "systemui"
+ description: "New implementation to track unfold latency that excludes broken cases"
+ bug: "390649568"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "ui_rich_ongoing_force_expanded"
namespace: "systemui"
description: "Force promoted notifications to always be expanded"
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index c63c2b48638c..9c6bb2c8f778 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -42,6 +42,9 @@ android_library {
"//frameworks/libs/systemui:tracinglib-platform",
],
- kotlincflags: ["-Xjvm-default=all"],
+ kotlincflags: [
+ "-Xjvm-default=all",
+ "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
+ ],
use_resource_processor: true,
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 4a4607b6e8fc..2ca70558f18b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -207,12 +207,7 @@ fun CommunalContainer(
Box(modifier = Modifier.fillMaxSize())
}
- scene(
- CommunalScenes.Communal,
- userActions =
- if (viewModel.v2FlagEnabled()) emptyMap()
- else mapOf(Swipe.End to CommunalScenes.Blank),
- ) {
+ scene(CommunalScenes.Communal, userActions = mapOf(Swipe.End to CommunalScenes.Blank)) {
CommunalScene(
backgroundType = backgroundType,
colors = colors,
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 3c0480d150e0..9c57efc24a22 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
@@ -1724,7 +1724,7 @@ private fun UmoLegacy(viewModel: BaseCommunalViewModel, modifier: Modifier = Mod
modifier
.clip(
shape =
- RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+ RoundedCornerShape(dimensionResource(R.dimen.notification_corner_radius))
)
.background(MaterialTheme.colorScheme.primary)
.pointerInput(Unit) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
index ba25719f1d60..0abed39dce6b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
@@ -26,18 +26,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.plugins.clocks.ClockController
import kotlin.math.min
import kotlin.math.roundToInt
/** Produces a [BurnInState] that can be used to query the `LockscreenBurnInViewModel` flows. */
@Composable
-fun rememberBurnIn(
- clockInteractor: KeyguardClockInteractor,
-): BurnInState {
- val clock by clockInteractor.currentClock.collectAsStateWithLifecycle()
+fun rememberBurnIn(clockViewModel: KeyguardClockViewModel): BurnInState {
+ val clock by clockViewModel.currentClock.collectAsStateWithLifecycle()
val (smartspaceTop, onSmartspaceTopChanged) = remember { mutableStateOf<Float?>(null) }
val (smallClockTop, onSmallClockTopChanged) = remember { mutableStateOf<Float?>(null) }
@@ -62,18 +60,12 @@ fun rememberBurnIn(
}
@Composable
-private fun rememberBurnInParameters(
- clock: ClockController?,
- topmostTop: Int,
-): BurnInParameters {
+private fun rememberBurnInParameters(clock: ClockController?, topmostTop: Int): BurnInParameters {
val density = LocalDensity.current
val topInset = WindowInsets.systemBars.union(WindowInsets.displayCutout).getTop(density)
return remember(clock, topInset, topmostTop) {
- BurnInParameters(
- topInset = topInset,
- minViewY = topmostTop,
- )
+ BurnInParameters(topInset = topInset, minViewY = topmostTop)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index abf7fdc05f2e..f51049a10569 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -38,11 +38,11 @@ import com.android.compose.animation.scene.ContentScope
import com.android.compose.modifiers.thenIf
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack
@@ -89,7 +89,7 @@ constructor(
private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
private val aodPromotedNotificationViewModelFactory: AODPromotedNotificationViewModel.Factory,
private val systemBarUtilsState: SystemBarUtilsState,
- private val clockInteractor: KeyguardClockInteractor,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) {
init {
@@ -118,7 +118,7 @@ constructor(
val isVisible by
keyguardRootViewModel.isAodPromotedNotifVisible.collectAsStateWithLifecycle()
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
AnimatedVisibility(
visible = isVisible,
@@ -141,7 +141,7 @@ constructor(
isVisible.stopAnimating()
}
}
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
AnimatedVisibility(
visibleState = transitionState,
enter = fadeIn(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 410499a3c23f..6293fc26f96a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -37,7 +37,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.modifiers.thenIf
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene
@@ -56,7 +55,7 @@ constructor(
private val mediaCarouselSection: MediaCarouselSection,
private val clockSection: DefaultClockSection,
private val weatherClockSection: WeatherClockSection,
- private val clockInteractor: KeyguardClockInteractor,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) {
@Composable
fun ContentScope.DefaultClockLayout(
@@ -138,7 +137,7 @@ constructor(
smartSpacePaddingTop: (Resources) -> Int,
modifier: Modifier = Modifier,
) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
Column(modifier = modifier) {
with(clockSection) {
@@ -163,7 +162,7 @@ constructor(
smartSpacePaddingTop: (Resources) -> Int,
shouldOffSetClockToOneHalf: Boolean = false,
) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle()
LaunchedEffect(isLargeClockVisible) {
@@ -204,7 +203,7 @@ constructor(
smartSpacePaddingTop: (Resources) -> Int,
modifier: Modifier = Modifier,
) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle()
val currentClockState = clockViewModel.currentClock.collectAsStateWithLifecycle()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index d7d4e1714aa6..09b8d178cc8e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -175,7 +175,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace(
viewModel: NotificationsPlaceholderViewModel,
) {
- val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
+ val isSnoozable by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
var scrollOffset by remember { mutableFloatStateOf(0f) }
val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
@@ -192,7 +192,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace(
)
}
- val nestedScrollConnection =
+ val snoozeScrollConnection =
object : NestedScrollConnection {
override suspend fun onPreFling(available: Velocity): Velocity {
if (
@@ -206,7 +206,7 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace(
}
}
- LaunchedEffect(isHeadsUp) { scrollOffset = 0f }
+ LaunchedEffect(isSnoozable) { scrollOffset = 0f }
LaunchedEffect(scrollableState.isScrollInProgress) {
if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) {
@@ -230,10 +230,8 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace(
),
)
}
- .thenIf(isHeadsUp) {
- Modifier.nestedScroll(nestedScrollConnection)
- .scrollable(orientation = Orientation.Vertical, state = scrollableState)
- },
+ .thenIf(isSnoozable) { Modifier.nestedScroll(snoozeScrollConnection) }
+ .scrollable(orientation = Orientation.Vertical, state = scrollableState),
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index 7c50d6f8af12..64f3cb13662a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -30,9 +30,9 @@ import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
@@ -57,7 +57,7 @@ constructor(
private val shadeSession: SaveableSession,
private val stackScrollView: Lazy<NotificationScrollView>,
private val clockSection: DefaultClockSection,
- private val clockInteractor: KeyguardClockInteractor,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) : Overlay {
override val key = Overlays.NotificationsShade
@@ -86,7 +86,7 @@ constructor(
OverlayShade(
panelElement = NotificationsShade.Elements.Panel,
- panelAlignment = Alignment.TopStart,
+ alignmentOnWideScreens = Alignment.TopStart,
modifier = modifier,
onScrimClicked = viewModel::onScrimClicked,
header = {
@@ -105,7 +105,7 @@ constructor(
Box {
Column {
if (viewModel.showClock) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
with(clockSection) {
SmallClock(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index cc58b8e13744..afdb3cbba60e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -128,7 +128,7 @@ constructor(
)
OverlayShade(
panelElement = QuickSettingsShade.Elements.Panel,
- panelAlignment = Alignment.TopEnd,
+ alignmentOnWideScreens = Alignment.TopEnd,
onScrimClicked = contentViewModel::onScrimClicked,
header = {
OverlayShadeHeader(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index da4e5824eb3e..aa0d474ba41c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -46,6 +46,8 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
+import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
import com.android.systemui.lifecycle.rememberActivated
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.qs.ui.composable.QuickSettingsTheme
@@ -202,7 +204,7 @@ fun SceneContainer(
SceneTransitionLayout(
state = state,
modifier = modifier.fillMaxSize(),
- swipeSourceDetector = viewModel.edgeDetector,
+ swipeSourceDetector = viewModel.swipeSourceDetector,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
@@ -239,7 +241,12 @@ fun SceneContainer(
BottomRightCornerRibbon(
content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
colorSaturation = { viewModel.ribbonColorSaturation },
- modifier = Modifier.align(Alignment.BottomEnd),
+ modifier =
+ Modifier.align(Alignment.BottomEnd)
+ .burnInAware(
+ viewModel = viewModel.burnIn,
+ params = rememberBurnIn(viewModel.clock).parameters,
+ ),
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 5dcec5b8836d..cdb1e2e53b09 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -59,7 +59,7 @@ import com.android.systemui.res.R
@Composable
fun ContentScope.OverlayShade(
panelElement: ElementKey,
- panelAlignment: Alignment,
+ alignmentOnWideScreens: Alignment,
onScrimClicked: () -> Unit,
modifier: Modifier = Modifier,
header: @Composable () -> Unit,
@@ -71,7 +71,7 @@ fun ContentScope.OverlayShade(
Box(
modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth),
- contentAlignment = panelAlignment,
+ contentAlignment = if (isFullWidth) Alignment.TopCenter else alignmentOnWideScreens,
) {
Panel(
modifier =
diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp
index 090e9ccedda0..42dd85a3d0a7 100644
--- a/packages/SystemUI/compose/scene/Android.bp
+++ b/packages/SystemUI/compose/scene/Android.bp
@@ -45,6 +45,9 @@ android_library {
"mechanics",
],
- kotlincflags: ["-Xjvm-default=all"],
+ kotlincflags: [
+ "-Xjvm-default=all",
+ "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
+ ],
use_resource_processor: true,
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt
index cade9bff5abb..ad2ddfe2b2a4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt
@@ -16,9 +16,12 @@
package com.android.compose.animation.scene.testing
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.Element.Companion.AlphaUnspecified
+import com.android.compose.animation.scene.Element.Companion.SizeUnspecified
import com.android.compose.animation.scene.ElementModifier
import com.android.compose.animation.scene.Scale
@@ -28,6 +31,12 @@ val SemanticsNode.lastAlphaForTesting: Float?
val SemanticsNode.lastScaleForTesting: Scale?
get() = elementState.lastScale.takeIf { it != Scale.Unspecified }
+val SemanticsNode.lastOffsetForTesting: Offset?
+ get() = elementState.lastOffset.takeIf { it != Offset.Unspecified }
+
+val SemanticsNode.lastSizeForTesting: IntSize?
+ get() = elementState.lastSize.takeIf { it != SizeUnspecified }
+
private val SemanticsNode.elementState: Element.State
get() {
val elementModifier =
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt
new file mode 100644
index 000000000000..7be7fa17eeea
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import org.json.JSONObject
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.UnknownTypeException
+
+fun Scale.asDataPoint() = DataPointTypes.scale.makeDataPoint(this)
+
+object DataPointTypes {
+ val scale: DataPointType<Scale> =
+ DataPointType(
+ "scale",
+ jsonToValue = {
+ when (it) {
+ "unspecified" -> Scale.Unspecified
+ "default" -> Scale.Default
+ "zero" -> Scale.Zero
+ is JSONObject -> {
+ val pivot = it.get("pivot")
+ Scale(
+ scaleX = it.getDouble("x").toFloat(),
+ scaleY = it.getDouble("y").toFloat(),
+ pivot =
+ when (pivot) {
+ "unspecified" -> Offset.Unspecified
+ "infinite" -> Offset.Infinite
+ is JSONObject ->
+ Offset(
+ pivot.getDouble("x").toFloat(),
+ pivot.getDouble("y").toFloat(),
+ )
+ else -> throw UnknownTypeException()
+ },
+ )
+ }
+ else -> throw UnknownTypeException()
+ }
+ },
+ valueToJson = {
+ when (it) {
+ Scale.Unspecified -> "unspecified"
+ Scale.Default -> "default"
+ Scale.Zero -> "zero"
+ else -> {
+ JSONObject().apply {
+ put("x", it.scaleX)
+ put("y", it.scaleY)
+ put(
+ "pivot",
+ when {
+ it.pivot.isUnspecified -> "unspecified"
+ !it.pivot.isFinite -> "infinite"
+ else ->
+ JSONObject().apply {
+ put("x", it.pivot.x)
+ put("y", it.pivot.y)
+ }
+ },
+ )
+ }
+ }
+ }
+ },
+ )
+}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt
new file mode 100644
index 000000000000..8658bbf1da56
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.DataPointTypes.scale
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastOffsetForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.animation.scene.testing.lastSizeForTesting
+import platform.test.motion.compose.DataPointTypes.intSize
+import platform.test.motion.compose.DataPointTypes.offset
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+
+/**
+ * [FeatureCapture] implementations to record animated state of [SceneTransitionLayout] [Element].
+ */
+object FeatureCaptures {
+
+ val elementAlpha =
+ FeatureCapture<SemanticsNode, Float>("alpha") {
+ DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+ }
+
+ val elementScale =
+ FeatureCapture<SemanticsNode, Scale>("scale") {
+ DataPoint.of(it.lastScaleForTesting, scale)
+ }
+
+ val elementOffset =
+ FeatureCapture<SemanticsNode, Offset>("offset") {
+ DataPoint.of(it.lastOffsetForTesting, offset)
+ }
+
+ val elementSize =
+ FeatureCapture<SemanticsNode, IntSize>("size") {
+ DataPoint.of(it.lastSizeForTesting, intSize)
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 2e5b5b56c982..aad1276d76e5 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -113,8 +113,8 @@ class DefaultClockProvider(
companion object {
// 750ms @ 120hz -> 90 frames of animation
- // In practice, 45 looks good enough
- const val NUM_CLOCK_FONT_ANIMATION_STEPS = 45
+ // In practice, 30 looks good enough and limits our memory usage
+ const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
val FLEX_TYPEFACE by lazy {
// TODO(b/364680873): Move constant to config_clockFontFamily when shipping
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index bd33e52689c2..f53f964cd3d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -64,12 +64,12 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-import java.util.Optional;
-
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+import java.util.List;
+import java.util.Optional;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@@ -171,6 +171,7 @@ public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
mActivityStarter,
mKeyguardInteractor,
mSceneInteractor,
+ mKosmos.getShadeRepository(),
Optional.of(() -> mWindowRootView));
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 494e0b4deef4..dd43d817cccc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -74,12 +74,12 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-import java.util.Optional;
-
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+import java.util.List;
+import java.util.Optional;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@@ -187,6 +187,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
mActivityStarter,
mKeyguardInteractor,
mSceneInteractor,
+ mKosmos.getShadeRepository(),
Optional.of(() -> mWindowRootView)
);
@@ -627,6 +628,22 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
onRemovedCallbackCaptor.getValue().onRemoved();
}
+ @Test
+ public void testTouchSessionStart_notifiesShadeOfUserInteraction() {
+ mTouchHandler.onSessionStart(mTouchSession);
+
+ mKosmos.getTestScope().getTestScheduler().runCurrent();
+ assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isTrue();
+
+ ArgumentCaptor<TouchHandler.TouchSession.Callback> onRemovedCallbackCaptor =
+ ArgumentCaptor.forClass(TouchHandler.TouchSession.Callback.class);
+ verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture());
+ onRemovedCallbackCaptor.getValue().onRemoved();
+
+ mKosmos.getTestScope().getTestScheduler().runCurrent();
+ assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isFalse();
+ }
+
private void swipeToPosition(float percent, float velocityY) {
Mockito.clearInvocations(mTouchSession);
mTouchHandler.onSessionStart(mTouchSession);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index d8a9719d2058..dda460a6198f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -30,22 +30,17 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.isFinite
-import androidx.compose.ui.geometry.isUnspecified
-import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.FeatureCaptures.elementAlpha
+import com.android.compose.animation.scene.FeatureCaptures.elementScale
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.isElement
-import com.android.compose.animation.scene.testing.lastAlphaForTesting
-import com.android.compose.animation.scene.testing.lastScaleForTesting
import com.android.compose.theme.PlatformTheme
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
@@ -71,12 +66,12 @@ import com.android.systemui.scene.ui.composable.Scene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.view.sceneJankMonitorFactory
import com.android.systemui.testKosmos
+import kotlin.test.Ignore
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
-import org.json.JSONObject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -89,14 +84,8 @@ import platform.test.motion.compose.MotionControl
import platform.test.motion.compose.feature
import platform.test.motion.compose.recordMotion
import platform.test.motion.compose.runTest
-import platform.test.motion.golden.DataPoint
-import platform.test.motion.golden.DataPointType
-import platform.test.motion.golden.DataPointTypes
-import platform.test.motion.golden.FeatureCapture
-import platform.test.motion.golden.UnknownTypeException
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.Displays.Phone
-import kotlin.test.Ignore
/** MotionTest for the Bouncer Predictive Back animation */
@LargeTest
@@ -280,72 +269,4 @@ class BouncerPredictiveBackTest : SysuiTestCase() {
override suspend fun onActivated() = awaitCancellation()
}
-
- companion object {
- private val elementAlpha =
- FeatureCapture<SemanticsNode, Float>("alpha") {
- DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
- }
-
- private val elementScale =
- FeatureCapture<SemanticsNode, Scale>("scale") {
- DataPoint.of(it.lastScaleForTesting, scale)
- }
-
- private val scale: DataPointType<Scale> =
- DataPointType(
- "scale",
- jsonToValue = {
- when (it) {
- "unspecified" -> Scale.Unspecified
- "default" -> Scale.Default
- "zero" -> Scale.Zero
- is JSONObject -> {
- val pivot = it.get("pivot")
- Scale(
- scaleX = it.getDouble("x").toFloat(),
- scaleY = it.getDouble("y").toFloat(),
- pivot =
- when (pivot) {
- "unspecified" -> Offset.Unspecified
- "infinite" -> Offset.Infinite
- is JSONObject ->
- Offset(
- pivot.getDouble("x").toFloat(),
- pivot.getDouble("y").toFloat(),
- )
- else -> throw UnknownTypeException()
- },
- )
- }
- else -> throw UnknownTypeException()
- }
- },
- valueToJson = {
- when (it) {
- Scale.Unspecified -> "unspecified"
- Scale.Default -> "default"
- Scale.Zero -> "zero"
- else -> {
- JSONObject().apply {
- put("x", it.scaleX)
- put("y", it.scaleY)
- put(
- "pivot",
- when {
- it.pivot.isUnspecified -> "unspecified"
- !it.pivot.isFinite -> "infinite"
- else ->
- JSONObject().apply {
- put("x", it.pivot.x)
- put("y", it.pivot.y)
- }
- },
- )
- }
- }
- }
- },
- )
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt
new file mode 100644
index 000000000000..0c97750ba281
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.communal
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP
+import com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.wakefulnessLifecycle
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceInactiveConditionTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().useUnconfinedTestDispatcher().also {
+ whenever(it.wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_AWAKE
+ }
+
+ private val Kosmos.underTest by
+ Kosmos.Fixture {
+ DeviceInactiveCondition(
+ applicationCoroutineScope,
+ keyguardStateController,
+ wakefulnessLifecycle,
+ keyguardUpdateMonitor,
+ keyguardInteractor,
+ JavaAdapter(applicationCoroutineScope),
+ )
+ }
+
+ @Test
+ fun asleep_conditionTrue() =
+ kosmos.runTest {
+ // Condition is false to start.
+ underTest.start()
+ assertThat(underTest.isConditionMet).isFalse()
+
+ // Condition is true when device goes to sleep.
+ sleep()
+ assertThat(underTest.isConditionMet).isTrue()
+ }
+
+ @Test
+ fun dozingAndAsleep_conditionFalse() =
+ kosmos.runTest {
+ // Condition is true when device is asleep.
+ underTest.start()
+ sleep()
+ assertThat(underTest.isConditionMet).isTrue()
+
+ // Condition turns false after doze starts.
+ fakeKeyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.UNINITIALIZED, to = DozeStateModel.DOZE)
+ )
+ assertThat(underTest.isConditionMet).isFalse()
+ }
+
+ fun Kosmos.sleep() {
+ whenever(wakefulnessLifecycle.wakefulness) doReturn WAKEFULNESS_ASLEEP
+ argumentCaptor<WakefulnessLifecycle.Observer>().apply {
+ verify(wakefulnessLifecycle).addObserver(capture())
+ firstValue.onStartedGoingToSleep()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
index 3eb08004ae61..f063655b9f86 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
@@ -18,7 +18,7 @@ package com.android.systemui.communal.data.db
import android.content.ComponentName
import android.os.UserHandle
-import android.os.UserManager
+import android.os.userManager
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,10 +27,13 @@ import com.android.systemui.communal.data.db.DefaultWidgetPopulation.SkipReason.
import com.android.systemui.communal.shared.model.SpanValue
import com.android.systemui.communal.widgets.CommunalWidgetHost
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
-import kotlinx.coroutines.test.runCurrent
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.userLockedInteractor
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -38,6 +41,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -46,8 +50,7 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
class DefaultWidgetPopulationTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val communalWidgetHost =
mock<CommunalWidgetHost> {
@@ -57,11 +60,6 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
private val communalWidgetDao = mock<CommunalWidgetDao>()
private val database = mock<SupportSQLiteDatabase>()
private val mainUser = UserHandle(0)
- private val userManager =
- mock<UserManager> {
- on { mainUser }.thenReturn(mainUser)
- on { getUserSerialNumber(0) }.thenReturn(0)
- }
private val defaultWidgets =
arrayOf(
@@ -74,6 +72,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
@Before
fun setUp() {
+ kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
underTest =
DefaultWidgetPopulation(
bgScope = kosmos.applicationCoroutineScope,
@@ -81,32 +80,45 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
communalWidgetDaoProvider = { communalWidgetDao },
defaultWidgets = defaultWidgets,
logBuffer = logcatLogBuffer("DefaultWidgetPopulationTest"),
- userManager = userManager,
+ userManager = kosmos.userManager,
+ userLockedInteractor = kosmos.userLockedInteractor,
)
}
@Test
+ fun testNoInteractionUntilMainUserUnlocked() =
+ kosmos.runTest {
+ kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, false)
+ // Database created
+ underTest.onCreate(database)
+ verify(communalWidgetHost, never())
+ .allocateIdAndBindWidget(provider = any(), user = any())
+ kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
+ verify(communalWidgetHost, atLeastOnce())
+ .allocateIdAndBindWidget(provider = any(), user = any())
+ }
+
+ @Test
fun testPopulateDefaultWidgetsWhenDatabaseCreated() =
- testScope.runTest {
+ kosmos.runTest {
// Database created
underTest.onCreate(database)
- runCurrent()
// Verify default widgets bound
verify(communalWidgetHost)
.allocateIdAndBindWidget(
provider = eq(ComponentName.unflattenFromString(defaultWidgets[0])!!),
- user = eq(mainUser),
+ user = eq(UserHandle(MAIN_USER_ID)),
)
verify(communalWidgetHost)
.allocateIdAndBindWidget(
provider = eq(ComponentName.unflattenFromString(defaultWidgets[1])!!),
- user = eq(mainUser),
+ user = eq(UserHandle(MAIN_USER_ID)),
)
verify(communalWidgetHost)
.allocateIdAndBindWidget(
provider = eq(ComponentName.unflattenFromString(defaultWidgets[2])!!),
- user = eq(mainUser),
+ user = eq(UserHandle(MAIN_USER_ID)),
)
// Verify default widgets added in database
@@ -138,13 +150,12 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
@Test
fun testSkipDefaultWidgetsPopulation() =
- testScope.runTest {
+ kosmos.runTest {
// Skip default widgets population
underTest.skipDefaultWidgetsPopulation(RESTORED_FROM_BACKUP)
// Database created
underTest.onCreate(database)
- runCurrent()
// Verify no widget bounded or added to the database
verify(communalWidgetHost, never()).allocateIdAndBindWidget(any(), any())
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 c3cc3e66f81f..8424746f3db5 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
@@ -75,6 +75,7 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.statusbar.phone.fakeManagedProfileController
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -163,12 +164,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun isCommunalAvailable_storageUnlockedAndMainUser_true() =
+ fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() =
kosmos.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -176,12 +177,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun isCommunalAvailable_storageLockedAndMainUser_false() =
+ fun isCommunalAvailable_mainUserLockedAndMainUser_false() =
kosmos.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(true)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -189,12 +190,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() =
+ fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() =
kosmos.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(secondaryUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -207,7 +208,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -222,7 +223,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -1282,7 +1283,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun showCommunalWhileCharging() =
kosmos.runTest {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
fakeSettings.putIntForUser(
@@ -1302,7 +1303,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun showCommunalWhilePosturedAndCharging() =
kosmos.runTest {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
fakeSettings.putIntForUser(
@@ -1323,7 +1324,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun showCommunalWhileDocked() =
kosmos.runTest {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
index c158baf5a80c..619995478ecc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
@@ -35,7 +35,6 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.shade.domain.interactor.enableDualShade
@@ -74,7 +73,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
@@ -83,7 +82,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
}
@@ -96,7 +95,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
@@ -106,7 +105,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
@@ -120,7 +119,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
@@ -130,7 +129,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
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 dbdd7fb2773a..85155157eda2 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
@@ -203,7 +203,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
// Keyguard showing, storage unlocked, main user, and tutorial not started.
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setKeyguardOccluded(false)
- keyguardRepository.setIsEncryptedOrLockdown(false)
+ userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
setIsMainUser(true)
tutorialRepository.setTutorialSettingState(
Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
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 95681941a1c1..c15f797aad5d 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
@@ -37,7 +37,9 @@ import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
@@ -91,6 +93,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
kosmos.testDispatcher,
{ widgetManager },
helper,
+ kosmos.userLockedInteractor,
)
}
@@ -269,6 +272,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
// Binding to the service does not require keyguard showing
setCommunalAvailable(true, setKeyguardShowing = false)
+ fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
runCurrent()
verify(widgetManager).register()
@@ -283,7 +287,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
setKeyguardShowing: Boolean = true,
) =
with(kosmos) {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
if (setKeyguardShowing) {
fakeKeyguardRepository.setKeyguardShowing(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
index 1c93b3c66e32..102ce0b51c94 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
@@ -23,7 +23,6 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
@@ -62,15 +61,14 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
@Before
fun setUp() {
- underTest = kosmos.dreamUserActionsViewModel
+ underTest = kosmos.dreamUserActionsViewModelFactory.create()
underTest.activateIn(testScope)
}
@Test
- fun actions_communalNotAvailable_singleShade() =
+ fun actions_singleShade() =
testScope.runTest {
kosmos.enableSingleShade()
- kosmos.setCommunalAvailable(false)
val actions by collectLastValue(underTest.actions)
@@ -93,10 +91,9 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun actions_communalNotAvailable_splitShade() =
+ fun actions_splitShade() =
testScope.runTest {
kosmos.enableSplitShade()
- kosmos.setCommunalAvailable(false)
val actions by collectLastValue(underTest.actions)
@@ -121,10 +118,9 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun actions_communalNotAvailable_dualShade() =
+ fun actions_dualShade() =
testScope.runTest {
kosmos.enableDualShade()
- kosmos.setCommunalAvailable(false)
val actions by collectLastValue(underTest.actions)
@@ -148,88 +144,6 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
assertThat(actions?.get(Swipe.End)).isNull()
}
- @Test
- fun actions_communalAvailable_singleShade() =
- testScope.runTest {
- kosmos.enableSingleShade()
- kosmos.setCommunalAvailable(true)
-
- val actions by collectLastValue(underTest.actions)
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
- assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
-
- setUpState(isShadeTouchable = false, isDeviceUnlocked = false)
- assertThat(actions).isEmpty()
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
- assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
- }
-
- @Test
- fun actions_communalAvailable_splitShade() =
- testScope.runTest {
- kosmos.enableSplitShade()
- kosmos.setCommunalAvailable(true)
-
- val actions by collectLastValue(underTest.actions)
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
-
- setUpState(isShadeTouchable = false, isDeviceUnlocked = false)
- assertThat(actions).isEmpty()
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
- }
-
- @Test
- fun actions_communalAvailable_dualShade() =
- testScope.runTest {
- kosmos.enableDualShade()
- kosmos.setCommunalAvailable(true)
-
- val actions by collectLastValue(underTest.actions)
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
-
- setUpState(isShadeTouchable = false, isDeviceUnlocked = false)
- assertThat(actions).isEmpty()
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
- }
-
private fun TestScope.setUpState(isShadeTouchable: Boolean, isDeviceUnlocked: Boolean) {
if (isShadeTouchable) {
kosmos.powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index e13f3f12c55a..7e93f5a8c9a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -20,19 +20,26 @@ import android.os.PowerManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
import android.service.dream.dreamManager
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
+import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy
import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
@@ -56,11 +63,14 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -93,7 +103,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_COMMUNAL_SCENE_KTF_REFACTOR,
+ FLAG_GLANCEABLE_HUB_V2,
+ )
}
}
@@ -107,6 +120,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
// Transition to DOZING and set the power interactor asleep.
kosmos.powerInteractor.setAsleepForTest()
+ kosmos.setCommunalV2ConfigEnabled(true)
runBlocking {
kosmos.transitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -160,7 +174,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
@Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
- @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ @DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR, FLAG_GLANCEABLE_HUB_V2)
fun testTransitionToLockscreen_onWake_canDream_glanceableHubAvailable() =
kosmos.runTest {
whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
@@ -179,7 +193,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() =
kosmos.runTest {
setCommunalAvailable(true)
- whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ if (glanceableHubV2()) {
+ val user = fakeUserRepository.asMainUser()
+ fakeSettings.putIntForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ 1,
+ user.id,
+ )
+ batteryRepository.fake.setDevicePluggedIn(true)
+ } else {
+ whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ }
clearInvocations(fakeCommunalSceneRepository)
powerInteractor.setAwakeForTest()
@@ -240,7 +264,17 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
fun testTransitionToGlanceableHub_onWakeup_ifAvailable() =
kosmos.runTest {
setCommunalAvailable(true)
- whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ if (glanceableHubV2()) {
+ val user = fakeUserRepository.asMainUser()
+ fakeSettings.putIntForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ 1,
+ user.id,
+ )
+ batteryRepository.fake.setDevicePluggedIn(true)
+ } else {
+ whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ }
// Device turns on.
powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index 8e1068226431..5882cff74eb6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -19,14 +19,20 @@ package com.android.systemui.keyguard.domain.interactor
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
import android.service.dream.dreamManager
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.Flags.FLAG_COMMUNAL_SCENE_KTF_REFACTOR
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
+import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.common.data.repository.batteryRepository
+import com.android.systemui.common.data.repository.fake
import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -46,6 +52,8 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceTimeBy
@@ -66,7 +74,10 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_COMMUNAL_SCENE_KTF_REFACTOR,
+ FLAG_GLANCEABLE_HUB_V2,
+ )
.andSceneContainer()
}
}
@@ -101,6 +112,7 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu
)
reset(kosmos.transitionRepository)
kosmos.setCommunalAvailable(true)
+ kosmos.setCommunalV2ConfigEnabled(true)
}
kosmos.underTest.start()
}
@@ -202,7 +214,17 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu
reset(transitionRepository)
setCommunalAvailable(true)
- whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ if (glanceableHubV2()) {
+ val user = fakeUserRepository.asMainUser()
+ fakeSettings.putIntForUser(
+ Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
+ 1,
+ user.id,
+ )
+ batteryRepository.fake.setDevicePluggedIn(true)
+ } else {
+ whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ }
// Device wakes up.
powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
index b66e2fe13e8a..47ca4b14a26f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
@@ -41,7 +41,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
@@ -275,20 +275,20 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
assertThat(downDestination?.transitionKey).isNull()
}
- val downFromTopRightDestination =
+ val downFromEndHalfDestination =
userActions?.get(
Swipe.Down(
- fromSource = SceneContainerEdge.TopRight,
+ fromSource = SceneContainerArea.EndHalf,
pointerCount = if (downWithTwoPointers) 2 else 1,
)
)
when {
- !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull()
- downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull()
+ !isShadeTouchable -> assertThat(downFromEndHalfDestination).isNull()
+ downWithTwoPointers -> assertThat(downFromEndHalfDestination).isNull()
else -> {
- assertThat(downFromTopRightDestination)
+ assertThat(downFromEndHalfDestination)
.isEqualTo(ShowOverlay(Overlays.QuickSettingsShade))
- assertThat(downFromTopRightDestination?.transitionKey).isNull()
+ assertThat(downFromEndHalfDestination?.transitionKey).isNull()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
index 69485e848a6a..b177e07d09b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,8 +30,9 @@ import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.pm.PackageManager;
-import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.dream.lowlight.LowLightDreamManager;
@@ -39,6 +40,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.shared.condition.Condition;
import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
import dagger.Lazy;
@@ -53,7 +56,8 @@ import org.mockito.MockitoAnnotations;
import java.util.Set;
@SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
+@TestableLooper.RunWithLooper()
public class LowLightMonitorTest extends SysuiTestCase {
@Mock
@@ -78,6 +82,8 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Mock
private ComponentName mDreamComponent;
+ FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock());
+
Condition mCondition = mock(Condition.class);
Set<Condition> mConditionSet = Set.of(mCondition);
@@ -91,12 +97,13 @@ public class LowLightMonitorTest extends SysuiTestCase {
when(mLazyConditions.get()).thenReturn(mConditionSet);
mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent,
- mPackageManager);
+ mPackageManager, mBackgroundExecutor);
}
@Test
public void testSetAmbientLowLightWhenInLowLight() {
mLowLightMonitor.onConditionsChanged(true);
+ mBackgroundExecutor.runAllReady();
// Verify setting low light when condition is true
verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
}
@@ -105,6 +112,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
public void testExitAmbientLowLightWhenNotInLowLight() {
mLowLightMonitor.onConditionsChanged(true);
mLowLightMonitor.onConditionsChanged(false);
+ mBackgroundExecutor.runAllReady();
// Verify ambient light toggles back to light mode regular
verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
}
@@ -112,6 +120,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Test
public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() {
mLowLightMonitor.onScreenTurnedOn();
+ mBackgroundExecutor.runAllReady();
// Verify subscribing to low light conditions monitor when screen turns on.
verify(mMonitor).addSubscription(any());
@@ -125,6 +134,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
// Verify removing subscription when screen turns off.
mLowLightMonitor.onScreenTurnedOff();
+ mBackgroundExecutor.runAllReady();
verify(mMonitor).removeSubscription(token);
}
@@ -135,6 +145,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
mLowLightMonitor.onScreenTurnedOn();
mLowLightMonitor.onScreenTurnedOn();
+ mBackgroundExecutor.runAllReady();
// Verify subscription is only added once.
verify(mMonitor, times(1)).addSubscription(any());
}
@@ -146,6 +157,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
mLowLightMonitor.onScreenTurnedOn();
mLowLightMonitor.onScreenTurnedOn();
+ mBackgroundExecutor.runAllReady();
Set<Condition> conditions = captureConditions();
// Verify Monitor is subscribed to the expected conditions
assertThat(conditions).isEqualTo(mConditionSet);
@@ -154,7 +166,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Test
public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() {
mLowLightMonitor.onScreenTurnedOff();
-
+ mBackgroundExecutor.runAllReady();
// Verify doesn't remove subscription since there is none.
verify(mMonitor, never()).removeSubscription(any());
}
@@ -163,6 +175,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
public void testSubscribeIfScreenIsOnWhenStarting() {
when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
mLowLightMonitor.start();
+ mBackgroundExecutor.runAllReady();
// Verify to add subscription on start if the screen state is on
verify(mMonitor, times(1)).addSubscription(any());
}
@@ -170,9 +183,11 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Test
public void testNoSubscribeIfDreamNotPresent() {
LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
- mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager);
+ mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager,
+ mBackgroundExecutor);
when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
lowLightMonitor.start();
+ mBackgroundExecutor.runAllReady();
verify(mScreenLifecycle, never()).addObserver(any());
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 8e8867b80dc2..847044aa405e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -166,7 +166,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
mContext.getText(R.string.media_output_dialog_pairing_new).toString());
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_bindGroup_withSessionName_verifyView() {
when(mMediaSwitchingController.getSelectedMediaDevice())
@@ -188,7 +188,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_bindGroup_noSessionName_verifyView() {
when(mMediaSwitchingController.getSelectedMediaDevice())
@@ -237,7 +237,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() {
when(mMediaSwitchingController.getSelectableMediaDevice())
@@ -818,7 +818,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() {
when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(
@@ -935,7 +935,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
.isEqualTo(R.drawable.media_output_icon_volume);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_verifySessionView() {
initializeSession();
@@ -956,7 +956,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_verifyCollapsedView() {
initializeSession();
@@ -970,7 +970,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_expandIconClicked_verifyInitialView() {
initializeSession();
@@ -993,7 +993,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() {
initializeSession();
@@ -1016,7 +1016,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void deviceCanNotBeDeselected_verifyView() {
List<MediaDevice> selectedDevices = new ArrayList<>();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
index 52b9e47e6d3d..52a0a5445002 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt
@@ -30,7 +30,7 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -71,13 +71,13 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun downFromTopRight_switchesToQuickSettingsShade() =
+ fun downFromTopEnd_switchesToQuickSettingsShade() =
testScope.runTest {
val actions by collectLastValue(underTest.actions)
underTest.activateIn(this)
val action =
- (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay)
+ (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay)
assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade)
val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
assertThat(overlaysToHide).isNotNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
index 264eda5a07eb..668c606677ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepositoryTest.kt
@@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import com.android.systemui.settings.userFileManager
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
@@ -76,11 +77,11 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
@Test
fun setLargeTilesSpecs_inSharedPreferences() {
val setA = setOf("tileA", "tileB")
- underTest.setLargeTilesSpecs(setA.toTileSpecs())
+ underTest.writeLargeTileSpecs(setA.toTileSpecs())
assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA)
val setB = setOf("tileA", "tileB")
- underTest.setLargeTilesSpecs(setB.toTileSpecs())
+ underTest.writeLargeTileSpecs(setB.toTileSpecs())
assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB)
}
@@ -92,12 +93,12 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
val setA = setOf("tileA", "tileB")
- underTest.setLargeTilesSpecs(setA.toTileSpecs())
+ underTest.writeLargeTileSpecs(setA.toTileSpecs())
assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setA)
fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
val setB = setOf("tileA", "tileB")
- underTest.setLargeTilesSpecs(setB.toTileSpecs())
+ underTest.writeLargeTileSpecs(setB.toTileSpecs())
assertThat(getLargeTilesSpecsFromSharedPreferences()).isEqualTo(setB)
fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
@@ -106,7 +107,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
}
@Test
- fun setInitialTilesFromSettings_noLargeTiles_tilesSet() =
+ fun setUpgradePathFromSettings_noLargeTiles_tilesSet() =
with(kosmos) {
testScope.runTest {
val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -117,14 +118,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse()
- underTest.setInitialLargeTilesSpecs(tiles, PRIMARY_USER_ID)
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.ReadFromSettings(tiles),
+ PRIMARY_USER_ID,
+ )
assertThat(largeTiles).isEqualTo(tiles)
}
}
@Test
- fun setInitialTilesFromSettings_alreadyLargeTiles_tilesNotSet() =
+ fun setUpgradePathFromSettings_alreadyLargeTiles_tilesNotSet() =
with(kosmos) {
testScope.runTest {
val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -133,14 +137,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
setLargeTilesSpecsInSharedPreferences(setOf("tileC"))
- underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID)
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()),
+ ANOTHER_USER_ID,
+ )
assertThat(largeTiles).isEqualTo(setOf("tileC").toTileSpecs())
}
}
@Test
- fun setInitialTilesFromSettings_emptyLargeTiles_tilesNotSet() =
+ fun setUpgradePathFromSettings_emptyLargeTiles_tilesNotSet() =
with(kosmos) {
testScope.runTest {
val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -149,14 +156,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
fakeUserRepository.setSelectedUserInfo(ANOTHER_USER)
setLargeTilesSpecsInSharedPreferences(emptySet())
- underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID)
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()),
+ ANOTHER_USER_ID,
+ )
assertThat(largeTiles).isEmpty()
}
}
@Test
- fun setInitialTilesFromSettings_nonCurrentUser_tilesSetForCorrectUser() =
+ fun setUpgradePathFromSettings_nonCurrentUser_tilesSetForCorrectUser() =
with(kosmos) {
testScope.runTest {
val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -164,7 +174,10 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
fakeUserRepository.setUserInfos(USERS)
fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
- underTest.setInitialLargeTilesSpecs(setOf("tileA").toTileSpecs(), ANOTHER_USER_ID)
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.ReadFromSettings(setOf("tileA").toTileSpecs()),
+ ANOTHER_USER_ID,
+ )
assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
@@ -174,7 +187,7 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
}
@Test
- fun setInitialTiles_afterDefaultRead_noSetOnRepository_initialTilesCorrect() =
+ fun setUpgradePath_afterDefaultRead_noSetOnRepository_initialTilesCorrect() =
with(kosmos) {
testScope.runTest {
val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -186,14 +199,17 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
assertThat(currentLargeTiles).isNotEmpty()
val tiles = setOf("tileA", "tileB")
- underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID)
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()),
+ PRIMARY_USER_ID,
+ )
assertThat(largeTiles).isEqualTo(tiles.toTileSpecs())
}
}
@Test
- fun setInitialTiles_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() =
+ fun setUpgradePath_afterDefaultRead_largeTilesSetOnRepository_initialTilesCorrect() =
with(kosmos) {
testScope.runTest {
val largeTiles by collectLastValue(underTest.largeTilesSpecs)
@@ -204,15 +220,80 @@ class QSPreferencesRepositoryTest : SysuiTestCase() {
assertThat(currentLargeTiles).isNotEmpty()
- underTest.setLargeTilesSpecs(setOf(TileSpec.create("tileC")))
+ underTest.writeLargeTileSpecs(setOf(TileSpec.create("tileC")))
val tiles = setOf("tileA", "tileB")
- underTest.setInitialLargeTilesSpecs(tiles.toTileSpecs(), PRIMARY_USER_ID)
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.ReadFromSettings(tiles.toTileSpecs()),
+ PRIMARY_USER_ID,
+ )
assertThat(largeTiles).isEqualTo(setOf(TileSpec.create("tileC")))
}
}
+ @Test
+ fun setTilesRestored_noLargeTiles_tilesSet() =
+ with(kosmos) {
+ testScope.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+ fakeUserRepository.setUserInfos(USERS)
+ fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ val tiles = setOf("tileA", "tileB").toTileSpecs()
+
+ assertThat(getSharedPreferences().contains(LARGE_TILES_SPECS_KEY)).isFalse()
+
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.RestoreFromBackup(tiles),
+ PRIMARY_USER_ID,
+ )
+
+ assertThat(largeTiles).isEqualTo(tiles)
+ }
+ }
+
+ @Test
+ fun setDefaultTilesInitial_defaultSetLarge() =
+ with(kosmos) {
+ testScope.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+ fakeUserRepository.setUserInfos(USERS)
+ fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.DefaultSet,
+ PRIMARY_USER_ID,
+ )
+
+ assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
+ }
+ }
+
+ @Test
+ fun setTilesRestored_afterDefaultSet_tilesSet() =
+ with(kosmos) {
+ testScope.runTest {
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.DefaultSet,
+ PRIMARY_USER_ID,
+ )
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+ fakeUserRepository.setUserInfos(USERS)
+ fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ val tiles = setOf("tileA", "tileB").toTileSpecs()
+
+ underTest.setInitialOrUpgradeLargeTiles(
+ TilesUpgradePath.RestoreFromBackup(tiles),
+ PRIMARY_USER_ID,
+ )
+
+ assertThat(largeTiles).isEqualTo(tiles)
+ }
+ }
+
private fun getSharedPreferences(): SharedPreferences =
with(kosmos) {
return userFileManager.getSharedPreferences(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt
new file mode 100644
index 000000000000..f3c1f0c9dba8
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/LargeTilesUpgradePathsTest.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+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.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.common.shared.model.PackageChangeModel.Empty.packageName
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository
+import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor
+import com.android.systemui.qs.pipeline.data.repository.DefaultTilesQSHostRepository
+import com.android.systemui.qs.pipeline.data.repository.defaultTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
+import com.android.systemui.settings.userFileManager
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.userRepository
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LargeTilesUpgradePathsTest : SysuiTestCase() {
+
+ private val kosmos =
+ testKosmos().apply { defaultTilesRepository = DefaultTilesQSHostRepository(mainResources) }
+
+ private val defaultTiles = kosmos.defaultTilesRepository.defaultTiles.toSet()
+
+ private val underTest = kosmos.qsPreferencesInteractor
+
+ private val Kosmos.userId
+ get() = userRepository.getSelectedUserInfo().id
+
+ private val Kosmos.intent
+ get() =
+ Intent(ACTION_RESTORE_FINISHED).apply {
+ `package` = packageName
+ putExtra(Intent.EXTRA_USER_ID, kosmos.userId)
+ flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
+ }
+
+ /**
+ * This test corresponds to the case of a fresh start.
+ *
+ * The resulting large tiles are the default set of large tiles.
+ */
+ @Test
+ fun defaultTiles_noDataInSharedPreferences_defaultLargeTiles() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+ assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
+ }
+
+ /**
+ * This test corresponds to a user that upgraded in place from a build that didn't support large
+ * tiles to one that does. The current tiles of the user are read from settings.
+ *
+ * The resulting large tiles are those that were read from Settings.
+ */
+ @Test
+ fun upgradeInPlace_noDataInSharedPreferences_allLargeTiles() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ val tiles = setOf("a", "b", "c").toTileSpecs()
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.ReadFromSettings(tiles),
+ userId,
+ )
+
+ assertThat(largeTiles).isEqualTo(tiles)
+ }
+
+ /**
+ * This test corresponds to a fresh start, and then the user restarts the device, without ever
+ * having modified the set of large tiles.
+ *
+ * The resulting large tiles are the default large tiles that were set on the fresh start
+ */
+ @Test
+ fun defaultSet_restartDevice_largeTilesDontChange() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+ // User restarts the device, this will send a read from settings with the default
+ // set of tiles
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.ReadFromSettings(defaultTiles),
+ userId,
+ )
+
+ assertThat(largeTiles).isEqualTo(defaultLargeTilesRepository.defaultLargeTiles)
+ }
+
+ /**
+ * This test corresponds to a fresh start, following the user changing the sizes of some tiles.
+ * After that, the user restarts the device.
+ *
+ * The resulting set of large tiles are those that the user determined before restarting the
+ * device.
+ */
+ @Test
+ fun defaultSet_someSizeChanges_restart_correctSet() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+ underTest.setLargeTilesSpecs(largeTiles!! + setOf("a", "b").toTileSpecs())
+ val largeTilesBeforeRestart = largeTiles!!
+
+ // Restart
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.ReadFromSettings(defaultTiles),
+ userId,
+ )
+ assertThat(largeTiles).isEqualTo(largeTilesBeforeRestart)
+ }
+
+ /**
+ * This test corresponds to a user that upgraded, and after that performed some size changes.
+ * After that, the user restarts the device.
+ *
+ * The resulting set of large tiles are those that the user determined before restarting the
+ * device.
+ */
+ @Test
+ fun readFromSettings_changeSizes_restart_newLargeSet() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ val readTiles = setOf("a", "b", "c").toTileSpecs()
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.ReadFromSettings(readTiles),
+ userId,
+ )
+ underTest.setLargeTilesSpecs(emptySet())
+
+ assertThat(largeTiles).isEmpty()
+
+ // Restart
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.ReadFromSettings(readTiles),
+ userId,
+ )
+ assertThat(largeTiles).isEmpty()
+ }
+
+ /**
+ * This test corresponds to a user that upgraded from a build that didn't support tile sizes to
+ * one that does, via restore from backup. Note that there's no file in SharedPreferences to
+ * restore.
+ *
+ * The resulting set of large tiles are those that were restored from the backup.
+ */
+ @Test
+ fun restoreFromBackup_noDataInSharedPreferences_allLargeTiles() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ val tiles = setOf("a", "b", "c").toTileSpecs()
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.RestoreFromBackup(tiles),
+ userId,
+ )
+
+ assertThat(largeTiles).isEqualTo(tiles)
+ }
+
+ /**
+ * This test corresponds to a user that upgraded from a build that didn't support tile sizes to
+ * one that does, via restore from backup. However, the restore happens after SystemUI's
+ * initialization has set the tiles to default. Note that there's no file in SharedPreferences
+ * to restore.
+ *
+ * The resulting set of large tiles are those that were restored from the backup.
+ */
+ @Test
+ fun restoreFromBackup_afterDefault_noDataInSharedPreferences_allLargeTiles() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ underTest.setInitialOrUpgradeLargeTilesSpecs(TilesUpgradePath.DefaultSet, userId)
+
+ val tiles = setOf("a", "b", "c").toTileSpecs()
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.RestoreFromBackup(tiles),
+ userId,
+ )
+
+ assertThat(largeTiles).isEqualTo(tiles)
+ }
+
+ /**
+ * This test corresponds to a user that restored from a build that supported different sizes
+ * tiles. First the list of tiles is restored in Settings and then a file containing some large
+ * tiles overrides the current shared preferences file
+ *
+ * The resulting set of large tiles are those that were restored from the shared preferences
+ * backup (and not the full list).
+ */
+ @Test
+ fun restoreFromBackup_thenRestoreOfSharedPrefs_sharedPrefsAreLarge() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ val tiles = setOf("a", "b", "c").toTileSpecs()
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.RestoreFromBackup(tiles),
+ userId,
+ )
+
+ val tilesFromBackupOfSharedPrefs = setOf("a")
+ setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+
+ assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs())
+ }
+
+ /**
+ * This test corresponds to a user that restored from a build that supported different sizes
+ * tiles. However, this restore of settings happened after SystemUI's restore of the SharedPrefs
+ * containing the user's previous selections to large/small tiles.
+ *
+ * The resulting set of large tiles are those that were restored from the shared preferences
+ * backup (and not the full list).
+ */
+ @Test
+ fun restoreFromBackup_afterRestoreOfSharedPrefs_sharedPrefsAreLarge() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ val tiles = setOf("a", "b", "c").toTileSpecs()
+ val tilesFromBackupOfSharedPrefs = setOf("a")
+
+ setLargeTilesSpecsInSharedPreferences(tilesFromBackupOfSharedPrefs)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.RestoreFromBackup(tiles),
+ userId,
+ )
+
+ assertThat(largeTiles).isEqualTo(tilesFromBackupOfSharedPrefs.toTileSpecs())
+ }
+
+ /**
+ * This test corresponds to a user that upgraded from a build that didn't support tile sizes to
+ * one that does, via restore from backup. After that, the user modifies the size of some tiles
+ * and then restarts the device.
+ *
+ * The resulting set of large tiles are those after the user modifications.
+ */
+ @Test
+ fun restoreFromBackup_changeSizes_restart_newLargeSet() =
+ kosmos.runTest {
+ val largeTiles by collectLastValue(underTest.largeTilesSpecs)
+ val readTiles = setOf("a", "b", "c").toTileSpecs()
+
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.RestoreFromBackup(readTiles),
+ userId,
+ )
+ underTest.setLargeTilesSpecs(emptySet())
+
+ assertThat(largeTiles).isEmpty()
+
+ // Restart
+ underTest.setInitialOrUpgradeLargeTilesSpecs(
+ TilesUpgradePath.ReadFromSettings(readTiles),
+ userId,
+ )
+ assertThat(largeTiles).isEmpty()
+ }
+
+ private companion object {
+ private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs"
+
+ private fun Kosmos.getSharedPreferences(): SharedPreferences =
+ userFileManager.getSharedPreferences(
+ QSPreferencesRepository.FILE_NAME,
+ Context.MODE_PRIVATE,
+ userRepository.getSelectedUserInfo().id,
+ )
+
+ private fun Kosmos.setLargeTilesSpecsInSharedPreferences(specs: Set<String>) {
+ getSharedPreferences().edit().putStringSet(LARGE_TILES_SPECS_KEY, specs).apply()
+ }
+
+ private fun Kosmos.getLargeTilesSpecsFromSharedPreferences(): Set<String> {
+ return getSharedPreferences().getStringSet(LARGE_TILES_SPECS_KEY, emptySet())!!
+ }
+
+ private fun Set<String>.toTileSpecs(): Set<TileSpec> {
+ return map { TileSpec.create(it) }.toSet()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
index 79acfdaa415b..9838bcb86684 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
@@ -66,7 +66,7 @@ class IconTilesInteractorTest : SysuiTestCase() {
runCurrent()
// Resize it to large
- qsPreferencesRepository.setLargeTilesSpecs(setOf(spec))
+ qsPreferencesRepository.writeLargeTileSpecs(setOf(spec))
runCurrent()
// Assert that the new tile was added to the large tiles set
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
index 4b8cd3742bff..d9b3926fa215 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
@@ -24,6 +24,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.res.R
import com.android.systemui.retail.data.repository.FakeRetailModeRepository
@@ -242,9 +243,12 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
storeTilesForUser(startingTiles, userId)
val tiles by collectLastValue(underTest.tilesSpecs(userId))
- val tilesRead by collectLastValue(underTest.tilesReadFromSetting.consumeAsFlow())
+ val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
- assertThat(tilesRead).isEqualTo(startingTiles.toTileSpecs().toSet() to userId)
+ assertThat(tilesRead)
+ .isEqualTo(
+ TilesUpgradePath.ReadFromSettings(startingTiles.toTileSpecs().toSet()) to userId
+ )
}
@Test
@@ -258,13 +262,13 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
val tiles10 by collectLastValue(underTest.tilesSpecs(10))
val tiles11 by collectLastValue(underTest.tilesSpecs(11))
- val tilesRead by collectValues(underTest.tilesReadFromSetting.consumeAsFlow())
+ val tilesRead by collectValues(underTest.tilesUpgradePath.consumeAsFlow())
assertThat(tilesRead).hasSize(2)
assertThat(tilesRead)
.containsExactly(
- startingTiles10.toTileSpecs().toSet() to 10,
- startingTiles11.toTileSpecs().toSet() to 11,
+ TilesUpgradePath.ReadFromSettings(startingTiles10.toTileSpecs().toSet()) to 10,
+ TilesUpgradePath.ReadFromSettings(startingTiles11.toTileSpecs().toSet()) to 11,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
index 1945f750efaf..29bd18d3f3a0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
@@ -7,8 +7,8 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.pipeline.data.model.RestoreData
-import com.android.systemui.qs.pipeline.data.repository.UserTileSpecRepositoryTest.Companion.toTilesSet
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
@@ -352,11 +352,11 @@ class UserTileSpecRepositoryTest : SysuiTestCase() {
@Test
fun noSettingsStored_noTilesReadFromSettings() =
testScope.runTest {
- val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+ val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
val tiles by collectLastValue(underTest.tiles())
assertThat(tiles).isEqualTo(getDefaultTileSpecs())
- assertThat(tilesRead).isEqualTo(null)
+ assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet)
}
@Test
@@ -365,19 +365,20 @@ class UserTileSpecRepositoryTest : SysuiTestCase() {
val storedTiles = "a,b"
storeTiles(storedTiles)
val tiles by collectLastValue(underTest.tiles())
- val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+ val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
- assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet())
+ assertThat(tilesRead)
+ .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet()))
}
@Test
fun noSettingsStored_tilesChanged_tilesReadFromSettingsNotChanged() =
testScope.runTest {
- val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+ val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
val tiles by collectLastValue(underTest.tiles())
underTest.addTile(TileSpec.create("a"))
- assertThat(tilesRead).isEqualTo(null)
+ assertThat(tilesRead).isEqualTo(TilesUpgradePath.DefaultSet)
}
@Test
@@ -386,10 +387,34 @@ class UserTileSpecRepositoryTest : SysuiTestCase() {
val storedTiles = "a,b"
storeTiles(storedTiles)
val tiles by collectLastValue(underTest.tiles())
- val tilesRead by collectLastValue(underTest.tilesReadFromSettings.consumeAsFlow())
+ val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
underTest.addTile(TileSpec.create("c"))
- assertThat(tilesRead).isEqualTo(storedTiles.toTilesSet())
+ assertThat(tilesRead)
+ .isEqualTo(TilesUpgradePath.ReadFromSettings(storedTiles.toTilesSet()))
+ }
+
+ @Test
+ fun tilesRestoredFromBackup() =
+ testScope.runTest {
+ val specsBeforeRestore = "a,b,c,d,e"
+ val restoredSpecs = "a,c,d,f"
+ val autoAddedBeforeRestore = "b,d"
+ val restoredAutoAdded = "d,e"
+
+ storeTiles(specsBeforeRestore)
+ val tiles by collectLastValue(underTest.tiles())
+ val tilesRead by collectLastValue(underTest.tilesUpgradePath.consumeAsFlow())
+ runCurrent()
+
+ val restoreData =
+ RestoreData(restoredSpecs.toTileSpecs(), restoredAutoAdded.toTilesSet(), USER)
+ underTest.reconcileRestore(restoreData, autoAddedBeforeRestore.toTilesSet())
+ runCurrent()
+
+ val expected = "a,b,c,d,f"
+ assertThat(tilesRead)
+ .isEqualTo(TilesUpgradePath.RestoreFromBackup(expected.toTilesSet()))
}
private fun getDefaultTileSpecs(): List<TileSpec> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
index df2dd99c779e..b98059a1fe90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt
@@ -31,7 +31,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -84,13 +84,14 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun downFromTopLeft_switchesToNotificationsShade() =
+ fun downFromTopStart_switchesToNotificationsShade() =
testScope.runTest {
val actions by collectLastValue(underTest.actions)
underTest.activateIn(this)
val action =
- (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay)
+ (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf))
+ as? ShowOverlay)
assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade)
val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
assertThat(overlaysToHide).isNotNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index a0d86f27b9b8..80c7026b0cea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -28,6 +28,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
@@ -60,6 +61,10 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -713,4 +718,43 @@ class SceneInteractorTest : SysuiTestCase() {
assertThat(currentScene).isEqualTo(originalScene)
assertThat(currentOverlays).isEmpty()
}
+
+ @Test
+ fun changeScene_notifiesAboutToChangeListener() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ // Unlock so transitioning to the Gone scene becomes possible.
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ underTest.changeScene(toScene = Scenes.Gone, loggingReason = "")
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+ val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+ underTest.registerSceneStateProcessor(processor)
+
+ underTest.changeScene(
+ toScene = Scenes.Lockscreen,
+ sceneState = KeyguardState.AOD,
+ loggingReason = "",
+ )
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD)
+ }
+
+ @Test
+ fun changeScene_noOp_whenFromAndToAreTheSame() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>()
+ underTest.registerSceneStateProcessor(processor)
+
+ underTest.changeScene(toScene = checkNotNull(currentScene), loggingReason = "")
+
+ verify(processor, never()).onSceneAboutToChange(any(), any())
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt
new file mode 100644
index 000000000000..a09e5cd9de9b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetectorTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.BottomEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerSwipeDetectorTest : SysuiTestCase() {
+
+ private val edgeSize = 40
+ private val screenWidth = 800
+ private val screenHeight = 600
+
+ private val underTest = SceneContainerSwipeDetector(edgeSize = edgeSize.dp)
+
+ @Test
+ fun source_noEdge_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = screenWidth / 2 - 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopLeft_detectsLeftEdge() {
+ val detectedEdge = swipeHorizontallyFrom(x = 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopRight_detectsRightHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopRight_detectsRightEdge() {
+ val detectedEdge = swipeHorizontallyFrom(x = screenWidth - 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1)
+ assertThat(detectedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnBottom_detectsBottomEdge() {
+ val detectedEdge =
+ swipeVerticallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize / 2))
+ assertThat(detectedEdge).isEqualTo(BottomEdge)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnBottom_detectsLeftHalf() {
+ val detectedEdge =
+ swipeHorizontallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize - 1))
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnLeft_detectsLeftEdge() {
+ val detectedEdge = swipeHorizontallyFrom(x = edgeSize - 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnLeft_detectsLeftHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = edgeSize - 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnRight_detectsRightEdge() {
+ val detectedEdge =
+ swipeHorizontallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnRight_detectsRightHalf() {
+ val detectedEdge = swipeVerticallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
+ assertThat(detectedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun resolve_startEdgeInLtr_resolvesLeftEdge() {
+ val resolvedEdge = StartEdge.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun resolve_startEdgeInRtl_resolvesRightEdge() {
+ val resolvedEdge = StartEdge.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun resolve_endEdgeInLtr_resolvesRightEdge() {
+ val resolvedEdge = EndEdge.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(RightEdge)
+ }
+
+ @Test
+ fun resolve_endEdgeInRtl_resolvesLeftEdge() {
+ val resolvedEdge = EndEdge.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(LeftEdge)
+ }
+
+ @Test
+ fun resolve_startHalfInLtr_resolvesLeftHalf() {
+ val resolvedEdge = StartHalf.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(LeftHalf)
+ }
+
+ @Test
+ fun resolve_startHalfInRtl_resolvesRightHalf() {
+ val resolvedEdge = StartHalf.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun resolve_endHalfInLtr_resolvesRightHalf() {
+ val resolvedEdge = EndHalf.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(RightHalf)
+ }
+
+ @Test
+ fun resolve_endHalfInRtl_resolvesLeftHalf() {
+ val resolvedEdge = EndHalf.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(LeftHalf)
+ }
+
+ private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
+ return swipeFrom(x, y, Orientation.Vertical)
+ }
+
+ private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
+ return swipeFrom(x, y, Orientation.Horizontal)
+ }
+
+ private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerArea.Resolved? {
+ return underTest.source(
+ layoutSize = IntSize(width = screenWidth, height = screenHeight),
+ position = IntOffset(x, y),
+ density = Density(1f),
+ orientation = orientation,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 30d9f73d7441..adaebbd27986 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -55,6 +56,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
@@ -324,7 +326,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
kosmos.enableSingleShade()
assertThat(shadeMode).isEqualTo(ShadeMode.Single)
- assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector)
}
@Test
@@ -334,26 +336,28 @@ class SceneContainerViewModelTest : SysuiTestCase() {
kosmos.enableSplitShade()
assertThat(shadeMode).isEqualTo(ShadeMode.Split)
- assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ assertThat(underTest.swipeSourceDetector).isEqualTo(DefaultEdgeDetector)
}
@Test
- fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+ fun edgeDetector_dualShade_narrowScreen_usesSceneContainerSwipeDetector() =
testScope.runTest {
val shadeMode by collectLastValue(kosmos.shadeMode)
kosmos.enableDualShade(wideLayout = false)
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
- assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ assertThat(underTest.swipeSourceDetector)
+ .isInstanceOf(SceneContainerSwipeDetector::class.java)
}
@Test
- fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+ fun edgeDetector_dualShade_wideScreen_usesSceneContainerSwipeDetector() =
testScope.runTest {
val shadeMode by collectLastValue(kosmos.shadeMode)
kosmos.enableDualShade(wideLayout = true)
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
- assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ assertThat(underTest.swipeSourceDetector)
+ .isInstanceOf(SceneContainerSwipeDetector::class.java)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
deleted file mode 100644
index 3d76d280b2cc..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
+++ /dev/null
@@ -1,274 +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.scene.ui.viewmodel
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
-import com.google.common.truth.Truth.assertThat
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class SplitEdgeDetectorTest : SysuiTestCase() {
-
- private val edgeSize = 40
- private val screenWidth = 800
- private val screenHeight = 600
-
- private var edgeSplitFraction = 0.7f
-
- private val underTest =
- SplitEdgeDetector(
- topEdgeSplitFraction = { edgeSplitFraction },
- edgeSize = edgeSize.dp,
- )
-
- @Test
- fun source_noEdge_detectsNothing() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth / 2,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopLeft)
- }
-
- @Test
- fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(Left)
- }
-
- @Test
- fun source_swipeVerticallyOnTopRight_detectsTopRight() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth - 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun source_swipeHorizontallyOnTopRight_detectsRight() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = screenWidth - 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(Right)
- }
-
- @Test
- fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = (screenWidth * edgeSplitFraction).toInt() - 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopLeft)
- }
-
- @Test
- fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = (screenWidth * edgeSplitFraction).toInt() + 1,
- y = edgeSize - 1,
- )
- assertThat(detectedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun source_edgeSplitFractionUpdatesDynamically() {
- val middleX = (screenWidth * 0.5f).toInt()
- val topY = 0
-
- // Split closer to the right; middle of screen is considered "left".
- edgeSplitFraction = 0.6f
- assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
-
- // Split closer to the left; middle of screen is considered "right".
- edgeSplitFraction = 0.4f
- assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
-
- // Illegal fraction.
- edgeSplitFraction = 1.2f
- assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
-
- // Illegal fraction.
- edgeSplitFraction = -0.3f
- assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
- }
-
- @Test
- fun source_swipeVerticallyOnBottom_detectsBottom() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth / 3,
- y = screenHeight - (edgeSize / 2),
- )
- assertThat(detectedEdge).isEqualTo(Bottom)
- }
-
- @Test
- fun source_swipeHorizontallyOnBottom_detectsNothing() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = screenWidth / 3,
- y = screenHeight - (edgeSize - 1),
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun source_swipeHorizontallyOnLeft_detectsLeft() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = edgeSize - 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isEqualTo(Left)
- }
-
- @Test
- fun source_swipeVerticallyOnLeft_detectsNothing() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = edgeSize - 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun source_swipeHorizontallyOnRight_detectsRight() {
- val detectedEdge =
- swipeHorizontallyFrom(
- x = screenWidth - edgeSize + 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isEqualTo(Right)
- }
-
- @Test
- fun source_swipeVerticallyOnRight_detectsNothing() {
- val detectedEdge =
- swipeVerticallyFrom(
- x = screenWidth - edgeSize + 1,
- y = screenHeight / 2,
- )
- assertThat(detectedEdge).isNull()
- }
-
- @Test
- fun resolve_startInLtr_resolvesLeft() {
- val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(Left)
- }
-
- @Test
- fun resolve_startInRtl_resolvesRight() {
- val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(Right)
- }
-
- @Test
- fun resolve_endInLtr_resolvesRight() {
- val resolvedEdge = End.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(Right)
- }
-
- @Test
- fun resolve_endInRtl_resolvesLeft() {
- val resolvedEdge = End.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(Left)
- }
-
- @Test
- fun resolve_topStartInLtr_resolvesTopLeft() {
- val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(TopLeft)
- }
-
- @Test
- fun resolve_topStartInRtl_resolvesTopRight() {
- val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun resolve_topEndInLtr_resolvesTopRight() {
- val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
- assertThat(resolvedEdge).isEqualTo(TopRight)
- }
-
- @Test
- fun resolve_topEndInRtl_resolvesTopLeft() {
- val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
- assertThat(resolvedEdge).isEqualTo(TopLeft)
- }
-
- private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
- return swipeFrom(x, y, Orientation.Vertical)
- }
-
- private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
- return swipeFrom(x, y, Orientation.Horizontal)
- }
-
- private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
- return underTest.source(
- layoutSize = IntSize(width = screenWidth, height = screenHeight),
- position = IntOffset(x, y),
- density = Density(1f),
- orientation = orientation,
- )
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 3d3178793a09..c6801f1ad9d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -460,14 +460,14 @@ public class CommandQueueTest extends SysuiTestCase {
}
@Test
- public void testonDisplayAddSystemDecorations() {
+ public void testOnDisplayAddSystemDecorations() {
mCommandQueue.onDisplayAddSystemDecorations(DEFAULT_DISPLAY);
waitForIdleSync();
verify(mCallbacks).onDisplayAddSystemDecorations(eq(DEFAULT_DISPLAY));
}
@Test
- public void testonDisplayAddSystemDecorationsForSecondaryDisplay() {
+ public void testOnDisplayAddSystemDecorationsForSecondaryDisplay() {
mCommandQueue.onDisplayAddSystemDecorations(SECONDARY_DISPLAY);
waitForIdleSync();
verify(mCallbacks).onDisplayAddSystemDecorations(eq(SECONDARY_DISPLAY));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 816df0102940..20637cd4af33 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -61,14 +61,14 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.addNotif
+import com.android.systemui.statusbar.notification.data.repository.addNotifs
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
-import com.android.systemui.statusbar.notification.shared.CallType
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
-import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
import com.android.systemui.testKosmos
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -93,7 +93,6 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
- private val callRepo = kosmos.ongoingCallRepository
private val activeNotificationListRepository = kosmos.activeNotificationListRepository
private val mockSystemUIDialog = mock<SystemUIDialog>()
@@ -132,7 +131,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -145,7 +144,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -178,7 +177,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -191,7 +190,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -224,7 +223,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ addOngoingCallState("call")
val latest by collectLastValue(underTest.primaryChip)
@@ -237,9 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -255,16 +252,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "call",
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(callNotificationKey)
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -281,7 +269,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chipsLegacy_oneChip_notSquished() =
kosmos.runTest {
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState()
val latest by collectLastValue(underTest.chipsLegacy)
@@ -294,17 +282,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chips_oneChip_notSquished() =
kosmos.runTest {
- val callNotificationKey = "call"
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState()
val latest by collectLastValue(underTest.chips)
@@ -315,10 +293,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
@Test
- fun chipsLegacy_twoTimerChips_isSmallPortrait_andChipsModernizationDisabled_bothSquished() =
+ fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
@@ -329,12 +307,28 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
}
+ @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ @Test
+ fun chips_twoTimerChips_isSmallPortrait_bothSquished() =
+ kosmos.runTest {
+ screenRecordState.value = ScreenRecordModel.Recording
+ addOngoingCallState(key = "call")
+
+ val latest by collectLastValue(underTest.chips)
+
+ // Squished chips are icon only
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat(latest!!.active[1])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ }
+
@DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
@Test
fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
@@ -346,6 +340,23 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
}
+ @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ @Test
+ fun chips_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
+ kosmos.runTest {
+ screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
+ addOngoingCallState(key = "call")
+
+ val latest by collectLastValue(underTest.chips)
+
+ // The screen record countdown isn't squished to icon-only
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.Countdown::class.java)
+ // But the call chip *is* squished
+ assertThat(latest!!.active[1])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ }
+
@DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
@Test
fun chipsLegacy_numberOfChipsChanges_chipsGetSquishedAndUnsquished() =
@@ -354,7 +365,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// WHEN there's only one chip
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
// The screen record isn't squished because it's the only one
assertThat(latest!!.primary)
@@ -363,7 +374,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
// WHEN there's 2 chips
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
// THEN they both become squished
assertThat(latest!!.primary)
@@ -382,12 +393,44 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
}
+ @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ @Test
+ fun chips_numberOfChipsChanges_chipsGetSquishedAndUnsquished() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+
+ // WHEN there's only one chip
+ screenRecordState.value = ScreenRecordModel.Recording
+ removeOngoingCallState(key = "call")
+
+ // The screen record isn't squished because it's the only one
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+
+ // WHEN there's 2 chips
+ addOngoingCallState(key = "call")
+
+ // THEN they both become squished
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ // But the call chip *is* squished
+ assertThat(latest!!.active[1])
+ .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+
+ // WHEN we go back down to 1 chip
+ screenRecordState.value = ScreenRecordModel.DoingNothing
+
+ // THEN the remaining chip unsquishes
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ }
+
@DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
@Test
fun chipsLegacy_twoChips_isLandscape_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
// WHEN we're in landscape
val config =
@@ -405,12 +448,35 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
}
+ @EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ @Test
+ fun chips_twoChips_isLandscape_notSquished() =
+ kosmos.runTest {
+ screenRecordState.value = ScreenRecordModel.Recording
+ addOngoingCallState(key = "call")
+
+ // WHEN we're in landscape
+ val config =
+ Configuration(kosmos.mainResources.configuration).apply {
+ orientation = Configuration.ORIENTATION_LANDSCAPE
+ }
+ kosmos.fakeConfigurationRepository.onConfigurationChange(config)
+
+ val latest by collectLastValue(underTest.chips)
+
+ // THEN the chips aren't squished (squished chips would be icon only)
+ assertThat(latest!!.active[0])
+ .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat(latest!!.active[1])
+ .isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ }
+
@DisableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
@Test
fun chipsLegacy_twoChips_isLargeScreen_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34, notificationKey = "call"))
+ addOngoingCallState(key = "call")
// WHEN we're on a large screen
kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -424,25 +490,19 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
}
- @Test
@EnableFlags(StatusBarChipsModernization.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun chips_twoChips_chipsModernizationEnabled_notSquished() =
+ @Test
+ fun chips_twoChips_isLargeScreen_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "call",
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(key = "call")
+
+ // WHEN we're on a large screen
+ kosmos.displayStateRepository.setIsLargeScreen(true)
val latest by collectLastValue(underTest.chips)
- // Squished chips would be icon only
+ // THEN the chips aren't squished (squished chips would be icon only)
assertThat(latest!!.active[0])
.isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
assertThat(latest!!.active[1])
@@ -455,7 +515,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -469,7 +529,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -510,7 +570,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -525,9 +585,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(key = "call")
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -545,16 +603,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -575,9 +624,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value = MediaProjectionState.NotProjecting
val callNotificationKey = "call"
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.primaryChip)
@@ -593,9 +640,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -614,16 +659,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- )
- )
- )
+ addOngoingCallState(key = callNotificationKey)
val latest by collectLastValue(underTest.chips)
val unused by collectLastValue(underTest.chipsLegacy)
@@ -837,12 +873,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val unused by collectLastValue(underTest.chips)
val callNotificationKey = "call"
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
val firstIcon = createStatusBarIconViewOrNull()
- setNotifs(
+ activeNotificationListRepository.addNotifs(
listOf(
activeNotificationModel(
key = "firstNotif",
@@ -874,14 +908,10 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
val firstIcon = createStatusBarIconViewOrNull()
val secondIcon = createStatusBarIconViewOrNull()
- setNotifs(
+ addOngoingCallState(key = callNotificationKey)
+ activeNotificationListRepository.addNotifs(
listOf(
activeNotificationModel(
- key = callNotificationKey,
- whenTime = 499,
- callType = CallType.Ongoing,
- ),
- activeNotificationModel(
key = "firstNotif",
statusBarChipIcon = firstIcon,
promotedContent =
@@ -913,17 +943,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
@@ -942,20 +968,14 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
val notifIcon = createStatusBarIconViewOrNull()
screenRecordState.value = ScreenRecordModel.Recording
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- whenTime = 499,
- callType = CallType.Ongoing,
- ),
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- ),
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = notifIcon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
+ addOngoingCallState(key = callNotificationKey)
assertThat(latest!!.active.size).isEqualTo(2)
assertIsScreenRecordChip(latest!!.active[0])
@@ -982,7 +1002,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
)
)
// And everything else hidden
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
@@ -991,9 +1011,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertIsNotifChip(latest, context, notifIcon, "notif")
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
// THEN the higher priority call chip is used
assertIsCallChip(latest, callNotificationKey)
@@ -1024,17 +1042,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
val notifIcon = createStatusBarIconViewOrNull()
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = notifIcon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
@@ -1056,7 +1070,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority call is removed
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
// THEN the lower priority notif is used
assertIsNotifChip(latest, context, notifIcon, "notif")
@@ -1069,17 +1083,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val callNotificationKey = "call"
// Start with just the lowest priority chip shown
val notifIcon = createStatusBarIconViewOrNull()
- setNotifs(
- listOf(
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- )
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = notifIcon,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
// And everything else hidden
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
@@ -1092,9 +1104,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(
- inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
- )
+ addOngoingCallState(callNotificationKey)
// THEN the higher priority call chip is used as primary and notif is demoted to
// secondary
@@ -1125,7 +1135,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// WHEN screen record and call is dropped
screenRecordState.value = ScreenRecordModel.DoingNothing
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = callNotificationKey)
// THEN media projection and notif remain
assertIsShareToAppChip(latest!!.primary)
@@ -1172,21 +1182,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModelLegacy())
// WHEN the higher priority call chip is added
- setNotifs(
- listOf(
- activeNotificationModel(
- key = callNotificationKey,
- statusBarChipIcon = createStatusBarIconViewOrNull(),
- callType = CallType.Ongoing,
- whenTime = 499,
- ),
- activeNotificationModel(
- key = "notif",
- statusBarChipIcon = notifIcon,
- promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
- ),
- )
- )
+ addOngoingCallState(key = callNotificationKey)
// THEN the higher priority call chip and notif are active in that order
assertThat(latest!!.active.size).isEqualTo(2)
@@ -1372,7 +1368,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
@@ -1399,7 +1395,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
screenRecordState.value = ScreenRecordModel.DoingNothing
- callRepo.setOngoingCallState(OngoingCallModel.NoCall)
+ removeOngoingCallState(key = "call")
val latest by collectLastValue(underTest.primaryChip)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
deleted file mode 100644
index a64339e20f7c..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ /dev/null
@@ -1,1189 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.notification.row;
-
-import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO;
-import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
-import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE;
-import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
-import static android.app.NotificationManager.IMPORTANCE_LOW;
-import static android.app.NotificationManager.IMPORTANCE_MIN;
-import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
-import static android.print.PrintManager.PRINT_SPOOLER_PACKAGE_NAME;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.Flags;
-import android.app.INotificationManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationChannelGroup;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.StatusBarNotification;
-import android.telecom.TelecomManager;
-import android.testing.TestableLooper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.res.R;
-import com.android.systemui.statusbar.RankingBuilder;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.CountDownLatch;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@TestableLooper.RunWithLooper
-public class NotificationInfoTest extends SysuiTestCase {
- private static final String TEST_PACKAGE_NAME = "test_package";
- private static final String TEST_SYSTEM_PACKAGE_NAME = PRINT_SPOOLER_PACKAGE_NAME;
- private static final int TEST_UID = 1;
- private static final String TEST_CHANNEL = "test_channel";
- private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME";
-
- private TestableLooper mTestableLooper;
- private NotificationInfo mNotificationInfo;
- private NotificationChannel mNotificationChannel;
- private NotificationChannel mDefaultNotificationChannel;
- private NotificationChannel mClassifiedNotificationChannel;
- private StatusBarNotification mSbn;
- private NotificationEntry mEntry;
- private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
-
- @Rule
- public MockitoRule mockito = MockitoJUnit.rule();
- @Mock
- private MetricsLogger mMetricsLogger;
- @Mock
- private INotificationManager mMockINotificationManager;
- @Mock
- private PackageManager mMockPackageManager;
- @Mock
- private OnUserInteractionCallback mOnUserInteractionCallback;
- @Mock
- private ChannelEditorDialogController mChannelEditorDialogController;
- @Mock
- private AssistantFeedbackController mAssistantFeedbackController;
- @Mock
- private TelecomManager mTelecomManager;
-
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
- @Before
- public void setUp() throws Exception {
- mTestableLooper = TestableLooper.get(this);
-
- mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
-
- mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
- // Inflate the layout
- final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
- mNotificationInfo = (NotificationInfo) layoutInflater.inflate(R.layout.notification_info,
- null);
- mNotificationInfo.setGutsParent(mock(NotificationGuts.class));
- // Our view is never attached to a window so the View#post methods in NotificationInfo never
- // get called. Setting this will skip the post and do the action immediately.
- mNotificationInfo.mSkipPost = true;
-
- // PackageManager must return a packageInfo and applicationInfo.
- final PackageInfo packageInfo = new PackageInfo();
- packageInfo.packageName = TEST_PACKAGE_NAME;
- when(mMockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
- .thenReturn(packageInfo);
- final ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.uid = TEST_UID; // non-zero
- final PackageInfo systemPackageInfo = new PackageInfo();
- systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME;
- when(mMockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt()))
- .thenReturn(systemPackageInfo);
- when(mMockPackageManager.getPackageInfo(eq("android"), anyInt()))
- .thenReturn(packageInfo);
-
- ComponentName assistant = new ComponentName("package", "service");
- when(mMockINotificationManager.getAllowedNotificationAssistant()).thenReturn(assistant);
- ResolveInfo ri = new ResolveInfo();
- ri.activityInfo = new ActivityInfo();
- ri.activityInfo.packageName = assistant.getPackageName();
- ri.activityInfo.name = "activity";
- when(mMockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(List.of(ri));
-
- // Package has one channel by default.
- when(mMockINotificationManager.getNumNotificationChannelsForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean())).thenReturn(1);
-
- // Some test channels.
- mNotificationChannel = new NotificationChannel(
- TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
- mDefaultNotificationChannel = new NotificationChannel(
- NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME,
- IMPORTANCE_LOW);
- mClassifiedNotificationChannel =
- new NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW);
-
- Notification notification = new Notification();
- notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, applicationInfo);
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
- notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0);
- mEntry = new NotificationEntryBuilder().setSbn(mSbn).build();
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(false);
- when(mAssistantFeedbackController.getInlineDescriptionResource(any()))
- .thenReturn(R.string.notification_channel_summary_automatic);
- }
-
- private void doStandardBind() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- }
-
- @Test
- public void testBindNotification_SetsTextApplicationName() throws Exception {
- when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name");
- doStandardBind();
- final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
- assertTrue(textView.getText().toString().contains("App Name"));
- assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
- }
-
- @Test
- public void testBindNotification_SetsPackageIcon() throws Exception {
- final Drawable iconDrawable = mock(Drawable.class);
- when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class)))
- .thenReturn(iconDrawable);
- doStandardBind();
- final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon);
- assertEquals(iconDrawable, iconView.getDrawable());
- }
-
- @Test
- public void testBindNotification_noDelegate() throws Exception {
- doStandardBind();
- final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
- assertEquals(GONE, nameView.getVisibility());
- }
-
- @Test
- public void testBindNotification_delegate() throws Exception {
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, "other", 0, null, TEST_UID, 0,
- new Notification(), UserHandle.CURRENT, null, 0);
- final ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.uid = 7; // non-zero
- when(mMockPackageManager.getApplicationInfo(eq("other"), anyInt())).thenReturn(
- applicationInfo);
- when(mMockPackageManager.getApplicationLabel(any())).thenReturn("Other");
-
- NotificationEntry entry = new NotificationEntryBuilder().setSbn(mSbn).build();
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- entry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
- assertEquals(VISIBLE, nameView.getVisibility());
- assertTrue(nameView.getText().toString().contains("Proxied"));
- }
-
- @Test
- public void testBindNotification_GroupNameHiddenIfNoGroup() throws Exception {
- doStandardBind();
- final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
- assertEquals(GONE, groupNameView.getVisibility());
- }
-
- @Test
- public void testBindNotification_SetsGroupNameIfNonNull() throws Exception {
- mNotificationChannel.setGroup("test_group_id");
- final NotificationChannelGroup notificationChannelGroup =
- new NotificationChannelGroup("test_group_id", "Test Group Name");
- when(mMockINotificationManager.getNotificationChannelGroupForPackage(
- eq("test_group_id"), eq(TEST_PACKAGE_NAME), eq(TEST_UID)))
- .thenReturn(notificationChannelGroup);
- doStandardBind();
- final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
- assertEquals(View.VISIBLE, groupNameView.getVisibility());
- assertEquals("Test Group Name", groupNameView.getText());
- }
-
- @Test
- public void testBindNotification_SetsTextChannelName() throws Exception {
- doStandardBind();
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(TEST_CHANNEL_NAME, textView.getText());
- }
-
- @Test
- public void testBindNotification_DefaultChannelDoesNotUseChannelName() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mDefaultNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(GONE, textView.getVisibility());
- }
-
- @Test
- public void testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist()
- throws Exception {
- // Package has more than one channel by default.
- when(mMockINotificationManager.getNumNotificationChannelsForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean())).thenReturn(10);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mDefaultNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(VISIBLE, textView.getVisibility());
- }
-
- @Test
- public void testBindNotification_UnblockablePackageUsesChannelName() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- true,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(VISIBLE, textView.getVisibility());
- }
-
- @Test
- public void testBindNotification_SetsOnClickListenerForSettings() throws Exception {
- final CountDownLatch latch = new CountDownLatch(1);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- (View v, NotificationChannel c, int appUid) -> {
- assertEquals(mNotificationChannel, c);
- latch.countDown();
- },
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- settingsButton.performClick();
- // Verify that listener was triggered.
- assertEquals(0, latch.getCount());
- }
-
- @Test
- public void testBindNotification_SettingsButtonInvisibleWhenNoClickListener() throws Exception {
- doStandardBind();
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- assertTrue(settingsButton.getVisibility() != View.VISIBLE);
- }
-
- @Test
- public void testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned()
- throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- (View v, NotificationChannel c, int appUid) -> {
- assertEquals(mNotificationChannel, c);
- },
- null,
- null,
- mUiEventLogger,
- false,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- assertTrue(settingsButton.getVisibility() != View.VISIBLE);
- }
-
- @Test
- public void testBindNotification_SettingsButtonReappearsAfterSecondBind() throws Exception {
- doStandardBind();
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- (View v, NotificationChannel c, int appUid) -> { },
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- assertEquals(View.VISIBLE, settingsButton.getVisibility());
- }
-
- @Test
- public void testBindNotification_whenAppUnblockable() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- true,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text);
- assertEquals(View.VISIBLE, view.getVisibility());
- assertEquals(mContext.getString(R.string.notification_unblockable_desc),
- view.getText());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility());
- }
-
- @Test
- public void testBindNotification_whenCurrentlyInCall() throws Exception {
- when(mMockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true);
-
- Person person = new Person.Builder()
- .setName("caller")
- .build();
- Notification.Builder nb = new Notification.Builder(
- mContext, mNotificationChannel.getId())
- .setContentTitle("foo")
- .setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setStyle(Notification.CallStyle.forOngoingCall(
- person, mock(PendingIntent.class)))
- .setFullScreenIntent(mock(PendingIntent.class), true)
- .addAction(new Notification.Action.Builder(null, "test", null).build());
-
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
- nb.build(), UserHandle.getUserHandleForUid(TEST_UID), null, 0);
- mEntry.setSbn(mSbn);
- doStandardBind();
- final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text);
- assertEquals(View.VISIBLE, view.getVisibility());
- assertEquals(mContext.getString(R.string.notification_unblockable_call_desc),
- view.getText());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.non_configurable_text).getVisibility());
- }
-
- @Test
- public void testBindNotification_whenCurrentlyInCall_notCall() throws Exception {
- when(mMockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true);
-
- Notification.Builder nb = new Notification.Builder(
- mContext, mNotificationChannel.getId())
- .setContentTitle("foo")
- .setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setFullScreenIntent(mock(PendingIntent.class), true)
- .addAction(new Notification.Action.Builder(null, "test", null).build());
-
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
- nb.build(), UserHandle.getUserHandleForUid(TEST_UID), null, 0);
- mEntry.setSbn(mSbn);
- doStandardBind();
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility());
- assertEquals(VISIBLE,
- mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.non_configurable_text).getVisibility());
- }
-
- @Test
- public void testBindNotification_automaticIsVisible() throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- doStandardBind();
- assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
- assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
- }
-
- @Test
- public void testBindNotification_automaticIsGone() throws Exception {
- doStandardBind();
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
- }
-
- @Test
- public void testBindNotification_automaticIsSelected() throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- mNotificationChannel.unlockFields(USER_LOCKED_IMPORTANCE);
- doStandardBind();
- assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected());
- }
-
- @Test
- public void testBindNotification_alertIsSelected() throws Exception {
- doStandardBind();
- assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected());
- }
-
- @Test
- public void testBindNotification_silenceIsSelected() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected());
- }
-
- @Test
- public void testBindNotification_DoesNotUpdateNotificationChannel() throws Exception {
- doStandardBind();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testBindNotification_LogsOpen() throws Exception {
- doStandardBind();
- assertEquals(1, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- }
-
- @Test
- public void testDoesNotUpdateNotificationChannelAfterImportanceChanged() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.automatic).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testHandleCloseControls_persistAutomatic()
- throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- mNotificationChannel.unlockFields(USER_LOCKED_IMPORTANCE);
- doStandardBind();
-
- mNotificationInfo.handleCloseControls(true, false);
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).unlockNotificationChannel(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged()
- throws Exception {
- int originalImportance = mNotificationChannel.getImportance();
- doStandardBind();
-
- mNotificationInfo.handleCloseControls(true, false);
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- assertEquals(originalImportance, mNotificationChannel.getImportance());
-
- assertEquals(2, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged.
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(),
- mUiEventLogger.eventId(1));
- }
-
- @Test
- public void testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_UNSPECIFIED);
- doStandardBind();
-
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- assertEquals(IMPORTANCE_UNSPECIFIED, mNotificationChannel.getImportance());
- }
-
- @Test
- public void testSilenceCallsUpdateNotificationChannel() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance());
-
- assertEquals(2, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(),
- mUiEventLogger.eventId(1));
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testUnSilenceCallsUpdateNotificationChannel() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testAutomaticUnlocksUserImportance() throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- mNotificationChannel.lockFields(USER_LOCKED_IMPORTANCE);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.automatic).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).unlockNotificationChannel(
- anyString(), eq(TEST_UID), any());
- assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_UNSPECIFIED);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testSilenceCallsUpdateNotificationChannel_channelImportanceMin()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_MIN);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.silence).performClick();
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_MIN, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testSilence_closeGutsThenTryToSave() throws RemoteException {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.handleCloseControls(false, false);
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
-
- assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testAlertCallsUpdateNotificationChannel_channelImportanceMin()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_MIN);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.alert).performClick();
- assertEquals(mContext.getString(R.string.inline_ok_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testAdjustImportanceTemporarilyAllowsReordering() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- verify(mOnUserInteractionCallback).onImportanceChanged(mEntry);
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testDoneText()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.alert).performClick();
- assertEquals(mContext.getString(R.string.inline_ok_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.silence).performClick();
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- }
-
- @Test
- public void testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testCloseControlsDoesNotUpdateIfSaveIsFalse() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(false, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
-
- assertEquals(1, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- }
-
- @Test
- public void testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
-
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
- }
-
- @Test
- public void testCloseControls_withoutHittingApply() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
-
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testWillBeRemovedReturnsFalse() throws Exception {
- assertFalse(mNotificationInfo.willBeRemoved());
-
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertFalse(mNotificationInfo.willBeRemoved());
- }
-
-
- @Test
- @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
- public void testBindNotification_HidesFeedbackLink_flagOff() throws Exception {
- doStandardBind();
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.feedback).getVisibility());
- }
-
- @Test
- @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
- public void testBindNotification_SetsFeedbackLink_isReservedChannel() throws RemoteException {
- mEntry.setRanking(new RankingBuilder(mEntry.getRanking())
- .setSummarization("something").build());
- final CountDownLatch latch = new CountDownLatch(1);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mClassifiedNotificationChannel,
- mEntry,
- null,
- null,
- (View v, Intent intent) -> {
- latch.countDown();
- },
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- final View feedback = mNotificationInfo.findViewById(R.id.feedback);
- assertEquals(VISIBLE, feedback.getVisibility());
- feedback.performClick();
- // Verify that listener was triggered.
- assertEquals(0, latch.getCount());
- }
-
- @Test
- @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
- public void testBindNotification_hidesFeedbackLink_notReservedChannel() throws Exception {
- doStandardBind();
-
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.feedback).getVisibility());
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
new file mode 100644
index 000000000000..2945fa98caad
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
@@ -0,0 +1,910 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Flags
+import android.app.INotificationManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationChannel.SOCIAL_MEDIA_ID
+import android.app.NotificationChannelGroup
+import android.app.NotificationManager
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.app.PendingIntent
+import android.app.Person
+import android.content.ComponentName
+import android.content.Intent
+import android.content.mockPackageManager
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.testableLooper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.print.PrintManager
+import android.service.notification.StatusBarNotification
+import android.telecom.TelecomManager
+import android.testing.TestableLooper.RunWithLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.metricsLogger
+import com.android.internal.logging.uiEventLoggerFake
+import com.android.systemui.Dependency
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.AssistantFeedbackController
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.telecom.telecomManager
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+class NotificationInfoTest : SysuiTestCase() {
+ private val kosmos = Kosmos().also { it.testCase = this }
+
+ private lateinit var underTest: NotificationInfo
+ private lateinit var notificationChannel: NotificationChannel
+ private lateinit var defaultNotificationChannel: NotificationChannel
+ private lateinit var classifiedNotificationChannel: NotificationChannel
+ private lateinit var sbn: StatusBarNotification
+ private lateinit var entry: NotificationEntry
+
+ private val mockPackageManager = kosmos.mockPackageManager
+ private val uiEventLogger = kosmos.uiEventLoggerFake
+ private val testableLooper by lazy { kosmos.testableLooper }
+
+ private val onUserInteractionCallback = mock<OnUserInteractionCallback>()
+ private val mockINotificationManager = mock<INotificationManager>()
+ private val channelEditorDialogController = mock<ChannelEditorDialogController>()
+ private val assistantFeedbackController = mock<AssistantFeedbackController>()
+
+ @Before
+ fun setUp() {
+ mContext.addMockSystemService(TelecomManager::class.java, kosmos.telecomManager)
+
+ mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper)
+
+ // Inflate the layout
+ val inflater = LayoutInflater.from(mContext)
+ underTest = inflater.inflate(R.layout.notification_info, null) as NotificationInfo
+
+ underTest.setGutsParent(mock<NotificationGuts>())
+
+ // Our view is never attached to a window so the View#post methods in NotificationInfo never
+ // get called. Setting this will skip the post and do the action immediately.
+ underTest.mSkipPost = true
+
+ // PackageManager must return a packageInfo and applicationInfo.
+ val packageInfo = PackageInfo()
+ packageInfo.packageName = TEST_PACKAGE_NAME
+ whenever(mockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
+ .thenReturn(packageInfo)
+ val applicationInfo = ApplicationInfo()
+ applicationInfo.uid = TEST_UID // non-zero
+ val systemPackageInfo = PackageInfo()
+ systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME
+ whenever(mockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt()))
+ .thenReturn(systemPackageInfo)
+ whenever(mockPackageManager.getPackageInfo(eq("android"), anyInt())).thenReturn(packageInfo)
+
+ val assistant = ComponentName("package", "service")
+ whenever(mockINotificationManager.allowedNotificationAssistant).thenReturn(assistant)
+ val ri = ResolveInfo()
+ ri.activityInfo = ActivityInfo()
+ ri.activityInfo.packageName = assistant.packageName
+ ri.activityInfo.name = "activity"
+ whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(listOf(ri))
+
+ // Package has one channel by default.
+ whenever(
+ mockINotificationManager.getNumNotificationChannelsForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(1)
+
+ // Some test channels.
+ notificationChannel = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW)
+ defaultNotificationChannel =
+ NotificationChannel(
+ NotificationChannel.DEFAULT_CHANNEL_ID,
+ TEST_CHANNEL_NAME,
+ IMPORTANCE_LOW,
+ )
+ classifiedNotificationChannel =
+ NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)
+
+ val notification = Notification()
+ notification.extras.putParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO,
+ applicationInfo,
+ )
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ TEST_PACKAGE_NAME,
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ notification,
+ UserHandle.getUserHandleForUid(TEST_UID),
+ null,
+ 0,
+ )
+ entry = NotificationEntryBuilder().setSbn(sbn).build()
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(false)
+ whenever(assistantFeedbackController.getInlineDescriptionResource(any()))
+ .thenReturn(R.string.notification_channel_summary_automatic)
+ }
+
+ @Test
+ fun testBindNotification_SetsTextApplicationName() {
+ whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("App Name")
+ bindNotification()
+ val textView = underTest.findViewById<TextView>(R.id.pkg_name)
+ assertThat(textView.text.toString()).contains("App Name")
+ assertThat(underTest.findViewById<View>(R.id.header).visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_SetsPackageIcon() {
+ val iconDrawable = mock<Drawable>()
+ whenever(mockPackageManager.getApplicationIcon(any<ApplicationInfo>()))
+ .thenReturn(iconDrawable)
+ bindNotification()
+ val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon)
+ assertThat(iconView.drawable).isEqualTo(iconDrawable)
+ }
+
+ @Test
+ fun testBindNotification_noDelegate() {
+ bindNotification()
+ val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
+ assertThat(nameView.visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_delegate() {
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ "other",
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ Notification(),
+ UserHandle.CURRENT,
+ null,
+ 0,
+ )
+ val applicationInfo = ApplicationInfo()
+ applicationInfo.uid = 7 // non-zero
+ whenever(mockPackageManager.getApplicationInfo(eq("other"), anyInt()))
+ .thenReturn(applicationInfo)
+ whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("Other")
+
+ val entry = NotificationEntryBuilder().setSbn(sbn).build()
+ bindNotification(entry = entry)
+ val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
+ assertThat(nameView.visibility).isEqualTo(VISIBLE)
+ assertThat(nameView.text.toString()).contains("Proxied")
+ }
+
+ @Test
+ fun testBindNotification_GroupNameHiddenIfNoGroup() {
+ bindNotification()
+ val groupNameView = underTest.findViewById<TextView>(R.id.group_name)
+ assertThat(groupNameView.visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_SetsGroupNameIfNonNull() {
+ notificationChannel.group = "test_group_id"
+ val notificationChannelGroup = NotificationChannelGroup("test_group_id", "Test Group Name")
+ whenever(
+ mockINotificationManager.getNotificationChannelGroupForPackage(
+ eq("test_group_id"),
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ )
+ )
+ .thenReturn(notificationChannelGroup)
+ bindNotification()
+ val groupNameView = underTest.findViewById<TextView>(R.id.group_name)
+ assertThat(groupNameView.visibility).isEqualTo(VISIBLE)
+ assertThat(groupNameView.text).isEqualTo("Test Group Name")
+ }
+
+ @Test
+ fun testBindNotification_SetsTextChannelName() {
+ bindNotification()
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.text).isEqualTo(TEST_CHANNEL_NAME)
+ }
+
+ @Test
+ fun testBindNotification_DefaultChannelDoesNotUseChannelName() {
+ bindNotification(notificationChannel = defaultNotificationChannel)
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist() {
+ // Package has more than one channel by default.
+ whenever(
+ mockINotificationManager.getNumNotificationChannelsForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(10)
+ bindNotification(notificationChannel = defaultNotificationChannel)
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_UnblockablePackageUsesChannelName() {
+ bindNotification(isNonblockable = true)
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_SetsOnClickListenerForSettings() {
+ val latch = CountDownLatch(1)
+ bindNotification(
+ onSettingsClick = { _: View?, c: NotificationChannel?, _: Int ->
+ assertThat(c).isEqualTo(notificationChannel)
+ latch.countDown()
+ }
+ )
+
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ settingsButton.performClick()
+ // Verify that listener was triggered.
+ assertThat(latch.count).isEqualTo(0)
+ }
+
+ @Test
+ fun testBindNotification_SettingsButtonInvisibleWhenNoClickListener() {
+ bindNotification()
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ assertThat(settingsButton.visibility != VISIBLE).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() {
+ bindNotification(
+ onSettingsClick = { _: View?, c: NotificationChannel?, _: Int ->
+ assertThat(c).isEqualTo(notificationChannel)
+ },
+ isDeviceProvisioned = false,
+ )
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ assertThat(settingsButton.visibility != VISIBLE).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_SettingsButtonReappearsAfterSecondBind() {
+ bindNotification()
+ bindNotification(onSettingsClick = { _: View?, _: NotificationChannel?, _: Int -> })
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ assertThat(settingsButton.visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_whenAppUnblockable() {
+ bindNotification(isNonblockable = true)
+ val view = underTest.findViewById<TextView>(R.id.non_configurable_text)
+ assertThat(view.visibility).isEqualTo(VISIBLE)
+ assertThat(view.text).isEqualTo(mContext.getString(R.string.notification_unblockable_desc))
+ assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
+ .isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_whenCurrentlyInCall() {
+ whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true)
+
+ val person = Person.Builder().setName("caller").build()
+ val nb =
+ Notification.Builder(mContext, notificationChannel.id)
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(Notification.CallStyle.forOngoingCall(person, mock<PendingIntent>()))
+ .setFullScreenIntent(mock<PendingIntent>(), true)
+ .addAction(Notification.Action.Builder(null, "test", null).build())
+
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ TEST_PACKAGE_NAME,
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ nb.build(),
+ UserHandle.getUserHandleForUid(TEST_UID),
+ null,
+ 0,
+ )
+ entry.sbn = sbn
+ bindNotification()
+ val view = underTest.findViewById<TextView>(R.id.non_configurable_call_text)
+ assertThat(view.visibility).isEqualTo(VISIBLE)
+ assertThat(view.text)
+ .isEqualTo(mContext.getString(R.string.notification_unblockable_call_desc))
+ assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
+ .isEqualTo(GONE)
+ assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility)
+ .isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_whenCurrentlyInCall_notCall() {
+ whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true)
+
+ val nb =
+ Notification.Builder(mContext, notificationChannel.id)
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setFullScreenIntent(mock<PendingIntent>(), true)
+ .addAction(Notification.Action.Builder(null, "test", null).build())
+
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ TEST_PACKAGE_NAME,
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ nb.build(),
+ UserHandle.getUserHandleForUid(TEST_UID),
+ null,
+ 0,
+ )
+ entry.sbn = sbn
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.non_configurable_call_text).visibility)
+ .isEqualTo(GONE)
+ assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
+ .isEqualTo(VISIBLE)
+ assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility)
+ .isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_automaticIsVisible() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(VISIBLE)
+ assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility)
+ .isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_automaticIsGone() {
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(GONE)
+ assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_automaticIsSelected() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.automatic).isSelected).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_alertIsSelected() {
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.alert).isSelected).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_silenceIsSelected() {
+ bindNotification(wasShownHighPriority = false)
+ assertThat(underTest.findViewById<View>(R.id.silence).isSelected).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_DoesNotUpdateNotificationChannel() {
+ bindNotification()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testBindNotification_LogsOpen() {
+ bindNotification()
+ assertThat(uiEventLogger.numLogs()).isEqualTo(1)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ }
+
+ @Test
+ fun testDoesNotUpdateNotificationChannelAfterImportanceChanged() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.automatic).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testHandleCloseControls_persistAutomatic() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
+ bindNotification()
+
+ underTest.handleCloseControls(true, false)
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged() {
+ val originalImportance = notificationChannel.importance
+ bindNotification()
+
+ underTest.handleCloseControls(true, false)
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ assertThat(notificationChannel.importance).isEqualTo(originalImportance)
+
+ assertThat(uiEventLogger.numLogs()).isEqualTo(2)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged.
+ assertThat(uiEventLogger.eventId(1))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id)
+ }
+
+ @Test
+ fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED
+ bindNotification()
+
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ assertThat(notificationChannel.importance)
+ .isEqualTo(NotificationManager.IMPORTANCE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testSilenceCallsUpdateNotificationChannel() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW)
+
+ assertThat(uiEventLogger.numLogs()).isEqualTo(2)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ assertThat(uiEventLogger.eventId(1))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testUnSilenceCallsUpdateNotificationChannel() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testAutomaticUnlocksUserImportance() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ notificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.automatic).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any())
+ assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testSilenceCallsUpdateNotificationChannel_channelImportanceMin() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_MIN
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.silence).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_MIN)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ @Throws(RemoteException::class)
+ fun testSilence_closeGutsThenTryToSave() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.handleCloseControls(false, false)
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+
+ assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testAlertCallsUpdateNotificationChannel_channelImportanceMin() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_MIN
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.alert).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_ok_button))
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testAdjustImportanceTemporarilyAllowsReordering() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ verify(onUserInteractionCallback).onImportanceChanged(entry)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testDoneText() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.alert).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_ok_button))
+ underTest.findViewById<View>(R.id.silence).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ }
+
+ @Test
+ fun testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testCloseControlsDoesNotUpdateIfSaveIsFalse() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(false, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ eq(notificationChannel),
+ )
+
+ assertThat(uiEventLogger.numLogs()).isEqualTo(1)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ }
+
+ @Test
+ fun testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ eq(notificationChannel),
+ )
+
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ eq(notificationChannel),
+ )
+ }
+
+ @Test
+ fun testCloseControls_withoutHittingApply() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testWillBeRemovedReturnsFalse() {
+ assertThat(underTest.willBeRemoved()).isFalse()
+
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat(underTest.willBeRemoved()).isFalse()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ @Throws(Exception::class)
+ fun testBindNotification_HidesFeedbackLink_flagOff() {
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ @Throws(RemoteException::class)
+ fun testBindNotification_SetsFeedbackLink_isReservedChannel() {
+ entry.setRanking(RankingBuilder(entry.ranking).setSummarization("something").build())
+ val latch = CountDownLatch(1)
+ bindNotification(
+ notificationChannel = classifiedNotificationChannel,
+ onFeedbackClickListener = { _: View?, _: Intent? -> latch.countDown() },
+ wasShownHighPriority = false,
+ )
+
+ val feedback: View = underTest.findViewById(R.id.feedback)
+ assertThat(feedback.visibility).isEqualTo(VISIBLE)
+ feedback.performClick()
+ // Verify that listener was triggered.
+ assertThat(latch.count).isEqualTo(0)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ @Throws(Exception::class)
+ fun testBindNotification_hidesFeedbackLink_notReservedChannel() {
+ bindNotification()
+
+ assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE)
+ }
+
+ private fun bindNotification(
+ pm: PackageManager = this.mockPackageManager,
+ iNotificationManager: INotificationManager = this.mockINotificationManager,
+ onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback,
+ channelEditorDialogController: ChannelEditorDialogController =
+ this.channelEditorDialogController,
+ pkg: String = TEST_PACKAGE_NAME,
+ notificationChannel: NotificationChannel = this.notificationChannel,
+ entry: NotificationEntry = this.entry,
+ onSettingsClick: NotificationInfo.OnSettingsClickListener? = null,
+ onAppSettingsClick: NotificationInfo.OnAppSettingsClickListener? = null,
+ onFeedbackClickListener: NotificationInfo.OnFeedbackClickListener? = null,
+ uiEventLogger: UiEventLogger = this.uiEventLogger,
+ isDeviceProvisioned: Boolean = true,
+ isNonblockable: Boolean = false,
+ wasShownHighPriority: Boolean = true,
+ assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController,
+ metricsLogger: MetricsLogger = kosmos.metricsLogger,
+ onCloseClick: View.OnClickListener? = null,
+ ) {
+ underTest.bindNotification(
+ pm,
+ iNotificationManager,
+ onUserInteractionCallback,
+ channelEditorDialogController,
+ pkg,
+ notificationChannel,
+ entry,
+ onSettingsClick,
+ onAppSettingsClick,
+ onFeedbackClickListener,
+ uiEventLogger,
+ isDeviceProvisioned,
+ isNonblockable,
+ wasShownHighPriority,
+ assistantFeedbackController,
+ metricsLogger,
+ onCloseClick,
+ )
+ }
+
+ companion object {
+ private const val TEST_PACKAGE_NAME = "test_package"
+ private const val TEST_SYSTEM_PACKAGE_NAME = PrintManager.PRINT_SPOOLER_PACKAGE_NAME
+ private const val TEST_UID = 1
+ private const val TEST_CHANNEL = "test_channel"
+ private const val TEST_CHANNEL_NAME = "TEST CHANNEL NAME"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
index 8eea2a8e6121..048028cdc0fa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorTest.kt
@@ -21,52 +21,44 @@ 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.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
-import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-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.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.statusbar.lockscreenShadeTransitionController
+import com.android.systemui.statusbar.phone.screenOffAnimationController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.isNull
import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
class NotificationShelfInteractorTest : SysuiTestCase() {
- private val keyguardRepository = FakeKeyguardRepository()
- private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-
- private val screenOffAnimationController =
- mock<ScreenOffAnimationController>().also {
- whenever(it.allowWakeUpIfDozing()).thenReturn(true)
+ private val kosmos =
+ Kosmos().apply {
+ testCase = this@NotificationShelfInteractorTest
+ lockscreenShadeTransitionController = mock()
+ screenOffAnimationController = mock()
+ statusBarStateController = mock()
+ whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
}
- private val statusBarStateController: StatusBarStateController = mock()
- private val powerRepository = FakePowerRepository()
- private val powerInteractor =
- PowerInteractorFactory.create(
- repository = powerRepository,
- screenOffAnimationController = screenOffAnimationController,
- statusBarStateController = statusBarStateController,
- )
- .powerInteractor
-
- private val keyguardTransitionController: LockscreenShadeTransitionController = mock()
- private val underTest =
- NotificationShelfInteractor(
- keyguardRepository,
- deviceEntryFaceAuthRepository,
- powerInteractor,
- keyguardTransitionController,
- )
+ private val underTest = kosmos.notificationShelfInteractor
+
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
+
+ private val statusBarStateController = kosmos.statusBarStateController
+ private val powerRepository = kosmos.fakePowerRepository
+ private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController
@Test
fun shelfIsNotStatic_whenKeyguardNotShowing() = runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index e2fb3ba11a02..d570f18e35d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -19,73 +19,53 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel
import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.runTest
-import com.android.systemui.statusbar.LockscreenShadeTransitionController
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-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.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.shade.domain.interactor.enableDualShade
+import com.android.systemui.shade.domain.interactor.enableSingleShade
+import com.android.systemui.shade.domain.interactor.enableSplitShade
+import com.android.systemui.statusbar.lockscreenShadeTransitionController
+import com.android.systemui.statusbar.phone.screenOffAnimationController
import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito
import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
class NotificationShelfViewModelTest : SysuiTestCase() {
- @Component(modules = [SysUITestModule::class, ActivatableNotificationViewModelModule::class])
- @SysUISingleton
- interface TestComponent : SysUITestComponent<NotificationShelfViewModel> {
-
- val deviceEntryFaceAuthRepository: FakeDeviceEntryFaceAuthRepository
- val keyguardRepository: FakeKeyguardRepository
- val powerRepository: FakePowerRepository
-
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- mocks: TestMocksModule,
- ): TestComponent
+ private val kosmos =
+ Kosmos().apply {
+ testCase = this@NotificationShelfViewModelTest
+ lockscreenShadeTransitionController = mock()
+ screenOffAnimationController = mock()
+ statusBarStateController = mock()
+ whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true)
}
- }
-
- private val keyguardTransitionController: LockscreenShadeTransitionController = mock()
- private val screenOffAnimationController: ScreenOffAnimationController = mock {
- whenever(allowWakeUpIfDozing()).thenReturn(true)
- }
- private val statusBarStateController: SysuiStatusBarStateController = mock()
-
- private val testComponent: TestComponent =
- DaggerNotificationShelfViewModelTest_TestComponent.factory()
- .create(
- test = this,
- mocks =
- TestMocksModule(
- lockscreenShadeTransitionController = keyguardTransitionController,
- screenOffAnimationController = screenOffAnimationController,
- statusBarStateController = statusBarStateController,
- )
- )
+ private val underTest = kosmos.notificationShelfViewModel
+ private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController
+ private val powerRepository = kosmos.fakePowerRepository
@Test
fun canModifyColorOfNotifications_whenKeyguardNotShowing() =
- testComponent.runTest {
+ kosmos.runTest {
val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
keyguardRepository.setKeyguardShowing(false)
@@ -95,7 +75,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
@Test
fun canModifyColorOfNotifications_whenKeyguardShowingAndNotBypass() =
- testComponent.runTest {
+ kosmos.runTest {
val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
keyguardRepository.setKeyguardShowing(true)
@@ -106,7 +86,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
@Test
fun cannotModifyColorOfNotifications_whenBypass() =
- testComponent.runTest {
+ kosmos.runTest {
val canModifyNotifColor by collectLastValue(underTest.canModifyColorOfNotifications)
keyguardRepository.setKeyguardShowing(true)
@@ -117,7 +97,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
@Test
fun isClickable_whenKeyguardShowing() =
- testComponent.runTest {
+ kosmos.runTest {
val isClickable by collectLastValue(underTest.isClickable)
keyguardRepository.setKeyguardShowing(true)
@@ -127,7 +107,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
@Test
fun isNotClickable_whenKeyguardNotShowing() =
- testComponent.runTest {
+ kosmos.runTest {
val isClickable by collectLastValue(underTest.isClickable)
keyguardRepository.setKeyguardShowing(false)
@@ -137,7 +117,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
@Test
fun onClicked_goesToLockedShade() =
- with(testComponent) {
+ kosmos.runTest {
whenever(statusBarStateController.isDozing).thenReturn(true)
underTest.onShelfClicked()
@@ -146,4 +126,48 @@ class NotificationShelfViewModelTest : SysuiTestCase() {
assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
verify(keyguardTransitionController).goToLockedShade(Mockito.isNull(), eq(true))
}
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_splitShade_true() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableSplitShade()
+
+ assertThat(isShelfAlignedToEnd).isTrue()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_singleShade_false() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableSingleShade()
+
+ assertThat(isShelfAlignedToEnd).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_dualShade_wideScreen_false() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableDualShade(wideLayout = true)
+
+ assertThat(isShelfAlignedToEnd).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun isAlignedToEnd_dualShade_narrowScreen_false() =
+ kosmos.runTest {
+ val isShelfAlignedToEnd by collectLastValue(underTest.isAlignedToEnd)
+
+ kosmos.enableDualShade(wideLayout = false)
+
+ assertThat(isShelfAlignedToEnd).isFalse()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
index d14ff35f824a..e5cb0fbc9e4b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
@@ -49,6 +49,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
private val sectionsManager = mock<NotificationSectionsManager>()
private val msdlPlayer = kosmos.fakeMSDLPlayer
private var canRowBeDismissed = true
+ private var magneticAnimationsCancelled = false
private val underTest = kosmos.magneticNotificationRowManagerImpl
@@ -64,6 +65,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
children = notificationTestHelper.createGroup(childrenNumber).childrenContainer
swipedRow = children.attachedChildren[childrenNumber / 2]
configureMagneticRowListener(swipedRow)
+ magneticAnimationsCancelled = false
}
@Test
@@ -247,6 +249,35 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
assertThat(underTest.currentState).isEqualTo(State.IDLE)
}
+ @Test
+ fun onMagneticInteractionEnd_whenDetached_cancelsMagneticAnimations() =
+ kosmos.testScope.runTest {
+ // GIVEN the swiped row is detached
+ setDetachedState()
+
+ // WHEN the interaction ends on the row
+ underTest.onMagneticInteractionEnd(swipedRow, velocity = null)
+
+ // THEN magnetic animations are cancelled
+ assertThat(magneticAnimationsCancelled).isTrue()
+ }
+
+ @Test
+ fun onMagneticInteractionEnd_forMagneticNeighbor_cancelsMagneticAnimations() =
+ kosmos.testScope.runTest {
+ val neighborRow = children.attachedChildren[childrenNumber / 2 - 1]
+ configureMagneticRowListener(neighborRow)
+
+ // GIVEN that targets are set
+ setTargets()
+
+ // WHEN the interactionEnd is called on a target different from the swiped row
+ underTest.onMagneticInteractionEnd(neighborRow, null)
+
+ // THEN magnetic animations are cancelled
+ assertThat(magneticAnimationsCancelled).isTrue()
+ }
+
private fun setDetachedState() {
val threshold = 100f
underTest.setSwipeThresholdPx(threshold)
@@ -284,7 +315,11 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
startVelocity: Float,
) {}
- override fun cancelMagneticAnimations() {}
+ override fun cancelMagneticAnimations() {
+ magneticAnimationsCancelled = true
+ }
+
+ override fun cancelTranslationAnimations() {}
override fun canRowBeDismissed(): Boolean = canRowBeDismissed
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 766ae73cb49d..789701f5e4b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -405,7 +405,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0);
mSwipeHelper.snapChild(mNotificationRow, 0, 0);
- verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
+ verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0);
verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
}
@@ -416,7 +416,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0);
mSwipeHelper.snapChild(mNotificationRow, 10, 0);
- verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
+ verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0);
verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
}
@@ -426,7 +426,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
mSwipeHelper.snapChild(mView, 10, 0);
- verify(mCallback).onDragCancelledWithVelocity(mView, 0);
+ verify(mCallback).onDragCancelled(mView);
verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0);
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 2e12336f6e93..6f785a3731e1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone;
import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
import static org.junit.Assert.assertFalse;
@@ -76,6 +77,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.ui.BouncerView;
import com.android.systemui.bouncer.ui.BouncerViewDelegate;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dreams.DreamOverlayStateController;
@@ -171,6 +173,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
@Mock private SceneInteractor mSceneInteractor;
@Mock private DismissCallbackRegistry mDismissCallbackRegistry;
@Mock private BouncerInteractor mBouncerInteractor;
+ @Mock private CommunalSceneInteractor mCommunalSceneInteractor;
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
@@ -209,6 +212,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
.thenReturn(mNotificationShadeWindowView);
when(mNotificationShadeWindowView.getWindowInsetsController())
.thenReturn(mWindowInsetsController);
+ when(mCommunalSceneInteractor.isIdleOnCommunal()).thenReturn(MutableStateFlow(false));
mStatusBarKeyguardViewManager =
new StatusBarKeyguardViewManager(
@@ -245,7 +249,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
mExecutor,
() -> mDeviceEntryInteractor,
mDismissCallbackRegistry,
- () -> mBouncerInteractor) {
+ () -> mBouncerInteractor,
+ mCommunalSceneInteractor) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -749,7 +754,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
mExecutor,
() -> mDeviceEntryInteractor,
mDismissCallbackRegistry,
- () -> mBouncerInteractor) {
+ () -> mBouncerInteractor,
+ mCommunalSceneInteractor) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 73c191b32393..c48287c32120 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor
import android.app.PendingIntent
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -30,12 +31,12 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -50,6 +51,7 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(StatusBarChipsModernization.FLAG_NAME)
class OngoingCallInteractorTest : SysuiTestCase() {
private val kosmos = Kosmos().useUnconfinedTestDispatcher()
private val repository = kosmos.activeNotificationListRepository
@@ -76,21 +78,13 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val testIntent: PendingIntent = mock()
val testPromotedContent =
PromotedNotificationContentModel.Builder("promotedCall").build()
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "promotedCall",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- statusBarChipIcon = testIconView,
- contentIntent = testIntent,
- promotedContent = testPromotedContent,
- )
- )
- }
- .build()
+ addOngoingCallState(
+ key = "promotedCall",
+ startTimeMs = 1000L,
+ statusBarChipIconView = testIconView,
+ contentIntent = testIntent,
+ promotedContent = testPromotedContent,
+ )
// Verify model is InCall and has the correct icon, intent, and promoted content.
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
@@ -101,45 +95,13 @@ class OngoingCallInteractorTest : SysuiTestCase() {
}
@Test
- fun ongoingCallNotification_emitsInCall() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.ongoingCallState)
-
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- )
- )
- }
- .build()
-
- assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
- }
-
- @Test
fun notificationRemoved_emitsNoCall() =
kosmos.runTest {
val latest by collectLastValue(underTest.ongoingCallState)
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- )
- )
- }
- .build()
-
- repository.activeNotifications.value = ActiveNotificationsStore()
+ addOngoingCallState(key = "testKey")
+ removeOngoingCallState(key = "testKey")
+
assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@@ -149,19 +111,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
}
@@ -172,19 +122,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
val latest by collectLastValue(underTest.ongoingCallState)
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
}
@@ -196,19 +134,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Start with notification and app not visible
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
// App becomes visible
@@ -234,7 +160,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
.ongoingProcessRequiresStatusBarVisible
)
- postOngoingCallNotification()
+ addOngoingCallState()
assertThat(isStatusBarRequired).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -256,9 +182,9 @@ class OngoingCallInteractorTest : SysuiTestCase() {
.ongoingProcessRequiresStatusBarVisible
)
- postOngoingCallNotification()
+ addOngoingCallState(key = "testKey")
- repository.activeNotifications.value = ActiveNotificationsStore()
+ removeOngoingCallState(key = "testKey")
assertThat(isStatusBarRequired).isFalse()
assertThat(requiresStatusBarVisibleInRepository).isFalse()
@@ -283,7 +209,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- postOngoingCallNotification()
+ addOngoingCallState(uid = UID)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -305,7 +231,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
// Set up notification but not in fullscreen
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
- postOngoingCallNotification()
+ addOngoingCallState()
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
verify(kosmos.swipeStatusBarAwayGestureHandler, never())
@@ -319,7 +245,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- postOngoingCallNotification()
+ addOngoingCallState()
assertThat(isGestureListeningEnabled).isTrue()
verify(kosmos.swipeStatusBarAwayGestureHandler)
@@ -333,7 +259,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- postOngoingCallNotification()
+ addOngoingCallState()
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
@@ -360,7 +286,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
)
// Start with an ongoing call (which should set status bar required)
- postOngoingCallNotification()
+ addOngoingCallState()
assertThat(isStatusBarRequiredForOngoingCall).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -374,22 +300,6 @@ class OngoingCallInteractorTest : SysuiTestCase() {
assertThat(requiresStatusBarVisibleInWindowController).isFalse()
}
- private fun postOngoingCallNotification() {
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
- }
-
companion object {
private const val UID = 885
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
index fecf1fd2f222..354edac75452 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt
@@ -20,9 +20,12 @@ import android.content.Context
import android.content.res.Resources
import android.hardware.devicestate.DeviceStateManager
import android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD
+import android.os.PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.R
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractorImpl
@@ -44,8 +47,10 @@ import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_OFF
import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.COOL_DOWN_DURATION
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_CLOSED
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.FOLDABLE_DEVICE_STATE_HALF_OPEN
+import com.android.systemui.unfold.DisplaySwitchLatencyTracker.Companion.SCREEN_EVENT_TIMEOUT
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
@@ -56,11 +61,13 @@ import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -73,6 +80,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -88,6 +96,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
private val animationStatusRepository = kosmos.fakeAnimationStatusRepository
private val keyguardInteractor = mock<KeyguardInteractor>()
private val displaySwitchLatencyLogger = mock<DisplaySwitchLatencyLogger>()
+ private val latencyTracker = mock<LatencyTracker>()
private val deviceStateManager = kosmos.deviceStateManager
private val closedDeviceState = kosmos.foldedDeviceStateList.first()
@@ -142,6 +151,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
displaySwitchLatencyLogger,
systemClock,
deviceStateManager,
+ latencyTracker,
)
}
@@ -195,6 +205,7 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
displaySwitchLatencyLogger,
systemClock,
deviceStateManager,
+ latencyTracker,
)
displaySwitchLatencyTracker.start()
@@ -370,6 +381,256 @@ class DisplaySwitchLatencyTrackerTest : SysuiTestCase() {
}
}
+ @Test
+ fun unfoldingDevice_startsLatencyTracking() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+
+ verify(latencyTracker).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun foldingDevice_doesntTrackLatency() {
+ testScope.runTest {
+ setDeviceState(UNFOLDED)
+ displaySwitchLatencyTracker.start()
+ runCurrent()
+
+ startFolding()
+
+ verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun foldedState_doesntStartTrackingOnScreenOn() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ powerInteractor.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+
+ verify(latencyTracker, never()).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun unfoldingDevice_endsLatencyTrackingWhenTransitionStarts() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ unfoldTransitionProgressProvider.onTransitionStarted()
+ runCurrent()
+
+ verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenScreenOn() {
+ testScope.runTest {
+ animationStatusRepository.onAnimationStatusChanged(enabled = false)
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ powerInteractor.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+
+ verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun unfoldingDevice_doesntEndLatencyTrackingWhenScreenOn() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ powerInteractor.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+
+ verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun unfoldingDevice_animationsDisabled_endsLatencyTrackingWhenDeviceGoesToSleep() {
+ testScope.runTest {
+ animationStatusRepository.onAnimationStatusChanged(enabled = false)
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ powerInteractor.setAsleepForTest(sleepReason = GO_TO_SLEEP_REASON_POWER_BUTTON)
+ runCurrent()
+
+ verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun displaySwitchInterrupted_cancelsTrackingWhenNewDeviceStateEmitted() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ finishFolding()
+
+ verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+ verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun displaySwitchInterrupted_cancelsTrackingForManyStateChanges() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ startUnfolding()
+ startFolding()
+ startUnfolding()
+ finishUnfolding()
+
+ verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+ verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun displaySwitchInterrupted_startsOneTrackingForManyStateChanges() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ startUnfolding()
+ startFolding()
+ startUnfolding()
+
+ verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun interruptedDisplaySwitchFinished_inCoolDownPeriod_trackingDisabled() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ finishFolding()
+
+ advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds))
+ startUnfolding()
+ finishUnfolding()
+
+ verify(latencyTracker, times(1)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun interruptedDisplaySwitchFinished_coolDownPassed_trackingWorksAsUsual() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ finishFolding()
+
+ advanceTimeBy(COOL_DOWN_DURATION.plus(10.milliseconds))
+ startUnfolding()
+ finishUnfolding()
+
+ verify(latencyTracker, times(2)).onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ verify(latencyTracker).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun displaySwitchInterrupted_coolDownExtendedByStartEvents() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ advanceTimeBy(COOL_DOWN_DURATION.minus(10.milliseconds))
+ startUnfolding()
+ advanceTimeBy(20.milliseconds)
+
+ startFolding()
+ finishUnfolding()
+
+ verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun displaySwitchInterrupted_coolDownExtendedByAnyEndEvent() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ startFolding()
+ startUnfolding()
+ advanceTimeBy(COOL_DOWN_DURATION - 10.milliseconds)
+ powerInteractor.setScreenPowerState(SCREEN_ON)
+ advanceTimeBy(20.milliseconds)
+
+ startFolding()
+ finishUnfolding()
+
+ verify(latencyTracker, never()).onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ @Test
+ fun displaySwitchTimedOut_trackingCancelled() {
+ testScope.runTest {
+ startInFoldedState(displaySwitchLatencyTracker)
+
+ startUnfolding()
+ advanceTimeBy(SCREEN_EVENT_TIMEOUT + 10.milliseconds)
+ finishUnfolding()
+
+ verify(latencyTracker).onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ }
+
+ private suspend fun TestScope.startInFoldedState(tracker: DisplaySwitchLatencyTracker) {
+ setDeviceState(FOLDED)
+ tracker.start()
+ runCurrent()
+ }
+
+ private suspend fun TestScope.startUnfolding() {
+ setDeviceState(HALF_FOLDED)
+ powerInteractor.setScreenPowerState(SCREEN_OFF)
+ runCurrent()
+ }
+
+ private suspend fun TestScope.startFolding() {
+ setDeviceState(FOLDED)
+ powerInteractor.setScreenPowerState(SCREEN_OFF)
+ runCurrent()
+ }
+
+ private fun TestScope.finishFolding() {
+ powerInteractor.setScreenPowerState(SCREEN_ON)
+ runCurrent()
+ }
+
+ private fun TestScope.finishUnfolding() {
+ unfoldTransitionProgressProvider.onTransitionStarted()
+ runCurrent()
+ }
+
private suspend fun setDeviceState(state: DeviceState) {
foldStateRepository.emit(state)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 3ca1f5c0dd30..bafa8cf05a7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.user.data.repository
import android.app.admin.devicePolicyManager
+import android.content.Intent
import android.content.pm.UserInfo
import android.internal.statusbar.fakeStatusBarService
import android.os.UserHandle
@@ -27,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
@@ -50,6 +52,8 @@ import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -172,6 +176,39 @@ class UserRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun userUnlockedFlow_tracksBroadcastedChanges() =
+ testScope.runTest {
+ val userHandle: UserHandle = mock()
+ underTest = create(testScope.backgroundScope)
+ val latest by collectLastValue(underTest.isUserUnlocked(userHandle))
+ whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(false)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED),
+ )
+
+ assertThat(latest).isFalse()
+
+ whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(true)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED),
+ )
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun userUnlockedFlow_initialValueReported() =
+ testScope.runTest {
+ val userHandle: UserHandle = mock()
+ underTest = create(testScope.backgroundScope)
+ whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(true)
+ val latest by collectLastValue(underTest.isUserUnlocked(userHandle))
+ assertThat(latest).isTrue()
+ }
+
+ @Test
fun refreshUsers_sortsByCreationTime_guestUserLast() =
testScope.runTest {
underTest = create(testScope.backgroundScope)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index 75f3386ed695..b8e19248b2de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -47,6 +47,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.keyguard.TestScopeProvider;
+import com.android.settingslib.volume.MediaSessions;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -268,13 +269,15 @@ public class VolumeDialogControllerImplTest extends SysuiTestCase {
@Test
public void testOnRemoteVolumeChanged_newStream_noNullPointer() {
MediaSession.Token token = new MediaSession.Token(Process.myUid(), null);
- mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(token, 0);
+ var sessionId = MediaSessions.SessionId.Companion.from(token);
+ mVolumeController.mMediaSessionsCallbacksW.onRemoteVolumeChanged(sessionId, 0);
}
@Test
public void testOnRemoteRemove_newStream_noNullPointer() {
MediaSession.Token token = new MediaSession.Token(Process.myUid(), null);
- mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(token);
+ var sessionId = MediaSessions.SessionId.Companion.from(token);
+ mVolumeController.mMediaSessionsCallbacksW.onRemoteRemoved(sessionId);
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
index 61ee5e04afd9..390518f3e2e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt
@@ -16,8 +16,10 @@
package com.android.systemui.window.ui.viewmodel
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -32,6 +34,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_BOUNCER_UI_REVAMP)
class WindowRootViewModelTest : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
diff --git a/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml
new file mode 100644
index 000000000000..1ba637f379c1
--- /dev/null
+++ b/packages/SystemUI/res/drawable/clipboard_minimized_background_inset.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/clipboard_minimized_background"
+ android:inset="4dp"/> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 91cd019c85d1..43808f215a81 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -149,9 +149,9 @@
style="@style/TextAppearance.AuthCredential.Indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginTop="24dp"
android:layout_marginHorizontal="24dp"
- android:accessibilityLiveRegion="assertive"
+ android:layout_marginTop="24dp"
+ android:accessibilityLiveRegion="polite"
android:fadingEdge="horizontal"
android:gravity="center_horizontal"
android:scrollHorizontally="true"
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 448b3e7d5ea0..915563b1ae20 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -171,12 +171,12 @@
android:layout_height="wrap_content"
android:visibility="gone"
android:elevation="7dp"
- android:padding="8dp"
+ android:padding="12dp"
app:layout_constraintBottom_toTopOf="@id/indication_container"
app:layout_constraintStart_toStartOf="parent"
- android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
- android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
- android:background="@drawable/clipboard_minimized_background">
+ android:layout_marginStart="4dp"
+ android:layout_marginBottom="2dp"
+ android:background="@drawable/clipboard_minimized_background_inset">
<ImageView
android:src="@drawable/ic_content_paste"
android:tint="?attr/overlayButtonTextColor"
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 67f620f6fc54..8ad99abccdfe 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -16,7 +16,7 @@
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/volume_dialog_root"
+ android:id="@+id/volume_dialog"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0"
diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml
index 6748cfa05c35..4e3c8cc4413b 100644
--- a/packages/SystemUI/res/layout/volume_ringer_button.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_button.xml
@@ -13,20 +13,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" >
-
- <ImageButton
- android:id="@+id/volume_drawer_button"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius"
- android:contentDescription="@string/volume_ringer_mode"
- android:gravity="center"
- android:tint="@androidprv:color/materialColorOnSurface"
- android:src="@drawable/volume_ringer_item_bg"
- android:background="@drawable/volume_ringer_item_bg"/>
-
-</FrameLayout>
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_ringer_item_bg"
+ android:contentDescription="@string/volume_ringer_mode"
+ android:gravity="center"
+ android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius"
+ android:src="@drawable/volume_ringer_item_bg"
+ android:tint="@androidprv:color/materialColorOnSurface" />
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 5ef4d4014ba6..7f2c89346423 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -258,7 +258,7 @@
<style name="TextAppearance.AuthNonBioCredential.Title">
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
<item name="android:layout_marginTop">24dp</item>
- <item name="android:textSize">36dp</item>
+ <item name="android:textSize">36sp</item>
<item name="android:focusable">true</item>
<item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
</style>
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index f835ad689132..e2065f175c79 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -352,6 +352,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
&& Math.abs(delta) > Math.abs(deltaPerpendicular)) {
if (mCallback.canChildBeDragged(mTouchedView)) {
mIsSwiping = true;
+ mCallback.setMagneticAndRoundableTargets(mTouchedView);
mCallback.onBeginDrag(mTouchedView);
mInitialTouchPos = getPos(ev);
mTranslation = getTranslation(mTouchedView);
@@ -444,6 +445,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
};
Animator anim = getViewTranslationAnimator(animView, newPos, updateListener);
+ mCallback.onMagneticInteractionEnd(animView, velocity);
if (anim == null) {
onDismissChildWithAnimationFinished();
return;
@@ -733,7 +735,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
dismissChild(mTouchedView, velocity,
!swipedFastEnough() /* useAccelerateInterpolator */);
} else {
- mCallback.onDragCancelledWithVelocity(mTouchedView, velocity);
+ mCallback.onMagneticInteractionEnd(mTouchedView, velocity);
+ mCallback.onDragCancelled(mTouchedView);
snapChild(mTouchedView, 0 /* leftTarget */, velocity);
}
mTouchedView = null;
@@ -935,18 +938,24 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
void onBeginDrag(View v);
+ /**
+ * Set magnetic and roundable targets for a view.
+ */
+ void setMagneticAndRoundableTargets(View v);
+
void onChildDismissed(View v);
void onDragCancelled(View v);
/**
- * A drag operation has been cancelled on a view with a final velocity.
- * @param v View that was dragged.
- * @param finalVelocity Final velocity of the drag.
+ * Notify that a magnetic interaction ended on a view with a velocity.
+ * <p>
+ * This method should be called when a view will snap back or be dismissed.
+ *
+ * @param view The {@link View} whose magnetic interaction ended.
+ * @param velocity The velocity when the interaction ended.
*/
- default void onDragCancelledWithVelocity(View v, float finalVelocity) {
- onDragCancelled(v);
- }
+ void onMagneticInteractionEnd(View view, float velocity);
/**
* Called when the child is long pressed and available to start drag and drop.
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS
index 1ed8c068f974..5a59b7aaef56 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
include /core/java/android/view/accessibility/OWNERS
jonesriley@google.com \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
index b65d29c6a0bb..429b4b0fccab 100644
--- a/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/ailabs/OWNERS
@@ -5,5 +5,4 @@ linyuh@google.com
pauldpong@google.com
praveenj@google.com
vicliang@google.com
-mfolkerts@google.com
yuklimko@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index e365b770c203..d8e7a168ef3c 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -43,6 +43,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.wm.shell.animation.FlingAnimationUtils
@@ -79,6 +80,7 @@ constructor(
private val activityStarter: ActivityStarter,
private val keyguardInteractor: KeyguardInteractor,
private val sceneInteractor: SceneInteractor,
+ private val shadeRepository: ShadeRepository,
private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
) : TouchHandler {
/** An interface for creating ValueAnimators. */
@@ -260,6 +262,8 @@ constructor(
}
scrimManager.addCallback(scrimManagerCallback)
currentScrimController = scrimManager.currentController
+
+ shadeRepository.setLegacyShadeTracking(true)
session.registerCallback {
velocityTracker?.apply { recycle() }
velocityTracker = null
@@ -270,6 +274,7 @@ constructor(
if (!Flags.communalBouncerDoNotModifyPluginOpen()) {
notificationShadeWindowController.setForcePluginOpen(false, this)
}
+ shadeRepository.setLegacyShadeTracking(false)
}
session.registerGestureListener(onGestureListener)
session.registerInputListener { ev: InputEvent -> onMotionEvent(ev) }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index 6cd763a9d3d0..bbf9a19012a4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -31,6 +31,7 @@ import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieProperty
import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.keyguard.KeyguardPINView
import com.android.systemui.CoreStartable
import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
@@ -50,7 +51,6 @@ import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
@SysUISingleton
@@ -65,51 +65,53 @@ constructor(
private val layoutInflater: Lazy<LayoutInflater>,
private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>,
private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
- private val windowManager: Lazy<WindowManager>
+ private val windowManager: Lazy<WindowManager>,
) : CoreStartable {
override fun start() {
- applicationScope
- .launch {
- sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
- if (isSfpsAvailable) {
- combine(
- biometricStatusInteractor.get().sfpsAuthenticationReason,
- deviceEntrySideFpsOverlayInteractor
- .get()
- .showIndicatorForDeviceEntry,
- sideFpsProgressBarViewModel.get().isVisible,
- ::Triple
+ applicationScope.launch {
+ sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
+ if (isSfpsAvailable) {
+ combine(
+ biometricStatusInteractor.get().sfpsAuthenticationReason,
+ deviceEntrySideFpsOverlayInteractor.get().showIndicatorForDeviceEntry,
+ sideFpsProgressBarViewModel.get().isVisible,
+ ::Triple,
+ )
+ .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
+ .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
+ val (
+ systemServerAuthReason,
+ showIndicatorForDeviceEntry,
+ progressBarIsVisible) =
+ combinedFlows
+ Log.d(
+ TAG,
+ "systemServerAuthReason = $systemServerAuthReason, " +
+ "showIndicatorForDeviceEntry = " +
+ "$showIndicatorForDeviceEntry, " +
+ "progressBarIsVisible = $progressBarIsVisible",
)
- .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
- .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
- val (
- systemServerAuthReason,
- showIndicatorForDeviceEntry,
- progressBarIsVisible) =
- combinedFlows
- Log.d(
- TAG,
- "systemServerAuthReason = $systemServerAuthReason, " +
- "showIndicatorForDeviceEntry = " +
- "$showIndicatorForDeviceEntry, " +
- "progressBarIsVisible = $progressBarIsVisible"
- )
- if (!isInRearDisplayMode) {
- if (progressBarIsVisible) {
- hide()
- } else if (systemServerAuthReason != NotRunning) {
- show()
- } else if (showIndicatorForDeviceEntry) {
- show()
- } else {
- hide()
- }
+ if (!isInRearDisplayMode) {
+ if (progressBarIsVisible) {
+ hide()
+ } else if (systemServerAuthReason != NotRunning) {
+ show()
+ } else if (showIndicatorForDeviceEntry) {
+ show()
+ overlayView?.announceForAccessibility(
+ applicationContext.resources.getString(
+ R.string.accessibility_side_fingerprint_indicator_label
+ )
+ )
+ } else {
+ hide()
}
}
- }
+ }
}
}
+ }
}
private var overlayView: View? = null
@@ -119,7 +121,7 @@ constructor(
if (overlayView?.isAttachedToWindow == true) {
Log.d(
TAG,
- "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request"
+ "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request",
)
return
}
@@ -137,11 +139,6 @@ constructor(
overlayView!!.visibility = View.INVISIBLE
Log.d(TAG, "show(): adding overlayView $overlayView")
windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
- overlayView!!.announceForAccessibility(
- applicationContext.resources.getString(
- R.string.accessibility_side_fingerprint_indicator_label
- )
- )
}
/** Hide the side fingerprint sensor indicator */
@@ -163,7 +160,7 @@ constructor(
fun bind(
overlayView: View,
viewModel: SideFpsOverlayViewModel,
- windowManager: WindowManager
+ windowManager: WindowManager,
) {
overlayView.repeatWhenAttached {
val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
@@ -186,7 +183,7 @@ constructor(
object : View.AccessibilityDelegate() {
override fun dispatchPopulateAccessibilityEvent(
host: View,
- event: AccessibilityEvent
+ event: AccessibilityEvent,
): Boolean {
return if (
event.getEventType() ===
diff --git a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
index 2e1b5ad177b5..e456310febfd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
+++ b/packages/SystemUI/src/com/android/systemui/communal/DeviceInactiveCondition.java
@@ -17,16 +17,19 @@
package com.android.systemui.communal;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
-import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.shared.model.DozeStateModel;
import com.android.systemui.shared.condition.Condition;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.kotlin.JavaAdapter;
import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
import javax.inject.Inject;
@@ -38,6 +41,10 @@ public class DeviceInactiveCondition extends Condition {
private final KeyguardStateController mKeyguardStateController;
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final KeyguardInteractor mKeyguardInteractor;
+ private final JavaAdapter mJavaAdapter;
+ private Job mAnyDozeListenerJob;
+ private boolean mAnyDoze;
private final KeyguardStateController.Callback mKeyguardStateCallback =
new KeyguardStateController.Callback() {
@Override
@@ -63,12 +70,14 @@ public class DeviceInactiveCondition extends Condition {
@Inject
public DeviceInactiveCondition(@Application CoroutineScope scope,
KeyguardStateController keyguardStateController,
- WakefulnessLifecycle wakefulnessLifecycle,
- KeyguardUpdateMonitor keyguardUpdateMonitor) {
+ WakefulnessLifecycle wakefulnessLifecycle, KeyguardUpdateMonitor keyguardUpdateMonitor,
+ KeyguardInteractor keyguardInteractor, JavaAdapter javaAdapter) {
super(scope);
mKeyguardStateController = keyguardStateController;
mWakefulnessLifecycle = wakefulnessLifecycle;
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ mKeyguardInteractor = keyguardInteractor;
+ mJavaAdapter = javaAdapter;
}
@Override
@@ -77,6 +86,11 @@ public class DeviceInactiveCondition extends Condition {
mKeyguardStateController.addCallback(mKeyguardStateCallback);
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
+ mAnyDozeListenerJob = mJavaAdapter.alwaysCollectFlow(
+ mKeyguardInteractor.getDozeTransitionModel(), dozeModel -> {
+ mAnyDoze = !DozeStateModel.Companion.isDozeOff(dozeModel.getTo());
+ updateState();
+ });
}
@Override
@@ -84,6 +98,7 @@ public class DeviceInactiveCondition extends Condition {
mKeyguardStateController.removeCallback(mKeyguardStateCallback);
mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateCallback);
mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
+ mAnyDozeListenerJob.cancel(null);
}
@Override
@@ -92,10 +107,10 @@ public class DeviceInactiveCondition extends Condition {
}
private void updateState() {
- final boolean asleep =
- mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
- || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_GOING_TO_SLEEP;
- updateCondition(asleep || mKeyguardStateController.isShowing()
- || mKeyguardUpdateMonitor.isDreaming());
+ final boolean asleep = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP;
+ // Doze/AoD is also a dream, but we should never override it with low light as to the user
+ // it's totally unrelated.
+ updateCondition(!mAnyDoze && (asleep || mKeyguardStateController.isShowing()
+ || mKeyguardUpdateMonitor.isDreaming()));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index 3907a37cd5d9..f304b97a4ffe 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -37,11 +37,13 @@ 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.user.domain.interactor.UserLockedInteractor
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
/**
* Callback that will be invoked when the Room database is created. Then the database will be
@@ -57,6 +59,7 @@ constructor(
@Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>,
@CommunalLog logBuffer: LogBuffer,
private val userManager: UserManager,
+ private val userLockedInteractor: UserLockedInteractor,
) : RoomDatabase.Callback() {
companion object {
private const val TAG = "DefaultWidgetPopulation"
@@ -79,38 +82,36 @@ constructor(
}
bgScope.launch {
- // Default widgets should be associated with the main user.
- val user = userManager.mainUser
-
- if (user == null) {
- logger.w(
- "Skipped populating default widgets. Reason: device does not have a main user"
- )
- return@launch
- }
+ userLockedInteractor.isUserUnlocked(userManager.mainUser).first { it }
+ populateDefaultWidgets()
+ }
+ }
- val userSerialNumber = userManager.getUserSerialNumber(user.identifier)
-
- defaultWidgets.forEachIndexed { index, name ->
- val provider = ComponentName.unflattenFromString(name)
- provider?.let {
- val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
- id?.let {
- communalWidgetDaoProvider
- .get()
- .addWidget(
- widgetId = id,
- componentName = name,
- rank = index,
- userSerialNumber = userSerialNumber,
- spanY = SpanValue.Fixed(3),
- )
- }
+ private fun populateDefaultWidgets() {
+ // Default widgets should be associated with the main user.
+ val user = userManager.mainUser ?: return
+
+ val userSerialNumber = userManager.getUserSerialNumber(user.identifier)
+
+ defaultWidgets.forEachIndexed { index, name ->
+ val provider = ComponentName.unflattenFromString(name)
+ provider?.let {
+ val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
+ id?.let {
+ communalWidgetDaoProvider
+ .get()
+ .addWidget(
+ widgetId = id,
+ componentName = name,
+ rank = index,
+ userSerialNumber = userSerialNumber,
+ spanY = SpanValue.Fixed(3),
+ )
}
}
-
- logger.i("Populated default widgets in the database.")
}
+
+ logger.i("Populated default widgets in the database.")
}
/**
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 de55c92e84f9..6dab32a66c94 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
@@ -69,6 +69,7 @@ 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.statusbar.phone.ManagedProfileController
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.emitOnStart
@@ -127,6 +128,7 @@ constructor(
private val batteryInteractor: BatteryInteractor,
private val dockManager: DockManager,
private val posturingInteractor: PosturingInteractor,
+ private val userLockedInteractor: UserLockedInteractor,
) {
private val logger = Logger(logBuffer, "CommunalInteractor")
@@ -162,7 +164,7 @@ constructor(
val isCommunalAvailable: Flow<Boolean> =
allOf(
communalSettingsInteractor.isCommunalEnabled,
- not(keyguardInteractor.isEncryptedOrLockdown),
+ userLockedInteractor.isUserUnlocked(userManager.mainUser),
keyguardInteractor.isKeyguardShowing,
)
.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
index 29d9cacdbc79..be1d005a0198 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
@@ -20,7 +20,6 @@ import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -60,8 +59,7 @@ constructor(
if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
add(Swipe.Up to bouncerOrGone)
- // "Home" is either Lockscreen, or Gone - if the device is entered.
- add(Swipe.End to SceneFamilies.Home)
+ add(Swipe.End to Scenes.Lockscreen)
addAll(
when (shadeMode) {
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 dec7ba3a3eb1..06a14eaa5c85 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.settings.UserTracker
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
@@ -58,6 +59,7 @@ constructor(
@Main private val uiDispatcher: CoroutineDispatcher,
private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>,
private val glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper,
+ private val userLockedInteractor: UserLockedInteractor,
) : CoreStartable {
private val appWidgetHost by lazy { appWidgetHostLazy.get() }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index fcc3ea9f7d58..fed77090c477 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -18,6 +18,7 @@ package com.android.systemui.dagger
import com.android.keyguard.KeyguardBiometricLockoutLogger
import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.unfoldLatencyTrackingFix
import com.android.systemui.LatencyTester
import com.android.systemui.SliceBroadcastRelayHandler
import com.android.systemui.accessibility.Magnification
@@ -60,6 +61,7 @@ import com.android.systemui.stylus.StylusUsiPowerStartable
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import com.android.systemui.theme.ThemeOverlayController
import com.android.systemui.unfold.DisplaySwitchLatencyTracker
+import com.android.systemui.unfold.NoCooldownDisplaySwitchLatencyTracker
import com.android.systemui.usb.StorageNotification
import com.android.systemui.util.NotificationChannels
import com.android.systemui.util.StartBinderLoggerModule
@@ -67,8 +69,10 @@ import com.android.systemui.wallpapers.dagger.WallpaperModule
import com.android.systemui.wmshell.WMShell
import dagger.Binds
import dagger.Module
+import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
+import javax.inject.Provider
/**
* DEPRECATED: DO NOT ADD THINGS TO THIS FILE.
@@ -148,12 +152,6 @@ abstract class SystemUICoreStartableModule {
@ClassKey(LatencyTester::class)
abstract fun bindLatencyTester(sysui: LatencyTester): CoreStartable
- /** Inject into DisplaySwitchLatencyTracker. */
- @Binds
- @IntoMap
- @ClassKey(DisplaySwitchLatencyTracker::class)
- abstract fun bindDisplaySwitchLatencyTracker(sysui: DisplaySwitchLatencyTracker): CoreStartable
-
/** Inject into NotificationChannels. */
@Binds
@IntoMap
@@ -353,4 +351,15 @@ abstract class SystemUICoreStartableModule {
@IntoMap
@ClassKey(ComplicationTypesUpdater::class)
abstract fun bindComplicationTypesUpdater(updater: ComplicationTypesUpdater): CoreStartable
+
+ companion object {
+ @Provides
+ @IntoMap
+ @ClassKey(DisplaySwitchLatencyTracker::class)
+ fun provideDisplaySwitchLatencyTracker(
+ noCoolDownVariant: Provider<NoCooldownDisplaySwitchLatencyTracker>,
+ coolDownVariant: Provider<DisplaySwitchLatencyTracker>,
+ ): CoreStartable =
+ if (unfoldLatencyTrackingFix()) coolDownVariant.get() else noCoolDownVariant.get()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
index 9ce2ce0100a3..7437d3e215d5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -19,7 +19,6 @@ package com.android.systemui.dreams.ui.viewmodel
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
@@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.map
class DreamUserActionsViewModel
@AssistedInject
constructor(
- private val communalInteractor: CommunalInteractor,
private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
private val shadeInteractor: ShadeInteractor,
private val shadeModeInteractor: ShadeModeInteractor,
@@ -54,14 +52,9 @@ constructor(
} else {
combine(
deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
- communalInteractor.isCommunalAvailable,
shadeModeInteractor.shadeMode,
- ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
+ ) { isDeviceUnlocked, shadeMode ->
buildList {
- if (isCommunalAvailable) {
- add(Swipe.Start to Scenes.Communal)
- }
-
val bouncerOrGone =
if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
add(Swipe.Up to bouncerOrGone)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 23be5c52ab5c..c61530c3dbcc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -163,10 +163,6 @@ constructor(
}
private fun bindJankViewModel() {
- if (SceneContainerFlag.isEnabled) {
- return
- }
-
jankHandle?.dispose()
jankHandle =
KeyguardJankBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index efa9c21f96b4..caf0fd4450fc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED;
@@ -76,7 +75,6 @@ import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
-import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
@@ -194,8 +192,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -286,9 +282,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private static final int SYSTEM_READY = 18;
private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19;
private static final int BOOT_INTERACTOR = 20;
- private static final int BEFORE_USER_SWITCHING = 21;
- private static final int USER_SWITCHING = 22;
- private static final int USER_SWITCH_COMPLETE = 23;
/** Enum for reasons behind updating wakeAndUnlock state. */
@Retention(RetentionPolicy.SOURCE)
@@ -306,8 +299,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
int WAKE_AND_UNLOCK = 3;
}
- private final List<LockNowCallback> mLockNowCallbacks = new ArrayList<>();
-
/**
* The default amount of time we stay awake (used for all key input)
*/
@@ -366,18 +357,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
private final Lazy<ShadeController> mShadeController;
private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
- /*
- * Records the user id on request to go away, for validation when WM calls back to start the
- * exit animation.
- */
- private int mGoingAwayRequestedForUserId = -1;
-
private boolean mSystemReady;
private boolean mBootCompleted;
private boolean mBootSendUserPresent;
private boolean mShuttingDown;
private boolean mDozing;
private boolean mAnimatingScreenOff;
+ private boolean mIgnoreDismiss;
private final Context mContext;
private final FalsingCollector mFalsingCollector;
@@ -640,78 +626,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
};
- @VisibleForTesting
- protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() {
-
- @Override
- public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) {
- mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING,
- newUser, 0, resultCallback));
- }
-
- @Override
- public void onUserChanging(int newUser, @NonNull Context userContext,
- @NonNull Runnable resultCallback) {
- mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING,
- newUser, 0, resultCallback));
- }
-
- @Override
- public void onUserChanged(int newUser, Context userContext) {
- mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCH_COMPLETE,
- newUser, 0));
- }
- };
-
- /**
- * Handle {@link #BEFORE_USER_SWITCHING}
- */
- @VisibleForTesting
- void handleBeforeUserSwitching(int userId, Runnable resultCallback) {
- Log.d(TAG, String.format("onBeforeUserSwitching %d", userId));
- synchronized (KeyguardViewMediator.this) {
- mHandler.removeMessages(DISMISS);
- notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
- resetKeyguardDonePendingLocked();
- adjustStatusBarLocked();
- mKeyguardStateController.notifyKeyguardGoingAway(false);
- if (mLockPatternUtils.isSecure(userId) && !mShowing) {
- doKeyguardLocked(null);
- } else {
- resetStateLocked();
- }
- resultCallback.run();
- }
- }
-
- /**
- * Handle {@link #USER_SWITCHING}
- */
- @VisibleForTesting
- void handleUserSwitching(int userId, Runnable resultCallback) {
- Log.d(TAG, String.format("onUserSwitching %d", userId));
- synchronized (KeyguardViewMediator.this) {
- if (!mLockPatternUtils.isSecure(userId)) {
- dismiss(null, null);
- }
- resultCallback.run();
- }
- }
-
- /**
- * Handle {@link #USER_SWITCH_COMPLETE}
- */
- @VisibleForTesting
- void handleUserSwitchComplete(int userId) {
- Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
- // Calling dismiss on a secure user will show the bouncer
- if (mLockPatternUtils.isSecure(userId)) {
- // We are calling dismiss with a delay as there are race conditions in some scenarios
- // caused by async layout listeners
- mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
- }
- }
-
KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
@Override
@@ -728,6 +642,27 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
@Override
+ public void onUserSwitching(int userId) {
+ Log.d(TAG, String.format("onUserSwitching %d", userId));
+ synchronized (KeyguardViewMediator.this) {
+ mIgnoreDismiss = true;
+ notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
+ resetKeyguardDonePendingLocked();
+ resetStateLocked();
+ adjustStatusBarLocked();
+ }
+ }
+
+ @Override
+ public void onUserSwitchComplete(int userId) {
+ mIgnoreDismiss = false;
+ Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
+ // We are calling dismiss with a delay as there are race conditions in some scenarios
+ // caused by async layout listeners
+ mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
+ }
+
+ @Override
public void onDeviceProvisioned() {
sendUserPresentBroadcast();
}
@@ -1736,13 +1671,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
com.android.internal.R.anim.lock_screen_behind_enter);
mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
- mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
- // start() can be invoked in the middle of user switching, so check for this state and issue
- // the call manually as that important event was missed.
- if (mUserTracker.isUserSwitching()) {
- handleBeforeUserSwitching(mUserTracker.getUserId(), () -> {});
- handleUserSwitching(mUserTracker.getUserId(), () -> {});
- }
+
mJavaAdapter.alwaysCollectFlow(
mWallpaperRepository.getWallpaperSupportsAmbientMode(),
this::setWallpaperSupportsAmbientMode);
@@ -1791,7 +1720,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// System ready can be invoked in the middle of user switching, so check for this state
// and issue the call manually as that important event was missed.
if (mUserTracker.isUserSwitching()) {
- mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {});
+ mUpdateCallback.onUserSwitching(mUserTracker.getUserId());
}
}
// Most services aren't available until the system reaches the ready state, so we
@@ -2432,23 +2361,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mCommunalSceneInteractor.get().showHubFromPowerButton();
}
- int currentUserId = mSelectedUserInteractor.getSelectedUserId();
- if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) {
- LockNowCallback callback = new LockNowCallback(currentUserId,
- IRemoteCallback.Stub.asInterface(
- options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK)));
- synchronized (mLockNowCallbacks) {
- mLockNowCallbacks.add(callback);
- }
- Log.d(TAG, "LockNowCallback required for user: " + callback.mUserId);
- }
-
// if another app is disabling us, don't show
if (!mExternallyEnabled
&& !mLockPatternUtils.isUserInLockdown(
mSelectedUserInteractor.getSelectedUserId())) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled");
- notifyLockNowCallback();
+
mNeedToReshowWhenReenabled = true;
return;
}
@@ -2466,7 +2384,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// We're removing "reset" in the refactor - "resetting" the views will happen
// as a reaction to the root cause of the "reset" signal.
if (KeyguardWmStateRefactor.isEnabled()) {
- notifyLockNowCallback();
return;
}
@@ -2479,7 +2396,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
+ "previously hiding. It should be safe to short-circuit "
+ "here.");
resetStateLocked(/* hideBouncer= */ false);
- notifyLockNowCallback();
return;
}
} else {
@@ -2506,7 +2422,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
Log.d(TAG, "doKeyguard: not showing because device isn't provisioned and the sim is"
+ " not locked or missing");
}
- notifyLockNowCallback();
return;
}
@@ -2514,7 +2429,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (mLockPatternUtils.isLockScreenDisabled(mSelectedUserInteractor.getSelectedUserId())
&& !lockedOrMissing && !forceShow) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off");
- notifyLockNowCallback();
return;
}
@@ -2562,6 +2476,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+ if (mIgnoreDismiss) {
+ android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
+ return;
+ }
+
if (mKeyguardStateController.isKeyguardGoingAway()) {
Log.i(TAG, "Ignoring dismiss because we're already going away.");
return;
@@ -2579,7 +2498,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
private void resetStateLocked(boolean hideBouncer) {
- if (DEBUG) Log.d(TAG, "resetStateLocked");
+ if (DEBUG) Log.e(TAG, "resetStateLocked");
Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0);
mHandler.sendMessage(msg);
}
@@ -2827,18 +2746,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
message = "BOOT_INTERACTOR";
handleBootInteractor();
break;
- case BEFORE_USER_SWITCHING:
- message = "BEFORE_USER_SWITCHING";
- handleBeforeUserSwitching(msg.arg1, (Runnable) msg.obj);
- break;
- case USER_SWITCHING:
- message = "USER_SWITCHING";
- handleUserSwitching(msg.arg1, (Runnable) msg.obj);
- break;
- case USER_SWITCH_COMPLETE:
- message = "USER_SWITCH_COMPLETE";
- handleUserSwitchComplete(msg.arg1);
- break;
}
Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
}
@@ -2980,9 +2887,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mUiBgExecutor.execute(() -> {
Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", "
+ reason + ")");
- if (showing) {
- notifyLockNowCallback();
- }
if (KeyguardWmStateRefactor.isEnabled()) {
// Handled in WmLockscreenVisibilityManager if flag is enabled.
@@ -3027,7 +2931,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
synchronized (KeyguardViewMediator.this) {
if (!mSystemReady) {
if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready.");
- notifyLockNowCallback();
return;
}
if (DEBUG) Log.d(TAG, "handleShow");
@@ -3086,11 +2989,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
}
- final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
+ private final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
@SuppressLint("MissingPermission")
@Override
public void run() {
Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable");
+ Log.d(TAG, "keyguardGoingAwayRunnable");
mKeyguardViewControllerLazy.get().keyguardGoingAway();
int flags = 0;
@@ -3127,10 +3031,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// Handled in WmLockscreenVisibilityManager if flag is enabled.
if (!KeyguardWmStateRefactor.isEnabled()) {
- mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
- Log.d(TAG, "keyguardGoingAway requested for userId: "
- + mGoingAwayRequestedForUserId);
-
// Don't actually hide the Keyguard at the moment, wait for window manager
// until it tells us it's safe to do so with startKeyguardExitAnimation.
// Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager
@@ -3269,30 +3169,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) {
Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime
+ " fadeoutDuration=" + fadeoutDuration);
- int currentUserId = mSelectedUserInteractor.getSelectedUserId();
- if (!KeyguardWmStateRefactor.isEnabled() && mGoingAwayRequestedForUserId != currentUserId) {
- Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId "
- + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: "
- + currentUserId);
- if (finishedCallback != null) {
- // There will not execute animation, send a finish callback to ensure the remote
- // animation won't hang there.
- try {
- finishedCallback.onAnimationFinished();
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call onAnimationFinished", e);
- }
- }
- mHiding = false;
- if (mLockPatternUtils.isSecure(currentUserId)) {
- doKeyguardLocked(null);
- } else {
- resetStateLocked();
- dismiss(null, null);
- }
- return;
- }
-
synchronized (KeyguardViewMediator.this) {
mIsKeyguardExitAnimationCanceled = false;
// Tell ActivityManager that we canceled the keyguard animation if
@@ -3537,13 +3413,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
* app transition before finishing the current RemoteAnimation, or the keyguard being re-shown).
*/
private void handleCancelKeyguardExitAnimation() {
- if (!KeyguardWmStateRefactor.isEnabled()
- && mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) {
- Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: "
- + mGoingAwayRequestedForUserId + ", current: "
- + mSelectedUserInteractor.getSelectedUserId());
- setPendingLock(true);
- }
if (mPendingLock) {
Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. "
+ "There's a pending lock, so we were cancelled because the device was locked "
@@ -3644,7 +3513,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mSurfaceBehindRemoteAnimationRequested = true;
if (ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS && !KeyguardWmStateRefactor.isEnabled()) {
- mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
startKeyguardTransition(false /* keyguardShowing */, false /* aodShowing */);
return;
}
@@ -3665,9 +3533,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (!KeyguardWmStateRefactor.isEnabled()) {
// Handled in WmLockscreenVisibilityManager.
- mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
- Log.d(TAG, "keyguardGoingAway requested for userId: "
- + mGoingAwayRequestedForUserId);
mActivityTaskManagerService.keyguardGoingAway(flags);
}
} catch (RemoteException e) {
@@ -4123,29 +3988,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged);
}
- private void notifyLockNowCallback() {
- List<LockNowCallback> callbacks;
- synchronized (mLockNowCallbacks) {
- callbacks = new ArrayList<LockNowCallback>(mLockNowCallbacks);
- mLockNowCallbacks.clear();
- }
- Iterator<LockNowCallback> iter = callbacks.listIterator();
- while (iter.hasNext()) {
- LockNowCallback callback = iter.next();
- iter.remove();
- if (callback.mUserId != mSelectedUserInteractor.getSelectedUserId()) {
- Log.i(TAG, "Not notifying lockNowCallback due to user mismatch");
- continue;
- }
- Log.i(TAG, "Notifying lockNowCallback");
- try {
- callback.mRemoteCallback.sendResult(null);
- } catch (RemoteException e) {
- Log.e(TAG, "Could not issue LockNowCallback sendResult", e);
- }
- }
- }
-
private void notifyTrustedChangedLocked(boolean trusted) {
int size = mKeyguardStateCallbacks.size();
for (int i = size - 1; i >= 0; i--) {
@@ -4310,14 +4152,4 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
};
}
-
- private class LockNowCallback {
- final int mUserId;
- final IRemoteCallback mRemoteCallback;
-
- LockNowCallback(int userId, IRemoteCallback remoteCallback) {
- mUserId = userId;
- mRemoteCallback = remoteCallback;
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index f85a23c1f091..eb96c921c181 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -24,6 +24,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.Flags.communalSceneKtfRefactor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -57,6 +58,7 @@ constructor(
keyguardInteractor: KeyguardInteractor,
powerInteractor: PowerInteractor,
private val communalInteractor: CommunalInteractor,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
private val communalSceneInteractor: CommunalSceneInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
val deviceEntryInteractor: DeviceEntryInteractor,
@@ -116,6 +118,17 @@ constructor(
}
}
+ @SuppressLint("MissingPermission")
+ private fun shouldTransitionToCommunal(
+ shouldShowCommunal: Boolean,
+ isCommunalAvailable: Boolean,
+ ) =
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ shouldShowCommunal
+ } else {
+ isCommunalAvailable && dreamManager.canStartDreaming(false)
+ }
+
@OptIn(FlowPreview::class)
@SuppressLint("MissingPermission")
private fun listenForDozingToDreaming() {
@@ -141,9 +154,10 @@ constructor(
.filterRelevantKeyguardStateAnd { isAwake -> isAwake }
.sample(
communalInteractor.isCommunalAvailable,
+ communalInteractor.shouldShowCommunal,
communalSceneInteractor.isIdleOnCommunal,
)
- .collect { (_, isCommunalAvailable, isIdleOnCommunal) ->
+ .collect { (_, isCommunalAvailable, shouldShowCommunal, isIdleOnCommunal) ->
val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value
@@ -177,11 +191,9 @@ constructor(
if (!SceneContainerFlag.isEnabled) {
startTransitionTo(KeyguardState.GLANCEABLE_HUB)
}
- } else if (isCommunalAvailable && dreamManager.canStartDreaming(false)) {
- // Using false for isScreenOn as canStartDreaming returns false if any
- // dream, including doze, is active.
- // This case handles tapping the power button to transition through
- // dream -> off -> hub.
+ } else if (
+ shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable)
+ ) {
if (!SceneContainerFlag.isEnabled) {
transitionToGlanceableHub()
}
@@ -203,6 +215,7 @@ constructor(
powerInteractor.detailedWakefulness
.filterRelevantKeyguardStateAnd { it.isAwake() }
.sample(
+ communalInteractor.shouldShowCommunal,
communalInteractor.isCommunalAvailable,
communalSceneInteractor.isIdleOnCommunal,
keyguardInteractor.biometricUnlockState,
@@ -212,6 +225,7 @@ constructor(
.collect {
(
_,
+ shouldShowCommunal,
isCommunalAvailable,
isIdleOnCommunal,
biometricUnlockState,
@@ -245,7 +259,9 @@ constructor(
ownerReason = "waking from dozing",
)
}
- } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+ } else if (
+ shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable)
+ ) {
if (!SceneContainerFlag.isEnabled) {
transitionToGlanceableHub()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 251af11f7fe6..c1c509b8fd57 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -129,20 +129,37 @@ constructor(
if (!communalSettingsInteractor.isCommunalFlagEnabled()) return
if (SceneContainerFlag.isEnabled) return
scope.launch {
- powerInteractor.isAwake
- .debounce(50L)
- .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
- .sample(communalInteractor.isCommunalAvailable)
- .collect { isCommunalAvailable ->
- if (isCommunalAvailable && dreamManager.canStartDreaming(false)) {
- // This case handles tapping the power button to transition through
- // dream -> off -> hub.
- communalSceneInteractor.snapToScene(
- newScene = CommunalScenes.Communal,
- loggingReason = "from dreaming to hub",
- )
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ powerInteractor.isAwake
+ .debounce(50L)
+ .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
+ .sample(communalInteractor.shouldShowCommunal)
+ .collect { shouldShowCommunal ->
+ if (shouldShowCommunal) {
+ // This case handles tapping the power button to transition through
+ // dream -> off -> hub.
+ communalSceneInteractor.snapToScene(
+ newScene = CommunalScenes.Communal,
+ loggingReason = "from dreaming to hub",
+ )
+ }
}
- }
+ } else {
+ powerInteractor.isAwake
+ .debounce(50L)
+ .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
+ .sample(communalInteractor.isCommunalAvailable)
+ .collect { isCommunalAvailable ->
+ if (isCommunalAvailable && dreamManager.canStartDreaming(false)) {
+ // This case handles tapping the power button to transition through
+ // dream -> off -> hub.
+ communalSceneInteractor.snapToScene(
+ newScene = CommunalScenes.Communal,
+ loggingReason = "from dreaming to hub",
+ )
+ }
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
index 382436cf9397..5f821022d580 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt
@@ -215,6 +215,7 @@ constructor(
animator = null,
modeOnCanceled = TransitionModeOnCanceled.RESET,
)
+ repository.nextLockscreenTargetState.value = DEFAULT_STATE
startTransition(newTransition)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 5c03d65e570f..8f6815829ba2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -69,7 +69,7 @@ constructor(
* Note that [onCancel] isn't used when the scene framework is enabled.
*/
fun sharedFlow(
- duration: Duration,
+ duration: Duration = transitionDuration,
onStep: (Float) -> Float,
startTime: Duration = 0.milliseconds,
onStart: (() -> Unit)? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
index 0cb684a1aabe..38263be33c82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.KeyguardJankViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -79,15 +80,18 @@ object KeyguardJankBinder {
}
}
- launch {
- viewModel.lockscreenToAodTransition.collect {
- processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD)
+ // The following is already done in KeyguardTransitionAnimationCallbackImpl.
+ if (!SceneContainerFlag.isEnabled) {
+ launch {
+ viewModel.lockscreenToAodTransition.collect {
+ processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD)
+ }
}
- }
- launch {
- viewModel.aodToLockscreenTransition.collect {
- processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD)
+ launch {
+ viewModel.aodToLockscreenTransition.collect {
+ processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
index 19cd501fa787..50f8e086ac6e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.transitions
+import android.util.MathUtils
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -33,8 +34,18 @@ constructor(
blurConfig: BlurConfig,
) {
val exitBlurRadius: Flow<Float> =
- transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+ transitionAnimation.sharedFlow(
+ onStep = { MathUtils.lerp(blurConfig.maxBlurRadiusPx, blurConfig.minBlurRadiusPx, it) },
+ onStart = { blurConfig.maxBlurRadiusPx },
+ onFinish = { blurConfig.minBlurRadiusPx },
+ onCancel = { blurConfig.maxBlurRadiusPx },
+ )
val enterBlurRadius: Flow<Float> =
- transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+ transitionAnimation.sharedFlow(
+ onStep = { MathUtils.lerp(blurConfig.minBlurRadiusPx, blurConfig.maxBlurRadiusPx, it) },
+ onStart = { blurConfig.minBlurRadiusPx },
+ onFinish = { blurConfig.maxBlurRadiusPx },
+ onCancel = { blurConfig.minBlurRadiusPx },
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
index 912ace7675d5..e5eec64ac615 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
@@ -27,6 +27,7 @@ import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.android.dream.lowlight.LowLightDreamManager;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.SystemUser;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.shared.condition.Condition;
@@ -36,6 +37,7 @@ import com.android.systemui.util.condition.ConditionalCoreStartable;
import dagger.Lazy;
import java.util.Set;
+import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
@@ -59,6 +61,8 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor
private final PackageManager mPackageManager;
+ private final Executor mExecutor;
+
@Inject
public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager,
@SystemUser Monitor conditionsMonitor,
@@ -66,7 +70,8 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor
ScreenLifecycle screenLifecycle,
LowLightLogger lowLightLogger,
@Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService,
- PackageManager packageManager) {
+ PackageManager packageManager,
+ @Background Executor backgroundExecutor) {
super(conditionsMonitor);
mLowLightDreamManager = lowLightDreamManager;
mConditionsMonitor = conditionsMonitor;
@@ -75,59 +80,69 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor
mLogger = lowLightLogger;
mLowLightDreamService = lowLightDreamService;
mPackageManager = packageManager;
+ mExecutor = backgroundExecutor;
}
@Override
public void onConditionsChanged(boolean allConditionsMet) {
- mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
+ mExecutor.execute(() -> {
+ mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
- mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
- ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
+ mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
+ ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
+ });
}
@Override
public void onScreenTurnedOn() {
- if (mSubscriptionToken == null) {
- mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");
-
- mSubscriptionToken = mConditionsMonitor.addSubscription(
- new Monitor.Subscription.Builder(this)
- .addConditions(mLowLightConditions.get())
- .build());
- }
+ mExecutor.execute(() -> {
+ if (mSubscriptionToken == null) {
+ mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");
+
+ mSubscriptionToken = mConditionsMonitor.addSubscription(
+ new Monitor.Subscription.Builder(this)
+ .addConditions(mLowLightConditions.get())
+ .build());
+ }
+ });
}
@Override
public void onScreenTurnedOff() {
- if (mSubscriptionToken != null) {
- mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
-
- mConditionsMonitor.removeSubscription(mSubscriptionToken);
- mSubscriptionToken = null;
- }
+ mExecutor.execute(() -> {
+ if (mSubscriptionToken != null) {
+ mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
+
+ mConditionsMonitor.removeSubscription(mSubscriptionToken);
+ mSubscriptionToken = null;
+ }
+ });
}
@Override
protected void onStart() {
- if (mLowLightDreamService != null) {
- // Note that the dream service is disabled by default. This prevents the dream from
- // appearing in settings on devices that don't have it explicitly excluded (done in
- // the settings overlay). Therefore, the component is enabled if it is to be used
- // here.
- mPackageManager.setComponentEnabledSetting(
- mLowLightDreamService,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
- PackageManager.DONT_KILL_APP
- );
- } else {
- // If there is no low light dream service, do not observe conditions.
- return;
- }
-
- mScreenLifecycle.addObserver(this);
- if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
- onScreenTurnedOn();
- }
+ mExecutor.execute(() -> {
+ if (mLowLightDreamService != null) {
+ // Note that the dream service is disabled by default. This prevents the dream from
+ // appearing in settings on devices that don't have it explicitly excluded (done in
+ // the settings overlay). Therefore, the component is enabled if it is to be used
+ // here.
+ mPackageManager.setComponentEnabledSetting(
+ mLowLightDreamService,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP
+ );
+ } else {
+ // If there is no low light dream service, do not observe conditions.
+ return;
+ }
+
+ mScreenLifecycle.addObserver(this);
+ if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
+ onScreenTurnedOn();
+ }
+ });
+
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
index 8469cb4ab565..f8072f2f79b4 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
@@ -78,7 +78,7 @@ public abstract class LowLightModule {
@Provides
@IntoSet
- @Named(com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS)
+ @Named(LOW_LIGHT_PRECONDITIONS)
static Condition provideLowLightCondition(LowLightCondition lowLightCondition,
DirectBootCondition directBootCondition) {
// Start lowlight if we are either in lowlight or in direct boot. The ordering of the
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index c9740811101b..be814aecc42b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -58,7 +58,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private static final float DEVICE_DISABLED_ALPHA = 0.5f;
private static final float DEVICE_ACTIVE_ALPHA = 1f;
protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
- private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherSessionGrouping();
+ private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping();
public MediaOutputAdapter(MediaSwitchingController controller) {
super(controller);
@@ -333,7 +333,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
- if (Flags.enableOutputSwitcherSessionGrouping()) {
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
return isGroupCheckboxEnabled(groupStatus);
}
return true;
@@ -391,7 +391,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
if (drawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) drawable).start();
}
- if (Flags.enableOutputSwitcherSessionGrouping()) {
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 51437b3bbdaf..9d375809786a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -733,7 +733,7 @@ public class MediaSwitchingController
selectedDevicesIds.add(connectedMediaDevice.getId());
}
boolean groupSelectedDevices =
- com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping();
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
int nextSelectedItemIndex = 0;
boolean suggestedDeviceAdded = false;
boolean displayGroupAdded = false;
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
index 1b9251061f3d..9319961f5b68 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt
@@ -24,7 +24,7 @@ import com.android.compose.animation.scene.UserActionResult.HideOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -38,7 +38,7 @@ class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() :
mapOf(
Swipe.Up to HideOverlay(Overlays.NotificationsShade),
Back to HideOverlay(Overlays.NotificationsShade),
- Swipe.Down(fromSource = SceneContainerEdge.TopRight) to
+ Swipe.Down(fromSource = SceneContainerArea.EndHalf) to
ShowOverlay(
Overlays.QuickSettingsShade,
hideCurrentOverlays = HideCurrentOverlays.Some(Overlays.NotificationsShade),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
index 16dff7d11002..11b014c2147f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/QSPreferencesRepository.kt
@@ -28,6 +28,7 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.qs.panels.shared.model.PanelsLog
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import com.android.systemui.settings.UserFileManager
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.SharedPreferencesExt.observe
@@ -83,34 +84,78 @@ constructor(
.flowOn(backgroundDispatcher)
/** Sets for the current user the set of [TileSpec] to display as large tiles. */
- fun setLargeTilesSpecs(specs: Set<TileSpec>) {
- setLargeTilesSpecsForUser(specs, userRepository.getSelectedUserInfo().id)
+ fun writeLargeTileSpecs(specs: Set<TileSpec>) {
+ with(getSharedPrefs(userRepository.getSelectedUserInfo().id)) {
+ writeLargeTileSpecs(specs)
+ setLargeTilesDefault(false)
+ }
}
- private fun setLargeTilesSpecsForUser(specs: Set<TileSpec>, userId: Int) {
- with(getSharedPrefs(userId)) {
- edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply()
+ suspend fun deleteLargeTileDataJob() {
+ userRepository.selectedUserInfo.collect { userInfo ->
+ getSharedPrefs(userInfo.id)
+ .edit()
+ .remove(LARGE_TILES_SPECS_KEY)
+ .remove(LARGE_TILES_DEFAULT_KEY)
+ .apply()
}
}
+ private fun SharedPreferences.writeLargeTileSpecs(specs: Set<TileSpec>) {
+ edit().putStringSet(LARGE_TILES_SPECS_KEY, specs.map { it.spec }.toSet()).apply()
+ }
+
/**
- * Sets the initial tiles as large, if there is no set in SharedPrefs for the [userId]. This is
- * to be used when upgrading to a build that supports large/small tiles.
+ * Sets the initial set of large tiles. One of the following cases will happen:
+ * * If we are setting the default set (no value stored in settings for the list of tiles), set
+ * the large tiles based on [defaultLargeTilesRepository]. We do this to signal future reboots
+ * that we have performed the upgrade path once. In this case, we will mark that we set them
+ * as the default in case a restore needs to modify them later.
+ * * If we got a list of tiles restored from a device and nothing has modified the list of
+ * tiles, set all the restored tiles to large. Note that if we also restored a set of large
+ * tiles before this was called, [LARGE_TILES_DEFAULT_KEY] will be false and we won't
+ * overwrite it.
+ * * If we got a list of tiles from settings, we consider that we upgraded in place and then we
+ * will set all those tiles to large IF there's no current set of large tiles.
*
* Even if largeTilesSpec is read Eagerly before we know if we are in an initial state, because
* we are not writing the default values to the SharedPreferences, the file will not contain the
* key and this call will succeed, as long as there hasn't been any calls to setLargeTilesSpecs
* for that user before.
*/
- fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, userId: Int) {
+ fun setInitialOrUpgradeLargeTiles(upgradePath: TilesUpgradePath, userId: Int) {
with(getSharedPrefs(userId)) {
- if (!contains(LARGE_TILES_SPECS_KEY)) {
- logger.i("Setting upgraded large tiles for user $userId: $specs")
- setLargeTilesSpecsForUser(specs, userId)
+ when (upgradePath) {
+ is TilesUpgradePath.DefaultSet -> {
+ writeLargeTileSpecs(defaultLargeTilesRepository.defaultLargeTiles)
+ logger.i("Large tiles set to default on init")
+ setLargeTilesDefault(true)
+ }
+ is TilesUpgradePath.RestoreFromBackup -> {
+ if (
+ getBoolean(LARGE_TILES_DEFAULT_KEY, false) ||
+ !contains(LARGE_TILES_SPECS_KEY)
+ ) {
+ writeLargeTileSpecs(upgradePath.value)
+ logger.i("Tiles restored from backup set to large: ${upgradePath.value}")
+ setLargeTilesDefault(false)
+ }
+ }
+ is TilesUpgradePath.ReadFromSettings -> {
+ if (!contains(LARGE_TILES_SPECS_KEY)) {
+ writeLargeTileSpecs(upgradePath.value)
+ logger.i("Tiles read from settings set to large: ${upgradePath.value}")
+ setLargeTilesDefault(false)
+ }
+ }
}
}
}
+ private fun SharedPreferences.setLargeTilesDefault(value: Boolean) {
+ edit().putBoolean(LARGE_TILES_DEFAULT_KEY, value).apply()
+ }
+
private fun getSharedPrefs(userId: Int): SharedPreferences {
return userFileManager.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, userId)
}
@@ -118,6 +163,7 @@ constructor(
companion object {
private const val TAG = "QSPreferencesRepository"
private const val LARGE_TILES_SPECS_KEY = "large_tiles_specs"
+ private const val LARGE_TILES_DEFAULT_KEY = "large_tiles_default"
const val FILE_NAME = "quick_settings_prefs"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt
index 86838b438bc6..9b98797ef393 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/QSPreferencesInteractor.kt
@@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.panels.data.repository.QSPreferencesRepository
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -27,10 +28,20 @@ class QSPreferencesInteractor @Inject constructor(private val repo: QSPreference
val largeTilesSpecs: Flow<Set<TileSpec>> = repo.largeTilesSpecs
fun setLargeTilesSpecs(specs: Set<TileSpec>) {
- repo.setLargeTilesSpecs(specs)
+ repo.writeLargeTileSpecs(specs)
}
- fun setInitialLargeTilesSpecs(specs: Set<TileSpec>, user: Int) {
- repo.setInitialLargeTilesSpecs(specs, user)
+ /**
+ * This method should be called to indicate that a "new" set of tiles has been determined for a
+ * particular user coming from different upgrade sources.
+ *
+ * @see TilesUpgradePath for more information
+ */
+ fun setInitialOrUpgradeLargeTilesSpecs(specs: TilesUpgradePath, user: Int) {
+ repo.setInitialOrUpgradeLargeTiles(specs, user)
+ }
+
+ suspend fun deleteLargeTilesDataJob() {
+ repo.deleteLargeTileDataJob()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt
index a8ac5c34d8f9..e2797356fa96 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/startable/QSPanelsCoreStartable.kt
@@ -19,11 +19,13 @@ package com.android.systemui.qs.panels.domain.startable
import com.android.app.tracing.coroutines.launchTraced
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.flags.QsInCompose
import com.android.systemui.qs.panels.domain.interactor.QSPreferencesInteractor
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
class QSPanelsCoreStartable
@Inject
@@ -33,10 +35,14 @@ constructor(
@Background private val backgroundApplicationScope: CoroutineScope,
) : CoreStartable {
override fun start() {
- backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") {
- tileSpecRepository.tilesReadFromSetting.receiveAsFlow().collect { (tiles, userId) ->
- preferenceInteractor.setInitialLargeTilesSpecs(tiles, userId)
+ if (QsInCompose.isEnabled) {
+ backgroundApplicationScope.launchTraced("QSPanelsCoreStartable.startingLargeTiles") {
+ tileSpecRepository.tilesUpgradePath.receiveAsFlow().collect { (tiles, userId) ->
+ preferenceInteractor.setInitialOrUpgradeLargeTilesSpecs(tiles, userId)
+ }
}
+ } else {
+ backgroundApplicationScope.launch { preferenceInteractor.deleteLargeTilesDataJob() }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
index 6b7dd386bb46..c50d5dad10c1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
@@ -24,6 +24,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.res.R
import com.android.systemui.retail.data.repository.RetailModeRepository
@@ -78,7 +79,7 @@ interface TileSpecRepository {
/** Reset the current set of tiles to the default list of tiles */
suspend fun resetToDefault(userId: Int)
- val tilesReadFromSetting: ReceiveChannel<Pair<Set<TileSpec>, Int>>
+ val tilesUpgradePath: ReceiveChannel<Pair<TilesUpgradePath, Int>>
companion object {
/** Position to indicate the end of the list */
@@ -112,8 +113,8 @@ constructor(
.filter { it !is TileSpec.Invalid }
}
- private val _tilesReadFromSetting = Channel<Pair<Set<TileSpec>, Int>>(capacity = 5)
- override val tilesReadFromSetting = _tilesReadFromSetting
+ private val _tilesUpgradePath = Channel<Pair<TilesUpgradePath, Int>>(capacity = 5)
+ override val tilesUpgradePath = _tilesUpgradePath
private val userTileRepositories = SparseArray<UserTileSpecRepository>()
@@ -122,8 +123,8 @@ constructor(
val userTileRepository = userTileSpecRepositoryFactory.create(userId)
userTileRepositories.put(userId, userTileRepository)
applicationScope.launchTraced("TileSpecRepository.aggregateTilesPerUser") {
- for (tilesFromSettings in userTileRepository.tilesReadFromSettings) {
- _tilesReadFromSetting.send(tilesFromSettings to userId)
+ for (tileUpgrade in userTileRepository.tilesUpgradePath) {
+ _tilesUpgradePath.send(tileUpgrade to userId)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
index 7b56cd92a081..5aa5edaa726e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
@@ -9,6 +9,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.util.settings.SecureSettings
import dagger.assisted.Assisted
@@ -49,8 +50,8 @@ constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
) {
- private val _tilesReadFromSettings = Channel<Set<TileSpec>>(capacity = 2)
- val tilesReadFromSettings: ReceiveChannel<Set<TileSpec>> = _tilesReadFromSettings
+ private val _tilesUpgradePath = Channel<TilesUpgradePath>(capacity = 3)
+ val tilesUpgradePath: ReceiveChannel<TilesUpgradePath> = _tilesUpgradePath
private val defaultTiles: List<TileSpec>
get() = defaultTilesRepository.defaultTiles
@@ -67,14 +68,23 @@ constructor(
.scan(loadTilesFromSettingsAndParse(userId)) { current, change ->
change
.apply(current)
- .also {
- if (current != it) {
+ .also { afterRestore ->
+ if (current != afterRestore) {
if (change is RestoreTiles) {
- logger.logTilesRestoredAndReconciled(current, it, userId)
+ logger.logTilesRestoredAndReconciled(
+ current,
+ afterRestore,
+ userId,
+ )
} else {
- logger.logProcessTileChange(change, it, userId)
+ logger.logProcessTileChange(change, afterRestore, userId)
}
}
+ if (change is RestoreTiles) {
+ _tilesUpgradePath.send(
+ TilesUpgradePath.RestoreFromBackup(afterRestore.toSet())
+ )
+ }
}
// Distinct preserves the order of the elements removing later
// duplicates,
@@ -154,7 +164,9 @@ constructor(
private suspend fun loadTilesFromSettingsAndParse(userId: Int): List<TileSpec> {
val loadedTiles = loadTilesFromSettings(userId)
if (loadedTiles.isNotEmpty()) {
- _tilesReadFromSettings.send(loadedTiles.toSet())
+ _tilesUpgradePath.send(TilesUpgradePath.ReadFromSettings(loadedTiles.toSet()))
+ } else {
+ _tilesUpgradePath.send(TilesUpgradePath.DefaultSet)
}
return parseTileSpecs(loadedTiles, userId)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt
new file mode 100644
index 000000000000..98f30c22d0f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TilesUpgradePath.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.shared
+
+/** Upgrade paths indicating the source of the list of QS tiles. */
+sealed interface TilesUpgradePath {
+
+ sealed interface UpgradeWithTiles : TilesUpgradePath {
+ val value: Set<TileSpec>
+ }
+
+ /** This indicates a set of tiles that was read from Settings on user start */
+ @JvmInline value class ReadFromSettings(override val value: Set<TileSpec>) : UpgradeWithTiles
+
+ /** This indicates a set of tiles that was restored from backup */
+ @JvmInline value class RestoreFromBackup(override val value: Set<TileSpec>) : UpgradeWithTiles
+
+ /**
+ * This indicates that no tiles were read from Settings on user start so the default has been
+ * stored.
+ */
+ data object DefaultSet : TilesUpgradePath
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
index 5bc26f50f70f..52c4e2fac6d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt
@@ -25,7 +25,7 @@ import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.compose.animation.scene.UserActionResult.ShowOverlay.HideCurrentOverlays
import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -47,7 +47,7 @@ constructor(private val editModeViewModel: EditModeViewModel) : UserActionsViewM
put(Back, HideOverlay(Overlays.QuickSettingsShade))
}
put(
- Swipe.Down(fromSource = SceneContainerEdge.TopLeft),
+ Swipe.Down(fromSource = SceneContainerArea.StartHalf),
ShowOverlay(
Overlays.NotificationsShade,
hideCurrentOverlays =
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index a4949ad66109..caa61617505f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.scene
-import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.SceneContainerTransitions
-import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -99,15 +96,5 @@ interface KeyguardlessSceneContainerFrameworkModule {
transitionsBuilder = SceneContainerTransitions(),
)
}
-
- @Provides
- fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
- return SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
- // replace this constant with dynamic window insets.
- edgeSize = 40.dp,
- )
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index a018283c3953..ea11d202b119 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.scene
-import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,8 +29,6 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.SceneContainerTransitions
-import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -121,15 +118,5 @@ interface SceneContainerFrameworkModule {
transitionsBuilder = SceneContainerTransitions(),
)
}
-
- @Provides
- fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
- return SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
- // replace this constant with dynamic window insets.
- edgeSize = 40.dp,
- )
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 7a32491c0b67..475c0794861f 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -240,7 +240,13 @@ constructor(
) {
val currentSceneKey = currentScene.value
val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene
- if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) {
+ if (
+ !validateSceneChange(
+ from = currentSceneKey,
+ to = resolvedScene,
+ loggingReason = loggingReason,
+ )
+ ) {
return
}
@@ -249,6 +255,7 @@ constructor(
logger.logSceneChanged(
from = currentSceneKey,
to = resolvedScene,
+ sceneState = sceneState,
reason = loggingReason,
isInstant = false,
)
@@ -272,13 +279,20 @@ constructor(
familyResolver.resolvedScene.value
}
} ?: toScene
- if (!validateSceneChange(to = resolvedScene, loggingReason = loggingReason)) {
+ if (
+ !validateSceneChange(
+ from = currentSceneKey,
+ to = resolvedScene,
+ loggingReason = loggingReason,
+ )
+ ) {
return
}
logger.logSceneChanged(
from = currentSceneKey,
to = resolvedScene,
+ sceneState = null,
reason = loggingReason,
isInstant = true,
)
@@ -489,11 +503,12 @@ constructor(
* Will throw a runtime exception for illegal states (for example, attempting to change to a
* scene that's not part of the current scene framework configuration).
*
+ * @param from The current scene being transitioned away from
* @param to The desired destination scene to transition to
* @param loggingReason The reason why the transition is requested, for logging purposes
* @return `true` if the scene change is valid; `false` if it shouldn't happen
*/
- private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean {
+ private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean {
check(
!shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings)
) {
@@ -503,6 +518,10 @@ constructor(
"Can't change scene to ${to.debugName} in split shade mode!"
}
+ if (from == to) {
+ return false
+ }
+
if (to !in repository.allContentKeys) {
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
index 16c2ef556de8..d00585858ccb 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -45,23 +45,30 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer:
)
}
- fun logSceneChanged(from: SceneKey, to: SceneKey, reason: String, isInstant: Boolean) {
+ fun logSceneChanged(
+ from: SceneKey,
+ to: SceneKey,
+ sceneState: Any?,
+ reason: String,
+ isInstant: Boolean,
+ ) {
logBuffer.log(
tag = TAG,
level = LogLevel.INFO,
messageInitializer = {
- str1 = from.toString()
- str2 = to.toString()
- str3 = reason
+ str1 = "${from.debugName} → ${to.debugName}"
+ str2 = reason
+ str3 = sceneState?.toString()
bool1 = isInstant
},
messagePrinter = {
buildString {
- append("Scene changed: $str1 → $str2")
+ append("Scene changed: $str1")
+ str3?.let { append(" (sceneState=$it)") }
if (isInstant) {
append(" (instant)")
}
- append(", reason: $str3")
+ append(", reason: $str2")
}
},
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
index f0f476e65e2f..364da5f8e80d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -30,19 +30,14 @@ import com.android.systemui.compose.ComposeInitializer
import com.android.systemui.res.R
/** A view that can serve as the root of the main SysUI window. */
-open class WindowRootView(
- context: Context,
- attrs: AttributeSet?,
-) :
- FrameLayout(
- context,
- attrs,
- ) {
+open class WindowRootView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
private lateinit var layoutInsetsController: LayoutInsetsController
private var leftInset = 0
private var rightInset = 0
+ private var previousInsets: WindowInsets? = null
+
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@@ -66,11 +61,14 @@ open class WindowRootView(
override fun generateDefaultLayoutParams(): FrameLayout.LayoutParams? {
return LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT
+ FrameLayout.LayoutParams.MATCH_PARENT,
)
}
override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
+ if (windowInsets == previousInsets) {
+ return windowInsets
+ }
val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
if (fitsSystemWindows) {
val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom
@@ -95,7 +93,7 @@ open class WindowRootView(
leftInset = pairInsets.first
rightInset = pairInsets.second
applyMargins()
- return windowInsets
+ return windowInsets.also { previousInsets = WindowInsets(it) }
}
fun setLayoutInsetsController(layoutInsetsController: LayoutInsetsController) {
@@ -143,37 +141,22 @@ open class WindowRootView(
interface LayoutInsetsController {
/** Update the insets and calculate them accordingly. */
- fun getinsets(
- windowInsets: WindowInsets?,
- displayCutout: DisplayCutout?,
- ): Pair<Int, Int>
+ fun getinsets(windowInsets: WindowInsets?, displayCutout: DisplayCutout?): Pair<Int, Int>
}
private class LayoutParams : FrameLayout.LayoutParams {
var ignoreRightInset = false
- constructor(
- width: Int,
- height: Int,
- ) : super(
- width,
- height,
- )
+ constructor(width: Int, height: Int) : super(width, height)
@SuppressLint("CustomViewStyleable")
- constructor(
- context: Context,
- attrs: AttributeSet?,
- ) : super(
- context,
- attrs,
- ) {
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
val obtainedAttributes =
context.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout)
ignoreRightInset =
obtainedAttributes.getBoolean(
R.styleable.StatusBarWindowView_Layout_ignoreRightInset,
- false
+ false,
)
obtainedAttributes.recycle()
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt
new file mode 100644
index 000000000000..ede453dbe6b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerSwipeDetector.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/** Identifies an area of the [SceneContainer] to detect swipe gestures on. */
+sealed class SceneContainerArea(private val resolveArea: (LayoutDirection) -> Resolved) :
+ SwipeSource {
+ data object StartEdge :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.LeftEdge else Resolved.RightEdge
+ }
+ )
+
+ data object StartHalf :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.LeftHalf else Resolved.RightHalf
+ }
+ )
+
+ data object EndEdge :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.RightEdge else Resolved.LeftEdge
+ }
+ )
+
+ data object EndHalf :
+ SceneContainerArea(
+ resolveArea = {
+ if (it == LayoutDirection.Ltr) Resolved.RightHalf else Resolved.LeftHalf
+ }
+ )
+
+ override fun resolve(layoutDirection: LayoutDirection): Resolved {
+ return resolveArea(layoutDirection)
+ }
+
+ sealed interface Resolved : SwipeSource.Resolved {
+ data object LeftEdge : Resolved
+
+ data object LeftHalf : Resolved
+
+ data object BottomEdge : Resolved
+
+ data object RightEdge : Resolved
+
+ data object RightHalf : Resolved
+ }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], but additionally
+ * detects the left and right halves of the screen (besides the edges).
+ *
+ * Corner cases (literally): A vertical swipe on the top-left corner of the screen will be resolved
+ * to [SceneContainerArea.Resolved.LeftHalf], whereas a horizontal swipe in the same position will
+ * be resolved to [SceneContainerArea.Resolved.LeftEdge]. The behavior is similar on the top-right
+ * corner of the screen.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerArea.StartEdge] and [SceneContainerArea.EndEdge] instead.
+ * These will be resolved at runtime to [SceneContainerArea.Resolved.LeftEdge] and
+ * [SceneContainerArea.Resolved.RightEdge] appropriately. Similarly, [SceneContainerArea.StartHalf]
+ * and [SceneContainerArea.EndHalf] will be resolved appropriately to
+ * [SceneContainerArea.Resolved.LeftHalf] and [SceneContainerArea.Resolved.RightHalf].
+ *
+ * @param edgeSize The fixed size of each edge.
+ */
+class SceneContainerSwipeDetector(val edgeSize: Dp) : SwipeSourceDetector {
+
+ private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+ override fun source(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): SceneContainerArea.Resolved {
+ val fixedEdge = fixedEdgeDetector.source(layoutSize, position, density, orientation)
+ return when (fixedEdge) {
+ Edge.Resolved.Left -> SceneContainerArea.Resolved.LeftEdge
+ Edge.Resolved.Bottom -> SceneContainerArea.Resolved.BottomEdge
+ Edge.Resolved.Right -> SceneContainerArea.Resolved.RightEdge
+ else -> {
+ // Note: This intentionally includes Edge.Resolved.Top. At the moment, we don't need
+ // to detect swipes on the top edge, and consider them part of the right/left half.
+ if (position.x < layoutSize.width * 0.5f) {
+ SceneContainerArea.Resolved.LeftHalf
+ } else {
+ SceneContainerArea.Resolved.RightHalf
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index fbcd8ea9b9e4..01bcc2400933 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,6 +19,7 @@ package com.android.systemui.scene.ui.viewmodel
import android.view.MotionEvent
import android.view.View
import androidx.compose.runtime.getValue
+import androidx.compose.ui.unit.dp
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
@@ -31,6 +32,8 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -62,12 +65,13 @@ constructor(
private val powerInteractor: PowerInteractor,
shadeModeInteractor: ShadeModeInteractor,
private val remoteInputInteractor: RemoteInputInteractor,
- private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
val lightRevealScrim: LightRevealScrimViewModel,
val wallpaperViewModel: WallpaperViewModel,
keyguardInteractor: KeyguardInteractor,
+ val burnIn: AodBurnInViewModel,
+ val clock: KeyguardClockViewModel,
@Assisted view: View,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -85,16 +89,20 @@ constructor(
val hapticsViewModel: SceneContainerHapticsViewModel = hapticsViewModelFactory.create(view)
/**
- * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+ * The [SwipeSourceDetector] to use for defining which areas of the screen can be defined in the
* [UserAction]s for this container.
*/
- val edgeDetector: SwipeSourceDetector by
+ val swipeSourceDetector: SwipeSourceDetector by
hydrator.hydratedStateOf(
- traceName = "edgeDetector",
+ traceName = "swipeSourceDetector",
initialValue = DefaultEdgeDetector,
source =
shadeModeInteractor.shadeMode.map {
- if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+ if (it is ShadeMode.Dual) {
+ SceneContainerSwipeDetector(edgeSize = 40.dp)
+ } else {
+ DefaultEdgeDetector
+ }
},
)
@@ -237,6 +245,7 @@ constructor(
logger.logSceneChanged(
from = fromScene,
to = toScene,
+ sceneState = null,
reason = "user interaction",
isInstant = false,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
deleted file mode 100644
index f88bcb57a27d..000000000000
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
+++ /dev/null
@@ -1,116 +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.scene.ui.viewmodel
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
-import com.android.compose.animation.scene.Edge
-import com.android.compose.animation.scene.FixedSizeEdgeDetector
-import com.android.compose.animation.scene.SwipeSource
-import com.android.compose.animation.scene.SwipeSourceDetector
-
-/**
- * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
- * top-left and top-right.
- */
-enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
- SwipeSource {
- TopLeft(resolveEdge = { Resolved.TopLeft }),
- TopRight(resolveEdge = { Resolved.TopRight }),
- TopStart(
- resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
- ),
- TopEnd(
- resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
- ),
- Bottom(resolveEdge = { Resolved.Bottom }),
- Left(resolveEdge = { Resolved.Left }),
- Right(resolveEdge = { Resolved.Right }),
- Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
- End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
-
- override fun resolve(layoutDirection: LayoutDirection): Resolved {
- return resolveEdge(layoutDirection)
- }
-
- enum class Resolved : SwipeSource.Resolved {
- TopLeft,
- TopRight,
- Bottom,
- Left,
- Right,
- }
-}
-
-/**
- * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
- * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
- * may change during runtime.
- *
- * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
- * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
- * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
- * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
- * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
- * [SceneContainerEdge.Resolved.Right].
- *
- * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
- * percentage) of screen width to consider the split point between "top-left" and "top-right"
- * edges. It is called on each source detection event.
- * @param edgeSize The fixed size of each edge.
- */
-class SplitEdgeDetector(
- val topEdgeSplitFraction: () -> Float,
- val edgeSize: Dp,
-) : SwipeSourceDetector {
-
- private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
-
- override fun source(
- layoutSize: IntSize,
- position: IntOffset,
- density: Density,
- orientation: Orientation,
- ): SceneContainerEdge.Resolved? {
- val fixedEdge =
- fixedEdgeDetector.source(
- layoutSize,
- position,
- density,
- orientation,
- )
- return when (fixedEdge) {
- Edge.Resolved.Top -> {
- val topEdgeSplitFraction = topEdgeSplitFraction()
- require(topEdgeSplitFraction in 0f..1f) {
- "topEdgeSplitFraction must return a value between 0.0 and 1.0"
- }
- val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
- if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
- else SceneContainerEdge.Resolved.TopRight
- }
- Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
- Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
- Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
- null -> null
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
index b155ada87efd..1f534a5c191a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicy.kt
@@ -111,10 +111,7 @@ constructor(
statusbarWidth: Int,
): ShadeElement {
val xPercentage = motionEvent.x / statusbarWidth
- val threshold = shadeInteractor.get().getTopEdgeSplitFraction()
- return if (xPercentage < threshold) {
- notificationElement.get()
- } else qsShadeElement.get()
+ return if (xPercentage < 0.5f) notificationElement.get() else qsShadeElement.get()
}
private fun monitorDisplayRemovals(): Job {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
index 6eaedd73ea76..2b3e4b5db453 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt
@@ -34,7 +34,11 @@ constructor(
override fun animateCollapseQs(fullyCollapse: Boolean) {
if (shadeInteractor.isQsExpanded.value) {
val key =
- if (fullyCollapse || shadeModeInteractor.isDualShade) {
+ if (
+ fullyCollapse ||
+ shadeModeInteractor.isDualShade ||
+ shadeModeInteractor.isSplitShade
+ ) {
SceneFamilies.Home
} else {
Scenes.Shade
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index c8ce316c41dd..6d68796454eb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,7 +16,6 @@
package com.android.systemui.shade.domain.interactor
-import androidx.annotation.FloatRange
import com.android.compose.animation.scene.TransitionKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -66,16 +65,6 @@ interface ShadeInteractor : BaseShadeInteractor {
* wide as the entire screen.
*/
val isShadeLayoutWide: StateFlow<Boolean>
-
- /**
- * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
- * between "top-left" and "top-right" for the purposes of dual-shade invocation.
- *
- * Note that this fraction only determines the *split* between the absolute left and right
- * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
- * will resolve to "top-left".
- */
- @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
}
/** ShadeInteractor methods with implementations that differ between non-empty impls. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index b1129a94d833..77e6a833c153 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -48,8 +48,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {
override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
- override fun getTopEdgeSplitFraction(): Float = 0.5f
-
override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
index c6752f867183..cf3b08c041be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt
@@ -20,10 +20,11 @@ import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ShowOverlay
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
+import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */
fun singleShadeActions(
@@ -66,11 +67,10 @@ fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */
fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
- val notifShadeUserActionResult = UserActionResult.ShowOverlay(Overlays.NotificationsShade)
- val qsShadeuserActionResult = UserActionResult.ShowOverlay(Overlays.QuickSettingsShade)
return arrayOf(
- Swipe.Down to notifShadeUserActionResult,
- Swipe.Down(fromSource = SceneContainerEdge.TopRight) to qsShadeuserActionResult,
+ Swipe.Down to ShowOverlay(Overlays.NotificationsShade),
+ Swipe.Down(fromSource = SceneContainerArea.EndHalf) to
+ ShowOverlay(Overlays.QuickSettingsShade),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 155049f512d8..31fdec6147f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -93,6 +93,7 @@ public class NotificationShelf extends ActivatableNotificationView {
private int mPaddingBetweenElements;
private int mNotGoneIndex;
private boolean mHasItemsInStableShelf;
+ private boolean mAlignedToEnd;
private int mScrollFastThreshold;
private boolean mInteractive;
private boolean mAnimationsEnabled = true;
@@ -412,8 +413,22 @@ public class NotificationShelf extends ActivatableNotificationView {
public boolean isAlignedToEnd() {
if (!NotificationMinimalism.isEnabled()) {
return false;
+ } else if (SceneContainerFlag.isEnabled()) {
+ return mAlignedToEnd;
+ } else {
+ return mAmbientState.getUseSplitShade();
+ }
+ }
+
+ /** @see #isAlignedToEnd() */
+ public void setAlignedToEnd(boolean alignedToEnd) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+ if (mAlignedToEnd != alignedToEnd) {
+ mAlignedToEnd = alignedToEnd;
+ requestLayout();
}
- return mAmbientState.getUseSplitShade();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 3ba0ae3b3cb6..1a30caf0150b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -214,7 +214,6 @@ constructor(
if (
secondaryChip is InternalChipModel.Active &&
StatusBarNotifChips.isEnabled &&
- !StatusBarChipsModernization.isEnabled &&
!isScreenReasonablyLarge
) {
// If we have two showing chips and we don't have a ton of room
@@ -222,8 +221,10 @@ constructor(
// possible so that we have the highest chance of showing both chips (as
// opposed to showing the primary chip with a lot of text and completely
// hiding the secondary chip).
- // Also: If StatusBarChipsModernization is enabled, then we'll do the
- // squishing in Compose instead.
+ // TODO(b/392895330): If StatusBarChipsModernization is enabled, do the
+ // squishing in Compose instead, and be smart about it (e.g. if we have
+ // room for the first chip to show text and the second chip to be icon-only,
+ // do that instead of always squishing both chips.)
InternalMultipleOngoingActivityChipsModel(
primaryChip.squish(),
secondaryChip.squish(),
@@ -237,24 +238,31 @@ constructor(
/** Squishes the chip down to the smallest content possible. */
private fun InternalChipModel.Active.squish(): InternalChipModel.Active {
- return when (model) {
+ return if (model.shouldSquish()) {
+ InternalChipModel.Active(this.type, this.model.toIconOnly())
+ } else {
+ this
+ }
+ }
+
+ private fun OngoingActivityChipModel.Active.shouldSquish(): Boolean {
+ return when (this) {
// Icon-only is already maximum squished
- is OngoingActivityChipModel.Active.IconOnly -> this
+ is OngoingActivityChipModel.Active.IconOnly,
// Countdown shows just a single digit, so already maximum squished
- is OngoingActivityChipModel.Active.Countdown -> this
- // The other chips have icon+text, so we should hide the text
+ is OngoingActivityChipModel.Active.Countdown -> false
+ // The other chips have icon+text, so we can squish them by hiding text
is OngoingActivityChipModel.Active.Timer,
is OngoingActivityChipModel.Active.ShortTimeDelta,
- is OngoingActivityChipModel.Active.Text ->
- InternalChipModel.Active(this.type, this.model.toIconOnly())
+ is OngoingActivityChipModel.Active.Text -> true
}
}
private fun OngoingActivityChipModel.Active.toIconOnly(): OngoingActivityChipModel.Active {
// If this chip doesn't have an icon, then it only has text and we should continue showing
// its text. (This is theoretically impossible because
- // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon, but protect
- // against it just in case.)
+ // [OngoingActivityChipModel.Active.Countdown] is the only chip without an icon and
+ // [shouldSquish] returns false for that model, but protect against it just in case.)
val currentIcon = icon ?: return this
return OngoingActivityChipModel.Active.IconOnly(
key,
@@ -271,8 +279,38 @@ constructor(
*/
val chips: StateFlow<MultipleOngoingActivityChipsModel> =
if (StatusBarChipsModernization.isEnabled) {
- incomingChipBundle
- .map { bundle -> rankChips(bundle) }
+ combine(
+ incomingChipBundle.map { bundle -> rankChips(bundle) },
+ isScreenReasonablyLarge,
+ ) { rankedChips, isScreenReasonablyLarge ->
+ if (
+ StatusBarNotifChips.isEnabled &&
+ !isScreenReasonablyLarge &&
+ rankedChips.active.filter { !it.isHidden }.size >= 2
+ ) {
+ // If we have at least two showing chips and we don't have a ton of room
+ // (!isScreenReasonablyLarge), then we want to make both of them as small as
+ // possible so that we have the highest chance of showing both chips (as
+ // opposed to showing the first chip with a lot of text and completely
+ // hiding the other chips).
+ val squishedActiveChips =
+ rankedChips.active.map {
+ if (!it.isHidden && it.shouldSquish()) {
+ it.toIconOnly()
+ } else {
+ it
+ }
+ }
+
+ MultipleOngoingActivityChipsModel(
+ active = squishedActiveChips,
+ overflow = rankedChips.overflow,
+ inactive = rankedChips.inactive,
+ )
+ } else {
+ rankedChips
+ }
+ }
.stateIn(scope, SharingStarted.Lazily, MultipleOngoingActivityChipsModel())
} else {
MutableStateFlow(MultipleOngoingActivityChipsModel()).asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 09cc3f23032e..9dc651ed507a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -643,6 +643,10 @@ public final class NotificationEntry extends ListEntry {
return row.isMediaRow();
}
+ public boolean containsCustomViews() {
+ return getSbn().getNotification().containsCustomViews();
+ }
+
public void resetUserExpansion() {
if (row != null) row.resetUserExpansion();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 6491223e6e10..f9e9bee4d809 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -12,7 +12,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.util.children
/** Walks view hiearchy of a given notification to estimate its memory use. */
-internal object NotificationMemoryViewWalker {
+object NotificationMemoryViewWalker {
private const val TAG = "NotificationMemory"
@@ -26,9 +26,13 @@ internal object NotificationMemoryViewWalker {
private var softwareBitmaps = 0
fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
+
fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
+
fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
+
fun addStyle(styleUse: Int) = apply { style += styleUse }
+
fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
softwareBitmaps += softwareBitmapUse
}
@@ -67,14 +71,14 @@ internal object NotificationMemoryViewWalker {
getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
getViewUsage(
ViewType.PRIVATE_CONTRACTED_VIEW,
- row.privateLayout?.contractedChild
+ row.privateLayout?.contractedChild,
),
getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
getViewUsage(
ViewType.PUBLIC_VIEW,
row.publicLayout?.expandedChild,
row.publicLayout?.contractedChild,
- row.publicLayout?.headsUpChild
+ row.publicLayout?.headsUpChild,
),
)
.filterNotNull()
@@ -107,14 +111,14 @@ internal object NotificationMemoryViewWalker {
row.publicLayout?.expandedChild,
row.publicLayout?.contractedChild,
row.publicLayout?.headsUpChild,
- seenObjects = seenObjects
+ seenObjects = seenObjects,
)
}
private fun getViewUsage(
type: ViewType,
vararg rootViews: View?,
- seenObjects: HashSet<Int> = hashSetOf()
+ seenObjects: HashSet<Int> = hashSetOf(),
): NotificationViewUsage? {
val usageBuilder = lazy { UsageBuilder() }
rootViews.forEach { rootView ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 76ba7f9ea901..2bc48746f847 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -106,7 +106,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
@Override
public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
float startVelocity) {
- cancelMagneticAnimations();
+ cancelTranslationAnimations();
mMagneticAnimator.setSpring(springForce);
mMagneticAnimator.setStartVelocity(startVelocity);
mMagneticAnimator.animateToFinalPosition(endTranslation);
@@ -114,11 +114,15 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
@Override
public void cancelMagneticAnimations() {
- cancelTranslationAnimations();
mMagneticAnimator.cancel();
}
@Override
+ public void cancelTranslationAnimations() {
+ ExpandableView.this.cancelTranslationAnimations();
+ }
+
+ @Override
public boolean canRowBeDismissed() {
return canExpandableViewBeDismissed();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index c7e15fdb98c7..73e8246907aa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -901,6 +901,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder
if (!satisfiesMinHeightRequirement(view, entry, resources)) {
return "inflated notification does not meet minimum height requirement";
}
+
+ if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+ if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+ return "inflated notification does not meet maximum memory size requirement";
+ }
+ }
+
return null;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
new file mode 100644
index 000000000000..c55cb6725e45
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.os.Build;
+
+/**
+ * Holds compat {@link ChangeId} for {@link NotificationCustomContentMemoryVerifier}.
+ */
+final class NotificationCustomContentCompat {
+ /**
+ * Enables memory size checking of custom views included in notifications to ensure that
+ * they conform to the size limit set in `config_notificationStripRemoteViewSizeBytes`
+ * config.xml parameter.
+ * Notifications exceeding the size will be rejected.
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
+ public static final long CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS = 270553691L;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
new file mode 100644
index 000000000000..a3e6a5cddc94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifier.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.compat.CompatChanges
+import android.content.Context
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.traceSection
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Checks whether Notifications with Custom content views conform to configured memory limits. */
+object NotificationCustomContentMemoryVerifier {
+
+ private const val NOTIFICATION_SERVICE_TAG = "NotificationService"
+
+ /** Notifications with custom views need to conform to maximum memory consumption. */
+ @JvmStatic
+ fun requiresImageViewMemorySizeCheck(entry: NotificationEntry): Boolean {
+ if (!com.android.server.notification.Flags.notificationCustomViewUriRestriction()) {
+ return false
+ }
+
+ return entry.containsCustomViews()
+ }
+
+ /**
+ * This walks the custom view hierarchy contained in the passed Notification view and determines
+ * if the total memory consumption of all image views satisfies the limit set by
+ * [getStripViewSizeLimit]. It will also log to logcat if the limit exceeds
+ * [getWarnViewSizeLimit].
+ *
+ * @return true if the Notification conforms to the view size limits.
+ */
+ @JvmStatic
+ fun satisfiesMemoryLimits(view: View, entry: NotificationEntry): Boolean {
+ val mainColumnView =
+ view.findViewById<View>(com.android.internal.R.id.notification_main_column)
+ if (mainColumnView == null) {
+ Log.wtf(
+ NOTIFICATION_SERVICE_TAG,
+ "R.id.notification_main_column view should not be null!",
+ )
+ return true
+ }
+
+ val memorySize =
+ traceSection("computeViewHiearchyImageViewSize") {
+ computeViewHierarchyImageViewSize(view)
+ }
+
+ if (memorySize > getStripViewSizeLimit(view.context)) {
+ val stripOversizedView = isCompatChangeEnabledForUid(entry.sbn.uid)
+ if (stripOversizedView) {
+ Log.w(
+ NOTIFICATION_SERVICE_TAG,
+ "Dropped notification due to too large RemoteViews ($memorySize bytes) on " +
+ "pkg: ${entry.sbn.packageName} tag: ${entry.sbn.tag} id: ${entry.sbn.id}",
+ )
+ } else {
+ Log.w(
+ NOTIFICATION_SERVICE_TAG,
+ "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+ "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+ "this WILL notification WILL be dropped when targetSdk " +
+ "is set to ${Build.VERSION_CODES.BAKLAVA}!",
+ )
+ }
+
+ // We still warn for size, but return "satisfies = ok" if the target SDK
+ // is too low.
+ return !stripOversizedView
+ }
+
+ if (memorySize > getWarnViewSizeLimit(view.context)) {
+ // We emit the same warning as NotificationManagerService does to keep some consistency
+ // for developers.
+ Log.w(
+ NOTIFICATION_SERVICE_TAG,
+ "RemoteViews too large on pkg: ${entry.sbn.packageName} " +
+ "tag: ${entry.sbn.tag} id: ${entry.sbn.id} " +
+ "this notifications might be dropped in a future release",
+ )
+ }
+ return true
+ }
+
+ private fun isCompatChangeEnabledForUid(uid: Int): Boolean =
+ try {
+ CompatChanges.isChangeEnabled(
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS,
+ uid,
+ )
+ } catch (e: RuntimeException) {
+ Log.wtf(NOTIFICATION_SERVICE_TAG, "Failed to contact system_server for compat change.")
+ false
+ }
+
+ @VisibleForTesting
+ @JvmStatic
+ fun computeViewHierarchyImageViewSize(view: View): Int =
+ when (view) {
+ is ViewGroup -> {
+ var use = 0
+ for (i in 0 until view.childCount) {
+ use += computeViewHierarchyImageViewSize(view.getChildAt(i))
+ }
+ use
+ }
+ is ImageView -> computeImageViewSize(view)
+ else -> 0
+ }
+
+ /**
+ * Returns the memory size of a Bitmap contained in a passed [ImageView] in bytes. If the view
+ * contains any other kind of drawable, the memory size is estimated from its intrinsic
+ * dimensions.
+ *
+ * @return Bitmap size in bytes or 0 if no drawable is set.
+ */
+ private fun computeImageViewSize(view: ImageView): Int {
+ val drawable = view.drawable
+ return computeDrawableSize(drawable)
+ }
+
+ private fun computeDrawableSize(drawable: Drawable?): Int {
+ return when (drawable) {
+ null -> 0
+ is AdaptiveIconDrawable ->
+ computeDrawableSize(drawable.foreground) +
+ computeDrawableSize(drawable.background) +
+ computeDrawableSize(drawable.monochrome)
+ is BitmapDrawable -> drawable.bitmap.allocationByteCount
+ // People can sneak large drawables into those custom memory views via resources -
+ // we use the intrisic size as a proxy for how much memory rendering those will
+ // take.
+ else -> drawable.intrinsicWidth * drawable.intrinsicHeight * 4
+ }
+ }
+
+ /** @return Size of remote views after which a size warning is logged. */
+ @VisibleForTesting
+ fun getWarnViewSizeLimit(context: Context): Int =
+ context.resources.getInteger(
+ com.android.internal.R.integer.config_notificationWarnRemoteViewSizeBytes
+ )
+
+ /** @return Size of remote views after which the notification is dropped. */
+ @VisibleForTesting
+ fun getStripViewSizeLimit(context: Context): Int =
+ context.resources.getInteger(
+ com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index bea14b2c003f..49b682d0a5d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -83,20 +83,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private static final String TAG = "InfoGuts";
private int mActualHeight;
- @IntDef(prefix = { "ACTION_" }, value = {
- ACTION_NONE,
- ACTION_TOGGLE_ALERT,
- ACTION_TOGGLE_SILENT,
- })
- public @interface NotificationInfoAction {
- }
-
- public static final int ACTION_NONE = 0;
- // standard controls
- static final int ACTION_TOGGLE_SILENT = 2;
- // standard controls
- private static final int ACTION_TOGGLE_ALERT = 5;
-
private TextView mPriorityDescriptionView;
private TextView mSilentDescriptionView;
private TextView mAutomaticDescriptionView;
@@ -123,7 +109,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
* The last importance level chosen by the user. Null if the user has not chosen an importance
* level; non-null once the user takes an action which indicates an explicit preference.
*/
- @Nullable private Integer mChosenImportance;
+ @Nullable
+ private Integer mChosenImportance;
private boolean mIsAutomaticChosen;
private boolean mIsSingleDefaultChannel;
private boolean mIsNonblockable;
@@ -143,27 +130,27 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
boolean mSkipPost = false;
// used by standard ui
- private OnClickListener mOnAutomatic = v -> {
+ private final OnClickListener mOnAutomatic = v -> {
mIsAutomaticChosen = true;
applyAlertingBehavior(BEHAVIOR_AUTOMATIC, true /* userTriggered */);
};
// used by standard ui
- private OnClickListener mOnAlert = v -> {
+ private final OnClickListener mOnAlert = v -> {
mChosenImportance = IMPORTANCE_DEFAULT;
mIsAutomaticChosen = false;
applyAlertingBehavior(BEHAVIOR_ALERTING, true /* userTriggered */);
};
// used by standard ui
- private OnClickListener mOnSilent = v -> {
+ private final OnClickListener mOnSilent = v -> {
mChosenImportance = IMPORTANCE_LOW;
mIsAutomaticChosen = false;
applyAlertingBehavior(BEHAVIOR_SILENT, true /* userTriggered */);
};
// used by standard ui
- private OnClickListener mOnDismissSettings = v -> {
+ private final OnClickListener mOnDismissSettings = v -> {
mPressedApply = true;
mGutsContainer.closeControls(v, /* save= */ true);
};
@@ -181,13 +168,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
mAutomaticDescriptionView = findViewById(R.id.automatic_summary);
}
- // Specify a CheckSaveListener to override when/if the user's changes are committed.
- public interface CheckSaveListener {
- // Invoked when importance has changed and the NotificationInfo wants to try to save it.
- // Listener should run saveImportance unless the change should be canceled.
- void checkSave(Runnable saveImportance, StatusBarNotification sbn);
- }
-
public interface OnSettingsClickListener {
void onClick(View v, NotificationChannel channel, int appUid);
}
@@ -216,7 +196,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
boolean isNonblockable,
boolean wasShownHighPriority,
AssistantFeedbackController assistantFeedbackController,
- MetricsLogger metricsLogger, OnClickListener onCloseClick)
+ MetricsLogger metricsLogger,
+ OnClickListener onCloseClick)
throws RemoteException {
mINotificationManager = iNotificationManager;
mMetricsLogger = metricsLogger;
@@ -623,7 +604,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
intent,
PackageManager.MATCH_DEFAULT_ONLY
);
- if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
+ if (resolveInfos == null || resolveInfos.isEmpty() || resolveInfos.get(0) == null) {
return null;
}
final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
@@ -758,6 +739,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
/**
* Returns a LogMaker with all available notification information.
* Caller should set category, type, and maybe subtype, before passing it to mMetricsLogger.
+ *
* @return LogMaker
*/
private LogMaker getLogMaker() {
@@ -769,10 +751,11 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
/**
* Returns an initialized LogMaker for logging importance changes.
* The caller may override the type before passing it to mMetricsLogger.
+ *
* @return LogMaker
*/
private LogMaker importanceChangeLogMaker() {
- Integer chosenImportance =
+ int chosenImportance =
mChosenImportance != null ? mChosenImportance : mStartingChannelImportance;
return getLogMaker().setCategory(MetricsEvent.ACTION_SAVE_IMPORTANCE)
.setType(MetricsEvent.TYPE_ACTION)
@@ -782,6 +765,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
/**
* Returns an initialized LogMaker for logging open/close of the info display.
* The caller may override the type before passing it to mMetricsLogger.
+ *
* @return LogMaker
*/
private LogMaker notificationControlsLogMaker() {
@@ -799,7 +783,9 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
@Retention(SOURCE)
@IntDef({BEHAVIOR_ALERTING, BEHAVIOR_SILENT, BEHAVIOR_AUTOMATIC})
- private @interface AlertingBehavior {}
+ private @interface AlertingBehavior {
+ }
+
private static final int BEHAVIOR_ALERTING = 0;
private static final int BEHAVIOR_SILENT = 1;
private static final int BEHAVIOR_AUTOMATIC = 2;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 20c3464536e9..589e5b8be240 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -1396,9 +1396,17 @@ constructor(
*/
@VisibleForTesting
fun isValidView(view: View, entry: NotificationEntry, resources: Resources): String? {
- return if (!satisfiesMinHeightRequirement(view, entry, resources)) {
- "inflated notification does not meet minimum height requirement"
- } else null
+ if (!satisfiesMinHeightRequirement(view, entry, resources)) {
+ return "inflated notification does not meet minimum height requirement"
+ }
+
+ if (NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry)) {
+ if (!NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry)) {
+ return "inflated notification does not meet maximum memory size requirement"
+ }
+ }
+
+ return null
}
private fun satisfiesMinHeightRequirement(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
index 9fdd0bcc4ee9..0703f2de250d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractor.kt
@@ -21,11 +21,14 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.NotificationShelf
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
/** Interactor for the [NotificationShelf] */
@SysUISingleton
@@ -35,6 +38,7 @@ constructor(
private val keyguardRepository: KeyguardRepository,
private val deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
private val powerInteractor: PowerInteractor,
+ private val shadeModeInteractor: ShadeModeInteractor,
private val keyguardTransitionController: LockscreenShadeTransitionController,
) {
/** Is the shelf showing on the keyguard? */
@@ -51,6 +55,16 @@ constructor(
isKeyguardShowing && isBypassEnabled
}
+ /** Should the shelf be aligned to the end in the current configuration? */
+ val isAlignedToEnd: Flow<Boolean>
+ get() =
+ shadeModeInteractor.shadeMode.map { shadeMode ->
+ when (shadeMode) {
+ ShadeMode.Split -> true
+ else -> false
+ }
+ }
+
/** Transition keyguard to the locked shade, triggered by the shelf. */
fun goToLockedShadeFromShelf() {
powerInteractor.wakeUpIfDozing("SHADE_CLICK", PowerManager.WAKE_REASON_GESTURE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 0352a304a5c1..f663ea019319 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -16,15 +16,16 @@
package com.android.systemui.statusbar.notification.shelf.ui.viewbinder
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
object NotificationShelfViewBinder {
@@ -41,6 +42,11 @@ object NotificationShelfViewBinder {
viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications)
}
launch { viewModel.isClickable.collect(::setCanInteract) }
+
+ if (SceneContainerFlag.isEnabled) {
+ launch { viewModel.isAlignedToEnd.collect(::setAlignedToEnd) }
+ }
+
registerViewListenersWhileAttached(shelf, viewModel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index 5ca8b53d0704..96cdda6d4a23 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -17,11 +17,13 @@
package com.android.systemui.statusbar.notification.shelf.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
/** ViewModel for [NotificationShelf]. */
@@ -40,6 +42,15 @@ constructor(
val canModifyColorOfNotifications: Flow<Boolean>
get() = interactor.isShelfStatic.map { static -> !static }
+ /** Is the shelf aligned to the end in the current configuration? */
+ val isAlignedToEnd: Flow<Boolean> by lazy {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ interactor.isAlignedToEnd
+ }
+ }
+
/** Notifies that the user has clicked the shelf. */
fun onShelfClicked() {
interactor.goToLockedShadeFromShelf()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index 3941700496f4..5a29a699a7e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -97,6 +97,7 @@ constructor(
stackScrollLayout,
MAGNETIC_TRANSLATION_MULTIPLIERS.size,
)
+ currentMagneticListeners.swipedListener()?.cancelTranslationAnimations()
newListeners.forEach {
if (currentMagneticListeners.contains(it)) {
it?.cancelMagneticAnimations()
@@ -214,22 +215,32 @@ constructor(
}
override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) {
- if (!row.isSwipedTarget()) return
-
- when (currentState) {
- State.PULLING -> {
- snapNeighborsBack(velocity)
- currentState = State.IDLE
- }
- State.DETACHED -> {
- currentState = State.IDLE
+ if (row.isSwipedTarget()) {
+ when (currentState) {
+ State.PULLING -> {
+ snapNeighborsBack(velocity)
+ currentState = State.IDLE
+ }
+ State.DETACHED -> {
+ // Cancel any detaching animation that may be occurring
+ currentMagneticListeners.swipedListener()?.cancelMagneticAnimations()
+ currentState = State.IDLE
+ }
+ else -> {}
}
- else -> {}
+ } else {
+ // A magnetic neighbor may be dismissing. In this case, we need to cancel any snap back
+ // magnetic animation to let the external dismiss animation proceed.
+ val listener = currentMagneticListeners.find { it == row.magneticRowListener }
+ listener?.cancelMagneticAnimations()
}
}
override fun reset() {
- currentMagneticListeners.forEach { it?.cancelMagneticAnimations() }
+ currentMagneticListeners.forEach {
+ it?.cancelMagneticAnimations()
+ it?.cancelTranslationAnimations()
+ }
currentState = State.IDLE
currentMagneticListeners = listOf()
currentRoundableTargets = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
index 46036d4c1fad..5959ef1e093b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
@@ -42,6 +42,9 @@ interface MagneticRowListener {
/** Cancel any animations related to the magnetic interactions of the row */
fun cancelMagneticAnimations()
+ /** Cancel any other animations related to the row's translation */
+ fun cancelTranslationAnimations()
+
/** Can the row be dismissed. */
fun canRowBeDismissed(): Boolean
}
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 48a6a4c057df..810d0b43b0dd 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
@@ -463,6 +463,13 @@ public class NotificationStackScrollLayoutController implements Dumpable {
}
@Override
+ public void onMagneticInteractionEnd(View view, float velocity) {
+ if (view instanceof ExpandableNotificationRow row) {
+ mMagneticNotificationRowManager.onMagneticInteractionEnd(row, velocity);
+ }
+ }
+
+ @Override
public float getTotalTranslationLength(View animView) {
return mView.getTotalTranslationLength(animView);
}
@@ -504,14 +511,6 @@ public class NotificationStackScrollLayoutController implements Dumpable {
public void onDragCancelled(View v) {
}
- @Override
- public void onDragCancelledWithVelocity(View v, float finalVelocity) {
- if (v instanceof ExpandableNotificationRow row) {
- mMagneticNotificationRowManager.onMagneticInteractionEnd(
- row, finalVelocity);
- }
- }
-
/**
* Handles cleanup after the given {@code view} has been fully swiped out (including
* re-invoking dismiss logic in case the notification has not made its way out yet).
@@ -539,10 +538,6 @@ public class NotificationStackScrollLayoutController implements Dumpable {
*/
public void handleChildViewDismissed(View view) {
- if (view instanceof ExpandableNotificationRow row) {
- mMagneticNotificationRowManager.onMagneticInteractionEnd(
- row, null /* velocity */);
- }
// The View needs to clean up the Swipe states, e.g. roundness.
mView.onSwipeEnd();
if (mView.getClearAllInProgress()) {
@@ -614,11 +609,15 @@ public class NotificationStackScrollLayoutController implements Dumpable {
@Override
public void onBeginDrag(View v) {
+ mView.onSwipeBegin(v);
+ }
+
+ @Override
+ public void setMagneticAndRoundableTargets(View v) {
if (v instanceof ExpandableNotificationRow row) {
mMagneticNotificationRowManager.setMagneticAndRoundableTargets(
row, mView, mSectionsManager);
}
- mView.onSwipeBegin(v);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index d476d482226d..6f4047f48205 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -362,7 +362,8 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc
superSnapChild(animView, targetLeft, velocity);
}
- mCallback.onDragCancelledWithVelocity(animView, velocity);
+ mCallback.onMagneticInteractionEnd(animView, velocity);
+ mCallback.onDragCancelled(animView);
if (targetLeft == 0) {
handleMenuCoveredOrDismissed();
}
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 1bcc5adea6e8..54efa4a2bcf2 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
@@ -478,7 +478,7 @@ constructor(
/**
* Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
- * notifications unless in splitshade.
+ * notifications unless it's a large screen.
*/
private val alphaForShadeAndQsExpansion: Flow<Float> =
if (SceneContainerFlag.isEnabled) {
@@ -501,16 +501,26 @@ constructor(
Split -> isAnyExpanded.filter { it }.map { 1f }
Dual ->
combineTransform(
+ shadeModeInteractor.isShadeLayoutWide,
headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
shadeInteractor.shadeExpansion,
shadeInteractor.qsExpansion,
- ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion ->
- if (isHeadsUpOrAnimatingAway) {
+ ) {
+ isShadeLayoutWide,
+ isHeadsUpOrAnimatingAway,
+ shadeExpansion,
+ qsExpansion ->
+ if (isShadeLayoutWide) {
+ if (shadeExpansion > 0f) {
+ emit(1f)
+ }
+ } else if (isHeadsUpOrAnimatingAway) {
// Ensure HUNs will be visible in QS shade (at least while
// unlocked)
emit(1f)
} else if (shadeExpansion > 0f || qsExpansion > 0f) {
- // Fade out as QS shade expands
+ // On a narrow screen, the QS shade overlaps with lockscreen
+ // notifications. Fade them out as the QS shade expands.
emit(1f - qsExpansion)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index a339bc98457e..58326dbb3a34 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -61,6 +61,7 @@ import kotlinx.coroutines.flow.StateFlowKt;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Objects;
/**
* The header group on Keyguard.
@@ -103,6 +104,9 @@ public class KeyguardStatusBarView extends RelativeLayout {
*/
private int mCutoutSideNudge = 0;
+ @Nullable
+ private WindowInsets mPreviousInsets = null;
+
private DisplayCutout mDisplayCutout;
private int mRoundedCornerPadding = 0;
// right and left padding applied to this view to account for cutouts and rounded corners
@@ -284,9 +288,12 @@ public class KeyguardStatusBarView extends RelativeLayout {
WindowInsets updateWindowInsets(
WindowInsets insets,
StatusBarContentInsetsProvider insetsProvider) {
- mLayoutState = LAYOUT_NONE;
- if (updateLayoutConsideringCutout(insetsProvider)) {
- requestLayout();
+ if (!Objects.equals(mPreviousInsets, insets)) {
+ mLayoutState = LAYOUT_NONE;
+ if (updateLayoutConsideringCutout(insetsProvider)) {
+ requestLayout();
+ }
+ mPreviousInsets = new WindowInsets(insets);
}
return super.onApplyWindowInsets(insets);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index b2c4ef95242b..01de925f3d78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -65,6 +65,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
import com.android.systemui.bouncer.ui.BouncerView;
import com.android.systemui.bouncer.util.BouncerTestUtilsKt;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
@@ -170,6 +171,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
private final Lazy<SceneInteractor> mSceneInteractorLazy;
private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy;
private final DismissCallbackRegistry mDismissCallbackRegistry;
+ private final CommunalSceneInteractor mCommunalSceneInteractor;
private Job mListenForAlternateBouncerTransitionSteps = null;
private Job mListenForKeyguardAuthenticatedBiometricsHandled = null;
@@ -406,7 +408,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
@Main DelayableExecutor executor,
Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
DismissCallbackRegistry dismissCallbackRegistry,
- Lazy<BouncerInteractor> bouncerInteractor
+ Lazy<BouncerInteractor> bouncerInteractor,
+ CommunalSceneInteractor communalSceneInteractor
) {
mContext = context;
mExecutor = executor;
@@ -443,6 +446,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor;
mDeviceEntryInteractorLazy = deviceEntryInteractorLazy;
mDismissCallbackRegistry = dismissCallbackRegistry;
+ mCommunalSceneInteractor = communalSceneInteractor;
}
KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -1364,11 +1368,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
}
mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
- boolean hideBouncerOverDream = isBouncerShowing()
- && mDreamOverlayStateController.isOverlayActive();
+ boolean hideBouncerOverDreamOrHub = isBouncerShowing()
+ && (mDreamOverlayStateController.isOverlayActive()
+ || mCommunalSceneInteractor.isIdleOnCommunal().getValue());
mCentralSurfaces.endAffordanceLaunch();
// The second condition is for SIM card locked bouncer
- if (hideBouncerOverDream || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) {
+ if (hideBouncerOverDreamOrHub
+ || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) {
hideBouncer(false);
updateStates();
} else {
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 e33baf7c33ae..ded964d8a1cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -57,11 +57,11 @@ import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeDisplayAware;
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
import com.android.systemui.statusbar.CommandQueue;
@@ -76,11 +76,11 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowDragController;
import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.wmshell.BubblesManager;
@@ -115,7 +115,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
private final static String TAG = "StatusBarNotificationActivityStarter";
private final Context mContext;
- private final int mDisplayId;
private final Handler mMainThreadHandler;
private final Executor mUiBgExecutor;
@@ -155,8 +154,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
@Inject
StatusBarNotificationActivityStarter(
- Context context,
- @DisplayId int displayId,
+ @ShadeDisplayAware Context context,
Handler mainThreadHandler,
@Background Executor uiBgExecutor,
NotificationVisibilityProvider visibilityProvider,
@@ -189,7 +187,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
PowerInteractor powerInteractor,
UserTracker userTracker) {
mContext = context;
- mDisplayId = displayId;
mMainThreadHandler = mainThreadHandler;
mUiBgExecutor = uiBgExecutor;
mVisibilityProvider = visibilityProvider;
@@ -493,6 +490,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
boolean animate,
boolean isActivityIntent) {
mLogger.logStartNotificationIntent(entry);
+ final int displayId = mContext.getDisplayId();
try {
ActivityTransitionAnimator.Controller animationController =
new StatusBarTransitionAnimatorController(
@@ -501,7 +499,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mShadeController,
mNotificationShadeWindowController,
mCommandQueue,
- mDisplayId,
+ displayId,
isActivityIntent);
mActivityTransitionAnimator.startPendingIntentWithAnimation(
animationController,
@@ -511,11 +509,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
long eventTime = row.getAndResetLastActionUpTime();
Bundle options = eventTime > 0
? getActivityOptions(
- mDisplayId,
+ displayId,
adapter,
mKeyguardStateController.isShowing(),
eventTime)
- : getActivityOptions(mDisplayId, adapter);
+ : getActivityOptions(displayId, adapter);
int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
null, null, options);
mLogger.logSendPendingIntent(entry, intent, result);
@@ -533,6 +531,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
public void startNotificationGutsIntent(@NonNull final Intent intent, final int appUid,
@NonNull ExpandableNotificationRow row) {
boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
+ final int displayId = mContext.getDisplayId();
ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
@Override
public boolean onDismiss() {
@@ -544,7 +543,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mShadeController,
mNotificationShadeWindowController,
mCommandQueue,
- mDisplayId,
+ displayId,
true /* isActivityIntent */);
mActivityTransitionAnimator.startIntentWithAnimation(
@@ -552,7 +551,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
(adapter) -> TaskStackBuilder.create(mContext)
.addNextIntentWithParentStack(intent)
.startActivities(getActivityOptions(
- mDisplayId,
+ displayId,
adapter),
new UserHandle(UserHandle.getUserId(appUid))));
});
@@ -571,6 +570,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
@Override
public void startHistoryIntent(View view, boolean showHistory) {
ModesEmptyShadeFix.assertInLegacyMode();
+ final int displayId = mContext.getDisplayId();
boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
@Override
@@ -597,13 +597,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mShadeController,
mNotificationShadeWindowController,
mCommandQueue,
- mDisplayId,
+ displayId,
true /* isActivityIntent */);
mActivityTransitionAnimator.startIntentWithAnimation(
animationController, animate, intent.getPackage(),
(adapter) -> tsb.startActivities(
- getActivityOptions(mDisplayId, adapter),
+ getActivityOptions(displayId, adapter),
mUserTracker.getUserHandle()));
});
return true;
@@ -620,6 +620,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
@Override
public void startSettingsIntent(@NonNull View view, @NonNull SettingsIntent intentInfo) {
+ final int displayId = mContext.getDisplayId();
boolean animate = mActivityStarter.shouldAnimateLaunch(true /* isActivityIntent */);
ActivityStarter.OnDismissAction onDismissAction = new ActivityStarter.OnDismissAction() {
@Override
@@ -642,13 +643,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit
mShadeController,
mNotificationShadeWindowController,
mCommandQueue,
- mDisplayId,
+ displayId,
true /* isActivityIntent */);
mActivityTransitionAnimator.startIntentWithAnimation(
animationController, animate, intentInfo.getTargetIntent().getPackage(),
(adapter) -> tsb.startActivities(
- getActivityOptions(mDisplayId, adapter),
+ getActivityOptions(displayId, adapter),
mUserTracker.getUserHandle()));
});
return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
index 72d093c65a91..9f05850f3405 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SplitShadeStateController.kt
@@ -22,11 +22,11 @@ interface SplitShadeStateController {
/** Returns true if the device should use the split notification shade. */
@Deprecated(
- message = "This is deprecated, please use ShadeInteractor#shadeMode instead",
+ message = "This is deprecated, please use ShadeModeInteractor#shadeMode instead",
replaceWith =
ReplaceWith(
- "shadeInteractor.shadeMode",
- "com.android.systemui.shade.domain.interactor.ShadeInteractor",
+ "shadeModeInteractor.shadeMode",
+ "com.android.systemui.shade.domain.interactor.ShadeModeInteractor",
),
)
fun shouldUseSplitNotificationShade(resources: Resources): Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
index 0ec996be72de..9b4902a9e7b2 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS
@@ -6,5 +6,4 @@ madym@google.com
mgalhardo@google.com
petrcermak@google.com
stevenckng@google.com
-tkachenkoi@google.com
-vanjan@google.com \ No newline at end of file
+vanjan@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
index f5aac720fd47..cd401d5deb6e 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/DisplaySwitchLatencyTracker.kt
@@ -19,8 +19,11 @@ package com.android.systemui.unfold
import android.content.Context
import android.hardware.devicestate.DeviceStateManager
import android.util.Log
+import androidx.annotation.VisibleForTesting
import com.android.app.tracing.TraceUtils.traceAsync
import com.android.app.tracing.instantForTrack
+import com.android.internal.util.LatencyTracker
+import com.android.internal.util.LatencyTracker.ACTION_SWITCH_DISPLAY_UNFOLD
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -30,10 +33,12 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.ScreenPowerState
import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessModel
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
import com.android.systemui.util.Compile
import com.android.systemui.util.Utils.isDeviceFoldable
@@ -42,17 +47,23 @@ import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.race
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.time.measureTimeMillis
-import java.time.Duration
import java.util.concurrent.Executor
import javax.inject.Inject
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import com.android.app.tracing.coroutines.launchTraced as launch
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.timeout
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
/**
@@ -73,63 +84,96 @@ constructor(
@Application private val applicationScope: CoroutineScope,
private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger,
private val systemClock: SystemClock,
- private val deviceStateManager: DeviceStateManager
+ private val deviceStateManager: DeviceStateManager,
+ private val latencyTracker: LatencyTracker,
) : CoreStartable {
private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher()
private val isAodEnabled: Boolean
get() = keyguardInteractor.isAodAvailable.value
+ private val displaySwitchStarted =
+ deviceStateRepository.state.pairwise().filter {
+ // Start tracking only when the foldable device is
+ // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
+ foldableDeviceState ->
+ foldableDeviceState.previousValue == DeviceState.FOLDED ||
+ foldableDeviceState.newValue == DeviceState.FOLDED
+ }
+
+ private var startOrEndEvent: Flow<Any> = merge(displaySwitchStarted, anyEndEventFlow())
+
+ private var isCoolingDown = false
+
override fun start() {
if (!isDeviceFoldable(context.resources, deviceStateManager)) {
return
}
applicationScope.launch(context = backgroundDispatcher) {
- deviceStateRepository.state
- .pairwise()
- .filter {
- // Start tracking only when the foldable device is
- // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or
- // unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
- foldableDeviceState ->
- foldableDeviceState.previousValue == DeviceState.FOLDED ||
- foldableDeviceState.newValue == DeviceState.FOLDED
+ displaySwitchStarted.collectLatest { (previousState, newState) ->
+ if (isCoolingDown) return@collectLatest
+ if (previousState == DeviceState.FOLDED) {
+ latencyTracker.onActionStart(ACTION_SWITCH_DISPLAY_UNFOLD)
+ instantForTrack(TAG) { "unfold latency tracking started" }
}
- .flatMapLatest { foldableDeviceState ->
- flow {
- var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent()
- val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt()
- displaySwitchLatencyEvent =
- displaySwitchLatencyEvent.withBeforeFields(
- foldableDeviceState.previousValue.toStatsInt()
- )
-
+ try {
+ withTimeout(SCREEN_EVENT_TIMEOUT) {
+ val event =
+ DisplaySwitchLatencyEvent().withBeforeFields(previousState.toStatsInt())
val displaySwitchTimeMs =
measureTimeMillis(systemClock) {
- try {
- withTimeout(SCREEN_EVENT_TIMEOUT) {
- traceAsync(TAG, "displaySwitch") {
- waitForDisplaySwitch(toFoldableDeviceState)
- }
- }
- } catch (e: TimeoutCancellationException) {
- Log.e(TAG, "Wait for display switch timed out")
+ traceAsync(TAG, "displaySwitch") {
+ waitForDisplaySwitch(newState.toStatsInt())
}
}
-
- displaySwitchLatencyEvent =
- displaySwitchLatencyEvent.withAfterFields(
- toFoldableDeviceState,
- displaySwitchTimeMs.toInt(),
- getCurrentState()
- )
- emit(displaySwitchLatencyEvent)
+ if (previousState == DeviceState.FOLDED) {
+ latencyTracker.onActionEnd(ACTION_SWITCH_DISPLAY_UNFOLD)
+ }
+ logDisplaySwitchEvent(event, newState, displaySwitchTimeMs)
}
+ } catch (e: TimeoutCancellationException) {
+ instantForTrack(TAG) { "tracking timed out" }
+ latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+ } catch (e: CancellationException) {
+ instantForTrack(TAG) { "new state interrupted, entering cool down" }
+ latencyTracker.onActionCancel(ACTION_SWITCH_DISPLAY_UNFOLD)
+ startCoolDown()
}
- .collect { displaySwitchLatencyLogger.log(it) }
+ }
}
}
+ @OptIn(FlowPreview::class)
+ private fun startCoolDown() {
+ if (isCoolingDown) return
+ isCoolingDown = true
+ applicationScope.launch(context = backgroundDispatcher) {
+ val startTime = systemClock.elapsedRealtime()
+ try {
+ startOrEndEvent.timeout(COOL_DOWN_DURATION).collect()
+ } catch (e: TimeoutCancellationException) {
+ instantForTrack(TAG) {
+ "cool down finished, lasted ${systemClock.elapsedRealtime() - startTime} ms"
+ }
+ isCoolingDown = false
+ }
+ }
+ }
+
+ private fun logDisplaySwitchEvent(
+ event: DisplaySwitchLatencyEvent,
+ toFoldableDeviceState: DeviceState,
+ displaySwitchTimeMs: Long,
+ ) {
+ displaySwitchLatencyLogger.log(
+ event.withAfterFields(
+ toFoldableDeviceState.toStatsInt(),
+ displaySwitchTimeMs.toInt(),
+ getCurrentState(),
+ )
+ )
+ }
+
private fun DeviceState.toStatsInt(): Int =
when (this) {
DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED
@@ -152,9 +196,20 @@ constructor(
}
}
+ private fun anyEndEventFlow(): Flow<Any> {
+ val unfoldStatus =
+ unfoldTransitionInteractor.unfoldTransitionStatus.filter { it is TransitionStarted }
+ // dropping first emission as we're only interested in new emissions, not current state
+ val screenOn =
+ powerInteractor.screenPowerState.drop(1).filter { it == ScreenPowerState.SCREEN_ON }
+ val goToSleep =
+ powerInteractor.detailedWakefulness.drop(1).filter { sleepWithScreenOff(it) }
+ return merge(screenOn, goToSleep, unfoldStatus)
+ }
+
private fun shouldWaitForTransitionStart(
toFoldableDeviceState: Int,
- isTransitionEnabled: Boolean
+ isTransitionEnabled: Boolean,
): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled)
private suspend fun waitForScreenTurnedOn() {
@@ -165,12 +220,13 @@ constructor(
private suspend fun waitForGoToSleepWithScreenOff() {
traceAsync(TAG, "waitForGoToSleepWithScreenOff()") {
- powerInteractor.detailedWakefulness
- .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled }
- .first()
+ powerInteractor.detailedWakefulness.filter { sleepWithScreenOff(it) }.first()
}
}
+ private fun sleepWithScreenOff(model: WakefulnessModel) =
+ model.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled
+
private fun getCurrentState(): Int =
when {
isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD
@@ -205,7 +261,7 @@ constructor(
private fun DisplaySwitchLatencyEvent.withAfterFields(
toFoldableDeviceState: Int,
displaySwitchTimeMs: Int,
- toState: Int
+ toState: Int,
): DisplaySwitchLatencyEvent {
log {
"toFoldableDeviceState=$toFoldableDeviceState, " +
@@ -217,7 +273,7 @@ constructor(
return copy(
toFoldableDeviceState = toFoldableDeviceState,
latencyMs = displaySwitchTimeMs,
- toState = toState
+ toState = toState,
)
}
@@ -250,14 +306,15 @@ constructor(
val hallSensorToFirstHingeAngleChangeMs: Int = VALUE_UNKNOWN,
val hallSensorToDeviceStateChangeMs: Int = VALUE_UNKNOWN,
val onScreenTurningOnToOnDrawnMs: Int = VALUE_UNKNOWN,
- val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN
+ val onDrawnToOnScreenTurnedOnMs: Int = VALUE_UNKNOWN,
)
companion object {
private const val VALUE_UNKNOWN = -1
private const val TAG = "DisplaySwitchLatency"
private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
- private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis()
+ @VisibleForTesting val SCREEN_EVENT_TIMEOUT = 15.seconds
+ @VisibleForTesting val COOL_DOWN_DURATION = 2.seconds
private const val FOLDABLE_DEVICE_STATE_UNKNOWN =
SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt
new file mode 100644
index 000000000000..6ac0bb168f18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/NoCooldownDisplaySwitchLatencyTracker.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.unfold
+
+import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import android.util.Log
+import com.android.app.tracing.TraceUtils.traceAsync
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.app.tracing.instantForTrack
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.unfoldLatencyTrackingFix
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.unfold.DisplaySwitchLatencyTracker.DisplaySwitchLatencyEvent
+import com.android.systemui.unfold.dagger.UnfoldSingleThreadBg
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import com.android.systemui.util.Compile
+import com.android.systemui.util.Utils.isDeviceFoldable
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.race
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.measureTimeMillis
+import java.time.Duration
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Old version of [DisplaySwitchLatencyTracker] tracking only [DisplaySwitchLatencyEvent]. Which
+ * version is used for tracking depends on [unfoldLatencyTrackingFix] flag.
+ */
+@SysUISingleton
+class NoCooldownDisplaySwitchLatencyTracker
+@Inject
+constructor(
+ private val context: Context,
+ private val deviceStateRepository: DeviceStateRepository,
+ private val powerInteractor: PowerInteractor,
+ private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
+ private val animationStatusRepository: AnimationStatusRepository,
+ private val keyguardInteractor: KeyguardInteractor,
+ @UnfoldSingleThreadBg private val singleThreadBgExecutor: Executor,
+ @Application private val applicationScope: CoroutineScope,
+ private val displaySwitchLatencyLogger: DisplaySwitchLatencyLogger,
+ private val systemClock: SystemClock,
+ private val deviceStateManager: DeviceStateManager,
+) : CoreStartable {
+
+ private val backgroundDispatcher = singleThreadBgExecutor.asCoroutineDispatcher()
+ private val isAodEnabled: Boolean
+ get() = keyguardInteractor.isAodAvailable.value
+
+ override fun start() {
+ if (!isDeviceFoldable(context.resources, deviceStateManager)) {
+ return
+ }
+ applicationScope.launch(context = backgroundDispatcher) {
+ deviceStateRepository.state
+ .pairwise()
+ .filter {
+ // Start tracking only when the foldable device is
+ // folding(UNFOLDED/HALF_FOLDED -> FOLDED) or
+ // unfolding(FOLDED -> HALF_FOLD/UNFOLDED)
+ foldableDeviceState ->
+ foldableDeviceState.previousValue == DeviceState.FOLDED ||
+ foldableDeviceState.newValue == DeviceState.FOLDED
+ }
+ .flatMapLatest { foldableDeviceState ->
+ flow {
+ var displaySwitchLatencyEvent = DisplaySwitchLatencyEvent()
+ val toFoldableDeviceState = foldableDeviceState.newValue.toStatsInt()
+ displaySwitchLatencyEvent =
+ displaySwitchLatencyEvent.withBeforeFields(
+ foldableDeviceState.previousValue.toStatsInt()
+ )
+
+ val displaySwitchTimeMs =
+ measureTimeMillis(systemClock) {
+ try {
+ withTimeout(SCREEN_EVENT_TIMEOUT) {
+ traceAsync(TAG, "displaySwitch") {
+ waitForDisplaySwitch(toFoldableDeviceState)
+ }
+ }
+ } catch (e: TimeoutCancellationException) {
+ Log.e(TAG, "Wait for display switch timed out")
+ }
+ }
+
+ displaySwitchLatencyEvent =
+ displaySwitchLatencyEvent.withAfterFields(
+ toFoldableDeviceState,
+ displaySwitchTimeMs.toInt(),
+ getCurrentState(),
+ )
+ emit(displaySwitchLatencyEvent)
+ }
+ }
+ .collect { displaySwitchLatencyLogger.log(it) }
+ }
+ }
+
+ private fun DeviceState.toStatsInt(): Int =
+ when (this) {
+ DeviceState.FOLDED -> FOLDABLE_DEVICE_STATE_CLOSED
+ DeviceState.HALF_FOLDED -> FOLDABLE_DEVICE_STATE_HALF_OPEN
+ DeviceState.UNFOLDED -> FOLDABLE_DEVICE_STATE_OPEN
+ DeviceState.CONCURRENT_DISPLAY -> FOLDABLE_DEVICE_STATE_FLIPPED
+ else -> FOLDABLE_DEVICE_STATE_UNKNOWN
+ }
+
+ private suspend fun waitForDisplaySwitch(toFoldableDeviceState: Int) {
+ val isTransitionEnabled =
+ unfoldTransitionInteractor.isAvailable &&
+ animationStatusRepository.areAnimationsEnabled().first()
+ if (shouldWaitForTransitionStart(toFoldableDeviceState, isTransitionEnabled)) {
+ traceAsync(TAG, "waitForTransitionStart()") {
+ unfoldTransitionInteractor.waitForTransitionStart()
+ }
+ } else {
+ race({ waitForScreenTurnedOn() }, { waitForGoToSleepWithScreenOff() })
+ }
+ }
+
+ private fun shouldWaitForTransitionStart(
+ toFoldableDeviceState: Int,
+ isTransitionEnabled: Boolean,
+ ): Boolean = (toFoldableDeviceState != FOLDABLE_DEVICE_STATE_CLOSED && isTransitionEnabled)
+
+ private suspend fun waitForScreenTurnedOn() {
+ traceAsync(TAG, "waitForScreenTurnedOn()") {
+ powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first()
+ }
+ }
+
+ private suspend fun waitForGoToSleepWithScreenOff() {
+ traceAsync(TAG, "waitForGoToSleepWithScreenOff()") {
+ powerInteractor.detailedWakefulness
+ .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP && !isAodEnabled }
+ .first()
+ }
+ }
+
+ private fun getCurrentState(): Int =
+ when {
+ isStateAod() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__AOD
+ isStateScreenOff() -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__SCREEN_OFF
+ else -> SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__TO_STATE__UNKNOWN
+ }
+
+ private fun isStateAod(): Boolean = (isAsleepDueToFold() && isAodEnabled)
+
+ private fun isStateScreenOff(): Boolean = (isAsleepDueToFold() && !isAodEnabled)
+
+ private fun isAsleepDueToFold(): Boolean {
+ val lastWakefulnessEvent = powerInteractor.detailedWakefulness.value
+
+ return (lastWakefulnessEvent.isAsleep() &&
+ (lastWakefulnessEvent.lastSleepReason == WakeSleepReason.FOLD))
+ }
+
+ private inline fun log(msg: () -> String) {
+ if (DEBUG) Log.d(TAG, msg())
+ }
+
+ private fun DisplaySwitchLatencyEvent.withBeforeFields(
+ fromFoldableDeviceState: Int
+ ): DisplaySwitchLatencyEvent {
+ log { "fromFoldableDeviceState=$fromFoldableDeviceState" }
+ instantForTrack(TAG) { "fromFoldableDeviceState=$fromFoldableDeviceState" }
+
+ return copy(fromFoldableDeviceState = fromFoldableDeviceState)
+ }
+
+ private fun DisplaySwitchLatencyEvent.withAfterFields(
+ toFoldableDeviceState: Int,
+ displaySwitchTimeMs: Int,
+ toState: Int,
+ ): DisplaySwitchLatencyEvent {
+ log {
+ "toFoldableDeviceState=$toFoldableDeviceState, " +
+ "toState=$toState, " +
+ "latencyMs=$displaySwitchTimeMs"
+ }
+ instantForTrack(TAG) { "toFoldableDeviceState=$toFoldableDeviceState, toState=$toState" }
+
+ return copy(
+ toFoldableDeviceState = toFoldableDeviceState,
+ latencyMs = displaySwitchTimeMs,
+ toState = toState,
+ )
+ }
+
+ companion object {
+ private const val VALUE_UNKNOWN = -1
+ private const val TAG = "DisplaySwitchLatency"
+ private val DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE)
+ private val SCREEN_EVENT_TIMEOUT = Duration.ofMillis(15000).toMillis()
+
+ private const val FOLDABLE_DEVICE_STATE_UNKNOWN =
+ SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_UNKNOWN
+ const val FOLDABLE_DEVICE_STATE_CLOSED =
+ SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_CLOSED
+ const val FOLDABLE_DEVICE_STATE_HALF_OPEN =
+ SysUiStatsLog
+ .DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_HALF_OPENED
+ private const val FOLDABLE_DEVICE_STATE_OPEN =
+ SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_OPENED
+ private const val FOLDABLE_DEVICE_STATE_FLIPPED =
+ SysUiStatsLog.DISPLAY_SWITCH_LATENCY_TRACKED__FROM_FOLDABLE_DEVICE_STATE__STATE_FLIPPED
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
index f806a5c52d5a..9248cc801227 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLatencyTracker.kt
@@ -22,6 +22,7 @@ import android.hardware.devicestate.DeviceStateManager
import android.os.Trace
import android.util.Log
import com.android.internal.util.LatencyTracker
+import com.android.systemui.Flags.unfoldLatencyTrackingFix
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
@@ -63,7 +64,7 @@ constructor(
/** Registers for relevant events only if the device is foldable. */
fun init() {
- if (!isFoldable) {
+ if (unfoldLatencyTrackingFix() || !isFoldable) {
return
}
deviceStateManager.registerCallback(uiBgExecutor, foldStateListener)
@@ -85,7 +86,7 @@ constructor(
if (DEBUG) {
Log.d(
TAG,
- "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+ "onScreenTurnedOn: folded = $folded, isTransitionEnabled = $isTransitionEnabled",
)
}
@@ -109,7 +110,7 @@ constructor(
if (DEBUG) {
Log.d(
TAG,
- "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled"
+ "onTransitionStarted: folded = $folded, isTransitionEnabled = $isTransitionEnabled",
)
}
@@ -161,7 +162,7 @@ constructor(
Log.d(
TAG,
"Starting ACTION_SWITCH_DISPLAY_UNFOLD, " +
- "isTransitionEnabled = $isTransitionEnabled"
+ "isTransitionEnabled = $isTransitionEnabled",
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
index 885a2b0d7305..c2f86a37c6d8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
@@ -21,6 +21,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
@@ -48,6 +49,9 @@ constructor(
val isAvailable: Boolean
get() = repository.isAvailable
+ /** Flow of latest [UnfoldTransitionStatus] changes */
+ val unfoldTransitionStatus: Flow<UnfoldTransitionStatus> = repository.transitionStatus
+
/**
* This mapping emits 1 when the device is completely unfolded and 0.0 when the device is
* completely folded.
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 ad97b21ea60b..c960b5525d96 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
@@ -21,6 +21,7 @@ import android.annotation.SuppressLint
import android.annotation.UserIdInt
import android.app.admin.DevicePolicyManager
import android.content.Context
+import android.content.Intent
import android.content.IntentFilter
import android.content.pm.UserInfo
import android.content.res.Resources
@@ -84,6 +85,9 @@ interface UserRepository {
/** [UserInfo] of the currently-selected user. */
val selectedUserInfo: Flow<UserInfo>
+ /** Tracks whether the main user is unlocked. */
+ fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean>
+
/** User ID of the main user. */
val mainUserId: Int
@@ -284,6 +288,18 @@ constructor(
}
.stateIn(applicationScope, SharingStarted.Eagerly, false)
+ override fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(Intent.ACTION_USER_UNLOCKED))
+ .map { getUnlockedState(userHandle) }
+ .onStart { emit(getUnlockedState(userHandle)) }
+
+ private suspend fun getUnlockedState(userHandle: UserHandle?): Boolean {
+ return withContext(backgroundDispatcher) {
+ userHandle?.let { user -> manager.isUserUnlocked(user) } ?: false
+ }
+ }
+
@SuppressLint("MissingPermission")
override val isLogoutToSystemUserEnabled: StateFlow<Boolean> =
selectedUser
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
new file mode 100644
index 000000000000..ef29a387e06f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.user.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class UserLockedInteractor @Inject constructor(val userRepository: UserRepository) {
+ fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
+ userRepository.isUserUnlocked(userHandle)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
index 4b01ded16495..f1abf105be8e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt
@@ -25,7 +25,7 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
/**
- * Repository for observing values of [Settings.Secure] for the currently active user. That means
+ * Repository for observing values of [Settings.System] for the currently active user. That means
* when user is switched and the new user has different value, flow will emit new value.
*/
// TODO: b/377244768 - Make internal once call sites inject SystemSettingsRepository instead.
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 68bffeefb0f0..4d5477052388 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -37,8 +37,6 @@ import android.media.IAudioService;
import android.media.IVolumeController;
import android.media.MediaRouter2Manager;
import android.media.VolumePolicy;
-import android.media.session.MediaController.PlaybackInfo;
-import android.media.session.MediaSession.Token;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerExecutor;
@@ -61,6 +59,7 @@ import androidx.lifecycle.Observer;
import com.android.internal.annotations.GuardedBy;
import com.android.settingslib.volume.MediaSessions;
+import com.android.settingslib.volume.MediaSessions.SessionId;
import com.android.systemui.Dumpable;
import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -1402,12 +1401,13 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
protected final class MediaSessionsCallbacks implements MediaSessions.Callbacks {
- private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
+ private final HashMap<SessionId, Integer> mRemoteStreams = new HashMap<>();
private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
@Override
- public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) {
+ public void onRemoteUpdate(
+ SessionId token, String name, MediaSessions.VolumeInfo volumeInfo) {
addStream(token, "onRemoteUpdate");
int stream = 0;
@@ -1415,14 +1415,15 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
stream = mRemoteStreams.get(token);
}
Slog.d(TAG,
- "onRemoteUpdate: stream: " + stream + " volume: " + pi.getCurrentVolume());
+ "onRemoteUpdate: stream: "
+ + stream + " volume: " + volumeInfo.getCurrentVolume());
boolean changed = mState.states.indexOfKey(stream) < 0;
final StreamState ss = streamStateW(stream);
ss.dynamic = true;
ss.levelMin = 0;
- ss.levelMax = pi.getMaxVolume();
- if (ss.level != pi.getCurrentVolume()) {
- ss.level = pi.getCurrentVolume();
+ ss.levelMax = volumeInfo.getMaxVolume();
+ if (ss.level != volumeInfo.getCurrentVolume()) {
+ ss.level = volumeInfo.getCurrentVolume();
changed = true;
}
if (!Objects.equals(ss.remoteLabel, name)) {
@@ -1437,11 +1438,11 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
@Override
- public void onRemoteVolumeChanged(Token token, int flags) {
- addStream(token, "onRemoteVolumeChanged");
+ public void onRemoteVolumeChanged(SessionId sessionId, int flags) {
+ addStream(sessionId, "onRemoteVolumeChanged");
int stream = 0;
synchronized (mRemoteStreams) {
- stream = mRemoteStreams.get(token);
+ stream = mRemoteStreams.get(sessionId);
}
final boolean showUI = shouldShowUI(flags);
Slog.d(TAG, "onRemoteVolumeChanged: stream: " + stream + " showui? " + showUI);
@@ -1459,7 +1460,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
@Override
- public void onRemoteRemoved(Token token) {
+ public void onRemoteRemoved(SessionId token) {
int stream;
synchronized (mRemoteStreams) {
if (!mRemoteStreams.containsKey(token)) {
@@ -1480,7 +1481,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
}
public void setStreamVolume(int stream, int level) {
- final Token token = findToken(stream);
+ final SessionId token = findToken(stream);
if (token == null) {
Log.w(TAG, "setStreamVolume: No token found for stream: " + stream);
return;
@@ -1488,9 +1489,9 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
mMediaSessions.setVolume(token, level);
}
- private Token findToken(int stream) {
+ private SessionId findToken(int stream) {
synchronized (mRemoteStreams) {
- for (Map.Entry<Token, Integer> entry : mRemoteStreams.entrySet()) {
+ for (Map.Entry<SessionId, Integer> entry : mRemoteStreams.entrySet()) {
if (entry.getValue().equals(stream)) {
return entry.getKey();
}
@@ -1499,7 +1500,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
return null;
}
- private void addStream(Token token, String triggeringMethod) {
+ private void addStream(SessionId token, String triggeringMethod) {
synchronized (mRemoteStreams) {
if (!mRemoteStreams.containsKey(token)) {
mRemoteStreams.put(token, mNextStream);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 83b7c1818341..86defff4a120 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -68,7 +68,7 @@ constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.volume_dialog)
- requireViewById<View>(R.id.volume_dialog_root).repeatWhenAttached {
+ requireViewById<View>(R.id.volume_dialog).repeatWhenAttached {
coroutineScopeTraced("[Volume]dialog") {
val component = componentFactory.create(this)
with(component.volumeDialogViewBinder()) { bind(this@VolumeDialog) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
index 20a74b027db5..afe3d7bf217a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
@@ -17,7 +17,9 @@
package com.android.systemui.volume.dialog.domain.interactor
import android.annotation.SuppressLint
+import android.provider.Settings
import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
@@ -28,8 +30,9 @@ import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityMod
import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible
import com.android.systemui.volume.dialog.utils.VolumeTracer
import javax.inject.Inject
-import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -43,8 +46,6 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
-private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds
-
/**
* Handles Volume Dialog visibility state. It might change from several sources:
* - [com.android.systemui.plugins.VolumeDialogController] requests visibility change;
@@ -60,8 +61,11 @@ constructor(
private val tracer: VolumeTracer,
private val repository: VolumeDialogVisibilityRepository,
private val controller: VolumeDialogController,
+ private val secureSettingsRepository: SecureSettingsRepository,
) {
+ private val defaultTimeout = 3.seconds
+
@SuppressLint("SharedFlowCreation")
private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val dialogVisibility: Flow<VolumeDialogVisibilityModel> =
@@ -73,7 +77,14 @@ constructor(
init {
merge(
mutableDismissDialogEvents.mapLatest {
- delay(MAX_DIALOG_SHOW_TIME)
+ delay(
+ secureSettingsRepository
+ .getInt(
+ Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT,
+ defaultTimeout.toInt(DurationUnit.MILLISECONDS),
+ )
+ .milliseconds
+ )
VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT)
},
callbacksInteractor.event,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index 3d0c7d64b2a4..92ec4f554548 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -246,16 +246,12 @@ constructor(
uiModel.drawerState.currentMode != uiModel.drawerState.previousMode
) {
val count = uiModel.availableButtons.size
- val selectedButton =
- getChildAt(count - uiModel.currentButtonIndex)
- .requireViewById<ImageButton>(R.id.volume_drawer_button)
+ val selectedButton = getChildAt(count - uiModel.currentButtonIndex) as ImageButton
val previousIndex =
uiModel.availableButtons.indexOfFirst {
it.ringerMode == uiModel.drawerState.previousMode
}
- val unselectedButton =
- getChildAt(count - previousIndex)
- .requireViewById<ImageButton>(R.id.volume_drawer_button)
+ val unselectedButton = getChildAt(count - previousIndex) as ImageButton
// We only need to execute on roundness animation end and volume dialog background
// progress update once because these changes should be applied once on volume dialog
// background and ringer drawer views.
@@ -306,7 +302,7 @@ constructor(
) {
val count = uiModel.availableButtons.size
uiModel.availableButtons.fastForEachIndexed { index, ringerButton ->
- val view = getChildAt(count - index)
+ val view = getChildAt(count - index) as ImageButton
val isOpen = uiModel.drawerState is RingerDrawerState.Open
if (index == uiModel.currentButtonIndex) {
view.bindDrawerButton(
@@ -323,37 +319,37 @@ constructor(
onAnimationEnd?.run()
}
- private fun View.bindDrawerButton(
+ private fun ImageButton.bindDrawerButton(
buttonViewModel: RingerButtonViewModel,
viewModel: VolumeDialogRingerDrawerViewModel,
isOpen: Boolean,
isSelected: Boolean = false,
isAnimated: Boolean = false,
) {
+ // id = buttonViewModel.viewId
+ setSelected(isSelected)
val ringerContentDesc = context.getString(buttonViewModel.contentDescriptionResId)
- with(requireViewById<ImageButton>(R.id.volume_drawer_button)) {
- setImageResource(buttonViewModel.imageResId)
- contentDescription =
- if (isSelected && !isOpen) {
- context.getString(
- R.string.volume_ringer_drawer_closed_content_description,
- ringerContentDesc,
- )
- } else {
- ringerContentDesc
- }
- if (isSelected && !isAnimated) {
- setBackgroundResource(R.drawable.volume_drawer_selection_bg)
- setColorFilter(context.getColor(internalR.color.materialColorOnPrimary))
- background = background.mutate()
- } else if (!isAnimated) {
- setBackgroundResource(R.drawable.volume_ringer_item_bg)
- setColorFilter(context.getColor(internalR.color.materialColorOnSurface))
- background = background.mutate()
- }
- setOnClickListener {
- viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
+ setImageResource(buttonViewModel.imageResId)
+ contentDescription =
+ if (isSelected && !isOpen) {
+ context.getString(
+ R.string.volume_ringer_drawer_closed_content_description,
+ ringerContentDesc,
+ )
+ } else {
+ ringerContentDesc
}
+ if (isSelected && !isAnimated) {
+ setBackgroundResource(R.drawable.volume_drawer_selection_bg)
+ setColorFilter(context.getColor(internalR.color.materialColorOnPrimary))
+ background = background.mutate()
+ } else if (!isAnimated) {
+ setBackgroundResource(R.drawable.volume_ringer_item_bg)
+ setColorFilter(context.getColor(internalR.color.materialColorOnSurface))
+ background = background.mutate()
+ }
+ setOnClickListener {
+ viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index f2d7d956291c..7cc4bcc4e11c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -74,7 +74,7 @@ constructor(
val insets: MutableStateFlow<WindowInsets> =
MutableStateFlow(WindowInsets.Builder().build())
// Root view of the Volume Dialog.
- val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root)
+ val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog)
animateVisibility(root, dialog, viewModel.dialogVisibilityModel)
diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
index 6b7de982e00a..22a74c86e0f1 100644
--- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
@@ -16,10 +16,8 @@
package com.android.systemui.window.data.repository
-import android.annotation.SuppressLint
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
/** Repository that maintains state for the window blur effect. */
@@ -28,6 +26,4 @@ class WindowRootViewBlurRepository @Inject constructor() {
val blurRadius = MutableStateFlow(0)
val isBlurOpaque = MutableStateFlow(false)
-
- @SuppressLint("SharedFlowCreation") val onBlurApplied = MutableSharedFlow<Int>()
}
diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
index e21e0a1cadc7..9e369347dea5 100644
--- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.window.domain.interactor
+import android.annotation.SuppressLint
import android.util.Log
import com.android.systemui.Flags
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -28,6 +29,7 @@ import com.android.systemui.window.data.repository.WindowRootViewBlurRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -52,6 +54,8 @@ constructor(
private val communalInteractor: CommunalInteractor,
private val repository: WindowRootViewBlurRepository,
) {
+ @SuppressLint("SharedFlowCreation") private val _onBlurAppliedEvent = MutableSharedFlow<Int>()
+
private var isBouncerTransitionInProgress: StateFlow<Boolean> =
if (Flags.bouncerUiRevamp()) {
keyguardTransitionInteractor
@@ -68,7 +72,7 @@ constructor(
* root view.
*/
suspend fun onBlurApplied(appliedBlurRadius: Int) {
- repository.onBlurApplied.emit(appliedBlurRadius)
+ _onBlurAppliedEvent.emit(appliedBlurRadius)
}
/** Radius of blur to be applied on the window root view. */
@@ -77,7 +81,7 @@ constructor(
/**
* Emits the applied blur radius whenever blur is successfully applied to the window root view.
*/
- val onBlurAppliedEvent: Flow<Int> = repository.onBlurApplied
+ val onBlurAppliedEvent: Flow<Int> = _onBlurAppliedEvent
/** Whether the blur applied is opaque or transparent. */
val isBlurOpaque: Flow<Boolean> =
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
new file mode 100644
index 000000000000..eb3ba82b043b
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ViewFlipper
+ android:id="@+id/flipper"
+ android:layout_width="match_parent"
+ android:layout_height="400dp"
+ android:flipInterval="1000"
+ />
+
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
new file mode 100644
index 000000000000..e2a00bd845cd
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/custom_view_flipper_image.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/imageview"
+ android:layout_width="match_parent"
+ android:layout_height="400dp" /> \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index e8054c07eac8..4ccfa29d4ba0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -206,7 +206,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
private @Mock ShadeInteractor mShadeInteractor;
private @Mock ShadeWindowLogger mShadeWindowLogger;
private @Mock SelectedUserInteractor mSelectedUserInteractor;
- private @Mock UserTracker.Callback mUserTrackerCallback;
private @Mock KeyguardInteractor mKeyguardInteractor;
private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor;
private @Captor ArgumentCaptor<KeyguardStateController.Callback>
@@ -281,7 +280,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
() -> mShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
- mock(UserTracker.class),
+ mUserTracker,
mKosmos.getNotificationShadeWindowModel(),
mSecureSettings,
mKosmos::getCommunalInteractor,
@@ -319,7 +318,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
} catch (Exception e) {
// Just so we don't have to add the exception signature to every test.
- fail(e.getMessage());
+ fail();
}
}
@@ -331,156 +330,18 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
/* First test the default behavior: handleUserSwitching() is not invoked */
when(mUserTracker.isUserSwitching()).thenReturn(false);
+ mViewMediator.mUpdateCallback = mock(KeyguardUpdateMonitorCallback.class);
mViewMediator.onSystemReady();
TestableLooper.get(this).processAllMessages();
- verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext),
- any(Runnable.class));
+ verify(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId);
/* Next test user switching is already in progress when started */
when(mUserTracker.isUserSwitching()).thenReturn(true);
mViewMediator.onSystemReady();
TestableLooper.get(this).processAllMessages();
- verify(mUserTrackerCallback).onUserChanging(eq(userId), eq(mContext),
- any(Runnable.class));
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testGoingAwayFollowedByBeforeUserSwitchDoesNotHideKeyguard() {
- setCurrentUser(/* userId= */1099, /* isSecure= */false);
-
- // Setup keyguard
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true, "");
-
- // Request keyguard going away
- when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
- mViewMediator.mKeyguardGoingAwayRunnable.run();
-
- // After the request, begin a switch to a new secure user
- int nextUserId = 500;
- setCurrentUser(nextUserId, /* isSecure= */true);
- Runnable result = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, result);
- processAllMessagesAndBgExecutorMessages();
- verify(result).run();
-
- // After that request has begun, have WM tell us to exit keyguard
- RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{
- mock(RemoteAnimationTarget.class)
- };
- RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{
- mock(RemoteAnimationTarget.class)
- };
- IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
- mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
- null, callback);
- processAllMessagesAndBgExecutorMessages();
-
- // The call to exit should be rejected, and keyguard should still be visible
- verify(mKeyguardUnlockAnimationController, never()).notifyStartSurfaceBehindRemoteAnimation(
- any(), any(), any(), anyLong(), anyBoolean());
- try {
- assertATMSLockScreenShowing(true);
- } catch (Exception e) {
- fail(e.getMessage());
- }
- assertTrue(mViewMediator.isShowingAndNotOccluded());
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testUserSwitchToSecureUserShowsBouncer() {
- setCurrentUser(/* userId= */1099, /* isSecure= */true);
-
- // Setup keyguard
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true, "");
-
- // After the request, begin a switch to a new secure user
- int nextUserId = 500;
- setCurrentUser(nextUserId, /* isSecure= */true);
-
- Runnable beforeResult = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
- processAllMessagesAndBgExecutorMessages();
- verify(beforeResult).run();
-
- // Dismiss should not be called while user switch is in progress
- Runnable onSwitchResult = mock(Runnable.class);
- mViewMediator.handleUserSwitching(nextUserId, onSwitchResult);
- processAllMessagesAndBgExecutorMessages();
- verify(onSwitchResult).run();
- verify(mStatusBarKeyguardViewManager, never()).dismissAndCollapse();
-
- // The attempt to dismiss only comes on user switch complete, which will trigger a call to
- // show the bouncer in StatusBarKeyguardViewManager
- mViewMediator.handleUserSwitchComplete(nextUserId);
- TestableLooper.get(this).moveTimeForward(600);
- processAllMessagesAndBgExecutorMessages();
-
- verify(mStatusBarKeyguardViewManager).dismissAndCollapse();
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testUserSwitchToInsecureUserDismissesKeyguard() {
- int userId = 1099;
- when(mUserTracker.getUserId()).thenReturn(userId);
-
- // Setup keyguard
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true, "");
-
- // After the request, begin a switch to an insecure user
- int nextUserId = 500;
- when(mLockPatternUtils.isSecure(nextUserId)).thenReturn(false);
-
- Runnable beforeResult = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
- processAllMessagesAndBgExecutorMessages();
- verify(beforeResult).run();
-
- // The call to dismiss comes during the user switch
- Runnable onSwitchResult = mock(Runnable.class);
- mViewMediator.handleUserSwitching(nextUserId, onSwitchResult);
- processAllMessagesAndBgExecutorMessages();
- verify(onSwitchResult).run();
-
- verify(mStatusBarKeyguardViewManager).dismissAndCollapse();
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testUserSwitchToSecureUserWhileKeyguardNotVisibleShowsKeyguard() {
- setCurrentUser(/* userId= */1099, /* isSecure= */true);
-
- // Setup keyguard as not visible
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(false, "");
- processAllMessagesAndBgExecutorMessages();
-
- // Begin a switch to a new secure user
- int nextUserId = 500;
- setCurrentUser(nextUserId, /* isSecure= */true);
-
- Runnable beforeResult = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
- processAllMessagesAndBgExecutorMessages();
- verify(beforeResult).run();
-
- try {
- assertATMSLockScreenShowing(true);
- } catch (Exception e) {
- fail();
- }
- assertTrue(mViewMediator.isShowingAndNotOccluded());
+ verify(mViewMediator.mUpdateCallback).onUserSwitching(userId);
}
@Test
@@ -1244,7 +1105,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
processAllMessagesAndBgExecutorMessages();
verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean());
-
+ assertATMSAndKeyguardViewMediatorStatesMatch();
}
@Test
@@ -1288,7 +1149,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
- mViewMediator.mKeyguardGoingAwayRunnable.run();
mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
null, callback);
processAllMessagesAndBgExecutorMessages();
@@ -1343,6 +1203,13 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
// The captor will have the most recent setLockScreenShown call's value.
assertEquals(showing, showingCaptor.getValue());
+
+ // We're now just after the last setLockScreenShown call. If we expect the lockscreen to be
+ // showing, ensure that we didn't subsequently ask for it to go away.
+ if (showing) {
+ orderedSetLockScreenShownCalls.verify(mActivityTaskManagerService, never())
+ .keyguardGoingAway(anyInt());
+ }
}
/**
@@ -1504,7 +1371,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mKeyguardTransitionBootInteractor,
mKosmos::getCommunalSceneInteractor,
mock(WindowManagerOcclusionManager.class));
- mViewMediator.mUserChangedCallback = mUserTrackerCallback;
mViewMediator.start();
mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null);
@@ -1518,10 +1384,4 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
private void captureKeyguardUpdateMonitorCallback() {
verify(mUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallbackCaptor.capture());
}
-
- private void setCurrentUser(int userId, boolean isSecure) {
- when(mUserTracker.getUserId()).thenReturn(userId);
- when(mSelectedUserInteractor.getSelectedUserId()).thenReturn(userId);
- when(mLockPatternUtils.isSecure(userId)).thenReturn(isSecure);
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 86094d1a0fef..88c2697fe2f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -1519,7 +1519,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void selectedDevicesAddedInSameOrder() {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
@@ -1537,7 +1537,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice2);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void selectedDevicesAddedInReverseOrder() {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
@@ -1555,7 +1555,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void firstSelectedDeviceIsFirstDeviceInGroupIsTrue() {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
new file mode 100644
index 000000000000..09fa3871f6e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierFlagDisabledTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.DisableFlags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationCustomContentMemoryVerifierFlagDisabledTest extends SysuiTestCase {
+
+ @Rule
+ public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+ @Test
+ @DisableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS
+ })
+ public void requiresImageViewMemorySizeCheck_flagDisabled_returnsFalse() {
+ NotificationEntry entry = buildAcceptableNotificationEntry(mContext);
+ assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+ .isFalse();
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
new file mode 100644
index 000000000000..1cadb3c0a909
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentMemoryVerifierTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildAcceptableNotificationEntry;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildOversizedNotification;
+import static com.android.systemui.statusbar.notification.row.NotificationCustomContentNotificationBuilder.buildWarningSizedNotification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.platform.test.annotations.EnableFlags;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.RemoteViews;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.notification.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@EnableFlags(Flags.FLAG_NOTIFICATION_CUSTOM_VIEW_URI_RESTRICTION)
+public class NotificationCustomContentMemoryVerifierTest extends SysuiTestCase {
+
+ private static final String AUTHORITY = "notification.memory.test.authority";
+ private static final Uri TEST_URI = new Uri.Builder()
+ .scheme("content")
+ .authority(AUTHORITY)
+ .path("path")
+ .build();
+
+ @Rule
+ public PlatformCompatChangeRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+ @Before
+ public void setUp() {
+ TestImageContentProvider provider = new TestImageContentProvider(mContext);
+ mContext.getContentResolver().addProvider(AUTHORITY, provider);
+ provider.onCreate();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void requiresImageViewMemorySizeCheck_customViewNotification_returnsTrue() {
+ NotificationEntry entry =
+ buildAcceptableNotificationEntry(
+ mContext);
+ assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void requiresImageViewMemorySizeCheck_plainNotification_returnsFalse() {
+ Notification notification =
+ new Notification.Builder(mContext, "ChannelId")
+ .setContentTitle("Just a notification")
+ .setContentText("Yep")
+ .build();
+ NotificationEntry entry = new NotificationEntryBuilder().setNotification(
+ notification).build();
+ assertThat(NotificationCustomContentMemoryVerifier.requiresImageViewMemorySizeCheck(entry))
+ .isFalse();
+ }
+
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_smallNotification_returnsTrue() {
+ Notification.Builder notification =
+ buildAcceptableNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ )
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_oversizedNotification_returnsFalse() {
+ Notification.Builder notification =
+ buildOversizedNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ ).isFalse();
+ }
+
+ @Test
+ @DisableCompatChanges(
+ {NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS}
+ )
+ public void satisfiesMemoryLimits_oversizedNotification_compatDisabled_returnsTrue() {
+ Notification.Builder notification =
+ buildOversizedNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ ).isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_warningSizedNotification_returnsTrue() {
+ Notification.Builder notification =
+ buildWarningSizedNotification(mContext,
+ TEST_URI);
+ NotificationEntry entry = toEntry(notification);
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(inflatedView, entry)
+ )
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void satisfiesMemoryLimits_viewWithoutCustomNotificationRoot_returnsTrue() {
+ NotificationEntry entry = new NotificationEntryBuilder().build();
+ View view = new FrameLayout(mContext);
+ assertThat(NotificationCustomContentMemoryVerifier.satisfiesMemoryLimits(view, entry))
+ .isTrue();
+ }
+
+ @Test
+ @EnableCompatChanges({
+ NotificationCustomContentCompat.CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS})
+ public void computeViewHierarchyImageViewSize_smallNotification_returnsSensibleValue() {
+ Notification.Builder notification =
+ buildAcceptableNotification(mContext,
+ TEST_URI);
+ // This should have a size of a single image
+ View inflatedView = inflateNotification(notification);
+ assertThat(
+ NotificationCustomContentMemoryVerifier.computeViewHierarchyImageViewSize(
+ inflatedView))
+ .isGreaterThan(170000);
+ }
+
+ private View inflateNotification(Notification.Builder builder) {
+ RemoteViews remoteViews = builder.createBigContentView();
+ return remoteViews.apply(mContext, new FrameLayout(mContext));
+ }
+
+ private NotificationEntry toEntry(Notification.Builder builder) {
+ return new NotificationEntryBuilder().setNotification(builder.build())
+ .setUid(Process.myUid()).build();
+ }
+
+
+ /** This provider serves the images for inflation. */
+ class TestImageContentProvider extends ContentProvider {
+
+ TestImageContentProvider(Context context) {
+ ProviderInfo info = new ProviderInfo();
+ info.authority = AUTHORITY;
+ info.exported = true;
+ attachInfoForTesting(context, info);
+ setAuthorities(AUTHORITY);
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) {
+ return getContext().getResources().openRawResourceFd(
+ NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE())
+ .getParcelFileDescriptor();
+ }
+
+ @Override
+ public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) {
+ return getContext().getResources().openRawResourceFd(
+ NotificationCustomContentNotificationBuilder.getDRAWABLE_IMAGE_RESOURCE());
+ }
+
+ @Override
+ public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
+ CancellationSignal signal) throws FileNotFoundException {
+ return openTypedAssetFile(uri, mimeTypeFilter, opts);
+ }
+
+ @Override
+ public int delete(Uri uri, Bundle extras) {
+ return 0;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return "image/png";
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values, Bundle extras) {
+ return super.insert(uri, values, extras);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, Bundle queryArgs,
+ CancellationSignal cancellationSignal) {
+ return super.query(uri, projection, queryArgs, cancellationSignal);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+ }
+
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
new file mode 100644
index 000000000000..ca4f24da3c08
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationCustomContentNotificationBuilder.kt
@@ -0,0 +1,94 @@
+/*
+ * 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
+ */
+
+@file:JvmName("NotificationCustomContentNotificationBuilder")
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Notification.DecoratedCustomViewStyle
+import android.content.Context
+import android.graphics.drawable.BitmapDrawable
+import android.net.Uri
+import android.os.Process
+import android.widget.RemoteViews
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.tests.R
+import org.hamcrest.Matchers.lessThan
+import org.junit.Assume.assumeThat
+
+public val DRAWABLE_IMAGE_RESOURCE = R.drawable.romainguy_rockaway
+
+fun buildAcceptableNotificationEntry(context: Context): NotificationEntry {
+ return NotificationEntryBuilder()
+ .setNotification(buildAcceptableNotification(context, null).build())
+ .setUid(Process.myUid())
+ .build()
+}
+
+fun buildAcceptableNotification(context: Context, uri: Uri?): Notification.Builder =
+ buildNotification(context, uri, 1)
+
+fun buildOversizedNotification(context: Context, uri: Uri): Notification.Builder {
+ val numImagesForOversize =
+ (NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context) /
+ drawableSizeOnDevice(context)) + 2
+ return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildWarningSizedNotification(context: Context, uri: Uri): Notification.Builder {
+ val numImagesForOversize =
+ (NotificationCustomContentMemoryVerifier.getWarnViewSizeLimit(context) /
+ drawableSizeOnDevice(context)) + 1
+ // The size needs to be smaller than outright stripping size.
+ assumeThat(
+ numImagesForOversize * drawableSizeOnDevice(context),
+ lessThan(NotificationCustomContentMemoryVerifier.getStripViewSizeLimit(context)),
+ )
+ return buildNotification(context, uri, numImagesForOversize)
+}
+
+fun buildNotification(context: Context, uri: Uri?, numImages: Int): Notification.Builder {
+ val remoteViews = RemoteViews(context.packageName, R.layout.custom_view_flipper)
+ repeat(numImages) { i ->
+ val remoteViewFlipperImageView =
+ RemoteViews(context.packageName, R.layout.custom_view_flipper_image)
+
+ if (uri == null) {
+ remoteViewFlipperImageView.setImageViewResource(
+ R.id.imageview,
+ R.drawable.romainguy_rockaway,
+ )
+ } else {
+ val imageUri = uri.buildUpon().appendPath(i.toString()).build()
+ remoteViewFlipperImageView.setImageViewUri(R.id.imageview, imageUri)
+ }
+ remoteViews.addView(R.id.flipper, remoteViewFlipperImageView)
+ }
+
+ return Notification.Builder(context, "ChannelId")
+ .setSmallIcon(android.R.drawable.ic_info)
+ .setStyle(DecoratedCustomViewStyle())
+ .setCustomContentView(remoteViews)
+ .setCustomBigContentView(remoteViews)
+ .setContentTitle("This is a remote view!")
+}
+
+fun drawableSizeOnDevice(context: Context): Int {
+ val drawable = context.resources.getDrawable(DRAWABLE_IMAGE_RESOURCE)
+ return (drawable as BitmapDrawable).bitmap.allocationByteCount
+}
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 2f30b745a4a3..3190d3ae8f16 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
@@ -91,11 +91,11 @@ import com.android.systemui.statusbar.notification.collection.provider.LaunchFul
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.data.repository.NotificationLaunchAnimationRepository;
import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -122,7 +122,6 @@ import java.util.Optional;
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
- private static final int DISPLAY_ID = 0;
private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
@@ -233,7 +232,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
mNotificationActivityStarter =
new StatusBarNotificationActivityStarter(
getContext(),
- DISPLAY_ID,
mHandler,
mUiBgExecutor,
mVisibilityProvider,
diff --git a/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt
index c936b914f44e..26618484669b 100644
--- a/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt
@@ -17,6 +17,15 @@
package android.os
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
-var Kosmos.userManager by Kosmos.Fixture { mock<UserManager>() }
+var Kosmos.userManager by
+ Kosmos.Fixture {
+ mock<UserManager> {
+ whenever(it.mainUser).thenReturn(UserHandle(MAIN_USER_ID))
+ whenever(it.getUserSerialNumber(eq(MAIN_USER_ID))).thenReturn(0)
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index b0a6de1f931a..0f21a16147f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -42,7 +42,9 @@ import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.settings.userTracker
import com.android.systemui.statusbar.phone.fakeManagedProfileController
+import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.mockito.mock
val Kosmos.communalInteractor by Fixture {
@@ -70,6 +72,7 @@ val Kosmos.communalInteractor by Fixture {
batteryInteractor = batteryInteractor,
dockManager = dockManager,
posturingInteractor = posturingInteractor,
+ userLockedInteractor = userLockedInteractor,
)
}
@@ -98,10 +101,8 @@ suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean) {
suspend fun Kosmos.setCommunalAvailable(available: Boolean) {
setCommunalEnabled(available)
- with(fakeKeyguardRepository) {
- setIsEncryptedOrLockdown(!available)
- setKeyguardShowing(available)
- }
+ fakeKeyguardRepository.setKeyguardShowing(available)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available)
}
suspend fun Kosmos.setCommunalV2Available(available: Boolean) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
index 2e59788663f7..4480539b9a15 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
@@ -16,18 +16,20 @@
package com.android.systemui.dreams.ui.viewmodel
-import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor
-val Kosmos.dreamUserActionsViewModel by
- Kosmos.Fixture {
- DreamUserActionsViewModel(
- communalInteractor = communalInteractor,
- deviceUnlockedInteractor = deviceUnlockedInteractor,
- shadeInteractor = shadeInteractor,
- shadeModeInteractor = shadeModeInteractor,
- )
+val Kosmos.dreamUserActionsViewModelFactory by Fixture {
+ object : DreamUserActionsViewModel.Factory {
+ override fun create(): DreamUserActionsViewModel {
+ return DreamUserActionsViewModel(
+ deviceUnlockedInteractor = deviceUnlockedInteractor,
+ shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
+ )
+ }
}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index fcdda9f13099..9da8e80283b6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor
import android.service.dream.dreamManager
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
@@ -44,5 +45,6 @@ var Kosmos.fromDozingTransitionInteractor by
deviceEntryInteractor = deviceEntryInteractor,
wakeToGoneInteractor = keyguardWakeDirectlyToGoneInteractor,
dreamManager = dreamManager,
+ communalSettingsInteractor = communalSettingsInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
index 5fc31f8b9e10..f2871149de11 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
@@ -19,6 +19,7 @@ package com.android.systemui.qs.pipeline.data.repository
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.pipeline.shared.TilesUpgradePath
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -79,9 +80,9 @@ class FakeTileSpecRepository(
with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles }
}
- override val tilesReadFromSetting: Channel<Pair<Set<TileSpec>, Int>> = Channel(capacity = 10)
+ override val tilesUpgradePath: Channel<Pair<TilesUpgradePath, Int>> = Channel(capacity = 10)
- suspend fun sendTilesReadFromSetting(tiles: Set<TileSpec>, userId: Int) {
- tilesReadFromSetting.send(tiles to userId)
+ suspend fun sendTilesFromUpgradePath(upgradePath: TilesUpgradePath, userId: Int) {
+ tilesUpgradePath.send(upgradePath to userId)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
index 5ff44e5d33c5..c5de02a7281b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
@@ -26,7 +26,7 @@ val Kosmos.minimumTilesRepository: MinimumTilesRepository by
Kosmos.Fixture { fakeMinimumTilesRepository }
var Kosmos.fakeDefaultTilesRepository by Kosmos.Fixture { FakeDefaultTilesRepository() }
-val Kosmos.defaultTilesRepository: DefaultTilesRepository by
+var Kosmos.defaultTilesRepository: DefaultTilesRepository by
Kosmos.Fixture { fakeDefaultTilesRepository }
val Kosmos.fakeTileSpecRepository by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index ce298bb90ba2..f0350acd83ca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -5,6 +5,8 @@ import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.lightRevealScrimViewModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -18,7 +20,6 @@ import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.composable.ConstantSceneContainerTransitionsBuilder
import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
@@ -97,7 +98,6 @@ val Kosmos.sceneContainerViewModelFactory by Fixture {
powerInteractor = powerInteractor,
shadeModeInteractor = shadeModeInteractor,
remoteInputInteractor = remoteInputInteractor,
- splitEdgeDetector = splitEdgeDetector,
logger = sceneLogger,
hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
view = view,
@@ -105,6 +105,8 @@ val Kosmos.sceneContainerViewModelFactory by Fixture {
lightRevealScrim = lightRevealScrimViewModel,
wallpaperViewModel = wallpaperViewModel,
keyguardInteractor = keyguardInteractor,
+ burnIn = aodBurnInViewModel,
+ clock = keyguardClockViewModel,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
index b40e1e7ab33b..6b641934bc44 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.data.repository
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
/**
* Make the repository hold [count] active notifications for testing. The keys of the notifications
@@ -37,3 +38,56 @@ fun ActiveNotificationListRepository.setActiveNotifs(count: Int) {
}
.build()
}
+
+/**
+ * Adds the given notification to the repository while *maintaining any notifications already
+ * present*. [notif] will be ranked highest.
+ */
+fun ActiveNotificationListRepository.addNotif(notif: ActiveNotificationModel) {
+ val currentNotifications = this.activeNotifications.value.individuals
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ addIndividualNotif(notif)
+ currentNotifications.forEach {
+ if (it.key != notif.key) {
+ addIndividualNotif(it.value)
+ }
+ }
+ }
+ .build()
+}
+
+/**
+ * Adds the given notification to the repository while *maintaining any notifications already
+ * present*. [notifs] will be ranked higher than existing notifs.
+ */
+fun ActiveNotificationListRepository.addNotifs(notifs: List<ActiveNotificationModel>) {
+ val currentNotifications = this.activeNotifications.value.individuals
+ val newKeys = notifs.map { it.key }
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ notifs.forEach { addIndividualNotif(it) }
+ currentNotifications.forEach {
+ if (!newKeys.contains(it.key)) {
+ addIndividualNotif(it.value)
+ }
+ }
+ }
+ .build()
+}
+
+fun ActiveNotificationListRepository.removeNotif(keyToRemove: String) {
+ val currentNotifications = this.activeNotifications.value.individuals
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ currentNotifications.forEach {
+ if (it.key != keyToRemove) {
+ addIndividualNotif(it.value)
+ }
+ }
+ }
+ .build()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
index 2057b849c069..c7380c91f703 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/domain/interactor/NotificationShelfInteractorKosmos.kt
@@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.statusbar.lockscreenShadeTransitionController
val Kosmos.notificationShelfInteractor by Fixture {
@@ -28,6 +29,7 @@ val Kosmos.notificationShelfInteractor by Fixture {
keyguardRepository = keyguardRepository,
deviceEntryFaceAuthRepository = deviceEntryFaceAuthRepository,
powerInteractor = powerInteractor,
+ shadeModeInteractor = shadeModeInteractor,
keyguardTransitionController = lockscreenShadeTransitionController,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
index 0d6ac4481742..d787e2c190c8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
@@ -52,7 +52,6 @@ val Kosmos.statusBarNotificationActivityStarter by
Kosmos.Fixture {
StatusBarNotificationActivityStarter(
applicationContext,
- applicationContext.displayId,
fakeExecutorHandler,
fakeExecutor,
notificationVisibilityProvider,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
deleted file mode 100644
index 923b36d4f2cf..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
+++ /dev/null
@@ -1,39 +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.statusbar.phone.ongoingcall.shared.model
-
-import android.app.PendingIntent
-import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-
-/** Helper for building [OngoingCallModel.InCall] instances in tests. */
-fun inCallModel(
- startTimeMs: Long,
- notificationIcon: StatusBarIconView? = null,
- intent: PendingIntent? = null,
- notificationKey: String = "test",
- appName: String = "",
- promotedContent: PromotedNotificationContentModel? = null,
-) =
- OngoingCallModel.InCall(
- startTimeMs,
- notificationIcon,
- intent,
- notificationKey,
- appName,
- promotedContent,
- )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
new file mode 100644
index 000000000000..d09d010cba2e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.ongoingcall.shared.model
+
+import android.app.PendingIntent
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.addNotif
+import com.android.systemui.statusbar.notification.data.repository.removeNotif
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
+import org.mockito.kotlin.mock
+
+/** Helper for building [OngoingCallModel.InCall] instances in tests. */
+fun inCallModel(
+ startTimeMs: Long,
+ notificationIcon: StatusBarIconView? = null,
+ intent: PendingIntent? = null,
+ notificationKey: String = "test",
+ appName: String = "",
+ promotedContent: PromotedNotificationContentModel? = null,
+) =
+ OngoingCallModel.InCall(
+ startTimeMs,
+ notificationIcon,
+ intent,
+ notificationKey,
+ appName,
+ promotedContent,
+ )
+
+object OngoingCallTestHelper {
+ /**
+ * Removes any ongoing call state and removes any call notification associated with [key]. Does
+ * it correctly based on whether [StatusBarChipsModernization] is enabled or not.
+ *
+ * @param key the notification key associated with the call notification.
+ */
+ fun Kosmos.removeOngoingCallState(key: String) {
+ if (StatusBarChipsModernization.isEnabled) {
+ activeNotificationListRepository.removeNotif(key)
+ } else {
+ ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
+ }
+ }
+
+ /**
+ * Sets SysUI to have an ongoing call state. Does it correctly based on whether
+ * [StatusBarChipsModernization] is enabled or not.
+ *
+ * @param key the notification key to be associated with the call notification
+ */
+ fun Kosmos.addOngoingCallState(
+ key: String = "notif",
+ startTimeMs: Long = 1000L,
+ statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
+ promotedContent: PromotedNotificationContentModel? = null,
+ contentIntent: PendingIntent? = null,
+ uid: Int = DEFAULT_UID,
+ ) {
+ if (StatusBarChipsModernization.isEnabled) {
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = key,
+ whenTime = startTimeMs,
+ callType = CallType.Ongoing,
+ statusBarChipIcon = statusBarChipIconView,
+ contentIntent = contentIntent,
+ promotedContent = promotedContent,
+ uid = uid,
+ )
+ )
+ } else {
+ ongoingCallRepository.setOngoingCallState(
+ inCallModel(
+ startTimeMs = startTimeMs,
+ notificationIcon = statusBarChipIconView,
+ intent = contentIntent,
+ notificationKey = key,
+ promotedContent = promotedContent,
+ )
+ )
+ }
+ }
+
+ private fun createStatusBarIconViewOrNull(): StatusBarIconView? =
+ if (StatusBarConnectedDisplays.isEnabled) {
+ null
+ } else {
+ mock<StatusBarIconView>()
+ }
+
+ private const val DEFAULT_UID = 886
+}
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 85d582a27faf..145bb93d9cb0 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
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.yield
@SysUISingleton
@@ -76,6 +77,11 @@ class FakeUserRepository @Inject constructor() : UserRepository {
override val isLogoutToSystemUserEnabled: StateFlow<Boolean> =
_isLogoutToSystemUserEnabled.asStateFlow()
+ private val _userUnlockedState = MutableStateFlow(emptyMap<UserHandle, Boolean>())
+
+ override fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
+ _userUnlockedState.map { it[userHandle] ?: false }
+
override var mainUserId: Int = MAIN_USER_ID
override var lastSelectedNonGuestUserId: Int = mainUserId
@@ -176,6 +182,14 @@ class FakeUserRepository @Inject constructor() : UserRepository {
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
+
+ fun setUserUnlocked(userId: Int, unlocked: Boolean) {
+ setUserUnlocked(UserHandle(userId), unlocked)
+ }
+
+ fun setUserUnlocked(userHandle: UserHandle, unlocked: Boolean) {
+ _userUnlockedState.update { it + (userHandle to unlocked) }
+ }
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
index e0b529261c4d..fd955089cdc7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.scene.ui.viewmodel
+package com.android.systemui.user.domain.interactor
-import androidx.compose.ui.unit.dp
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.user.data.repository.userRepository
-var Kosmos.splitEdgeDetector: SplitEdgeDetector by
- Kosmos.Fixture {
- SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- edgeSize = 40.dp,
- )
- }
+val Kosmos.userLockedInteractor by
+ Kosmos.Fixture { UserLockedInteractor(userRepository = userRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
index 0d2aa4c79753..888b7e625524 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -19,6 +19,7 @@ package com.android.systemui.volume.dialog.domain.interactor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.volumeDialogController
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
import com.android.systemui.volume.dialog.utils.volumeTracer
@@ -30,5 +31,6 @@ val Kosmos.volumeDialogVisibilityInteractor by
volumeTracer,
volumeDialogVisibilityRepository,
volumeDialogController,
+ secureSettingsRepository,
)
}
diff --git a/services/accessibility/OWNERS b/services/accessibility/OWNERS
index 4e1175034b5b..ab1e9ffe3bfe 100644
--- a/services/accessibility/OWNERS
+++ b/services/accessibility/OWNERS
@@ -1,4 +1,7 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
# Android Accessibility Framework owners
danielnorman@google.com
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index e8dddcb537cd..529a564ea607 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -100,6 +100,13 @@ flag {
}
flag {
+ name: "enable_low_vision_generic_feedback"
+ namespace: "accessibility"
+ description: "Use generic feedback for low vision."
+ bug: "393981463"
+}
+
+flag {
name: "enable_low_vision_hats"
namespace: "accessibility"
description: "Use HaTS for low vision feedback."
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 8e448676c214..db8441d2424b 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -510,6 +510,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
return mMetaState;
}
+ @VisibleForTesting
+ boolean getIsActiveForTesting() {
+ return mActive;
+ }
+
/**
* Updates delay that should be used when scheduling clicks. The delay will be used only for
* clicks scheduled after this point (pending click tasks are not affected).
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
index 0354d2be60c9..2ef11f4b78e1 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
@@ -20,11 +20,14 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M
import android.content.Context;
import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.widget.ImageButton;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -51,6 +54,8 @@ public class AutoclickTypePanel {
private final LinearLayout mDragButton;
private final LinearLayout mScrollButton;
+ private LinearLayout mSelectedButton;
+
public AutoclickTypePanel(Context context, WindowManager windowManager) {
mContext = context;
mWindowManager = windowManager;
@@ -80,6 +85,40 @@ public class AutoclickTypePanel {
// Initializes panel as collapsed state and only displays the left click button.
hideAllClickTypeButtons();
mLeftClickButton.setVisibility(View.VISIBLE);
+ setSelectedButton(/* selectedButton= */ mLeftClickButton);
+ }
+
+ /** Sets the selected button and updates the newly and previously selected button styling. */
+ private void setSelectedButton(@NonNull LinearLayout selectedButton) {
+ // Updates the previously selected button styling.
+ if (mSelectedButton != null) {
+ toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false);
+ }
+
+ mSelectedButton = selectedButton;
+
+ // Updates the newly selected button styling.
+ toggleSelectedButtonStyle(selectedButton, /* isSelected= */ true);
+ }
+
+ private void toggleSelectedButtonStyle(@NonNull LinearLayout button, boolean isSelected) {
+ // Sets icon background color.
+ GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
+ gradientDrawable.setColor(
+ mContext.getColor(
+ isSelected
+ ? R.color.materialColorPrimary
+ : R.color.materialColorSurfaceContainer));
+
+ // Sets icon color.
+ ImageButton imageButton = (ImageButton) button.getChildAt(/* index= */ 0);
+ Drawable drawable = imageButton.getDrawable();
+ drawable.mutate()
+ .setTint(
+ mContext.getColor(
+ isSelected
+ ? R.color.materialColorSurfaceContainer
+ : R.color.materialColorPrimary));
}
public void show() {
@@ -97,6 +136,9 @@ public class AutoclickTypePanel {
// buttons except the one user selected.
hideAllClickTypeButtons();
button.setVisibility(View.VISIBLE);
+
+ // Sets the newly selected button.
+ setSelectedButton(/* selectedButton= */ button);
} else {
// If the panel is already collapsed, we just need to expand it.
showAllClickTypeButtons();
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS
new file mode 100644
index 000000000000..ff812ad7e7e6
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/OWNERS
@@ -0,0 +1,8 @@
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 770744.
+
+juchengchou@google.com
+chenjean@google.com
+chihtinglo@google.com
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 43764442e2cf..d0ee7af1bbfb 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -27,6 +27,7 @@ import android.annotation.WorkerThread;
import android.app.appfunctions.AppFunctionException;
import android.app.appfunctions.AppFunctionManager;
import android.app.appfunctions.AppFunctionManagerHelper;
+import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
import android.app.appfunctions.AppFunctionRuntimeMetadata;
import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
@@ -513,7 +514,9 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
e = e.getCause();
}
int resultCode = AppFunctionException.ERROR_SYSTEM_ERROR;
- if (e instanceof AppSearchException appSearchException) {
+ if (e instanceof AppFunctionNotFoundException) {
+ resultCode = AppFunctionException.ERROR_FUNCTION_NOT_FOUND;
+ } else if (e instanceof AppSearchException appSearchException) {
resultCode =
mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(
appSearchException.getResultCode());
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index c385fbad02a5..f03e8c713228 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -30,7 +30,6 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
-import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -55,9 +54,9 @@ import android.companion.virtual.VirtualDeviceParams;
import android.companion.virtual.audio.IAudioConfigChangedCallback;
import android.companion.virtual.audio.IAudioRoutingCallback;
import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.companion.virtualdevice.flags.Flags;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.AttributionSource;
@@ -111,6 +110,7 @@ import android.util.SparseIntArray;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
+import android.window.DisplayWindowPolicyController;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -265,7 +265,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
UserHandle.SYSTEM);
}
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
try {
mActivityListener.onActivityLaunchBlocked(
displayId,
@@ -280,7 +280,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@Override
public void onSecureWindowShown(int displayId, @NonNull ActivityInfo activityInfo) {
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
try {
mActivityListener.onSecureWindowShown(
displayId,
@@ -318,7 +318,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@Override
public void onSecureWindowHidden(int displayId) {
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
try {
mActivityListener.onSecureWindowHidden(displayId);
} catch (RemoteException e) {
@@ -682,7 +682,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
checkCallerIsDeviceOwner();
final int displayId = exemption.getDisplayId();
if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return;
}
}
@@ -719,7 +719,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
checkCallerIsDeviceOwner();
final int displayId = exemption.getDisplayId();
if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return;
}
}
@@ -921,7 +921,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
break;
case POLICY_TYPE_BLOCKED_ACTIVITY:
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
synchronized (mVirtualDeviceLock) {
mDevicePolicies.put(policyType, devicePolicy);
}
@@ -938,7 +938,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
checkCallerIsDeviceOwner();
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return;
}
synchronized (mVirtualDeviceLock) {
@@ -1412,8 +1412,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return mirroredDisplayId == Display.INVALID_DISPLAY ? displayId : mirroredDisplayId;
}
- @GuardedBy("mVirtualDeviceLock")
- private GenericWindowPolicyController createWindowPolicyControllerLocked(
+ private GenericWindowPolicyController createWindowPolicyController(
@NonNull Set<String> displayCategories) {
final boolean activityLaunchAllowedByDefault =
getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT;
@@ -1422,28 +1421,28 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
final boolean showTasksInHostDeviceRecents =
getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
- if (mActivityListenerAdapter == null) {
- mActivityListenerAdapter = new GwpcActivityListener();
- }
-
- final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
- mAttributionSource,
- getAllowedUserHandles(),
- activityLaunchAllowedByDefault,
- mActivityPolicyExemptions,
- mActivityPolicyPackageExemptions,
- crossTaskNavigationAllowedByDefault,
- /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
- ? mParams.getBlockedCrossTaskNavigations()
- : mParams.getAllowedCrossTaskNavigations(),
- mActivityListenerAdapter,
- displayCategories,
- showTasksInHostDeviceRecents,
- mParams.getHomeComponent());
- gwpc.registerRunningAppsChangedListener(/* listener= */ this);
- return gwpc;
+ synchronized (mVirtualDeviceLock) {
+ if (mActivityListenerAdapter == null) {
+ mActivityListenerAdapter = new GwpcActivityListener();
+ }
+
+ return new GenericWindowPolicyController(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
+ mAttributionSource,
+ getAllowedUserHandles(),
+ activityLaunchAllowedByDefault,
+ mActivityPolicyExemptions,
+ mActivityPolicyPackageExemptions,
+ crossTaskNavigationAllowedByDefault,
+ /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
+ ? mParams.getBlockedCrossTaskNavigations()
+ : mParams.getAllowedCrossTaskNavigations(),
+ mActivityListenerAdapter,
+ displayCategories,
+ showTasksInHostDeviceRecents,
+ mParams.getHomeComponent());
+ }
}
@Override // Binder call
@@ -1451,55 +1450,54 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@NonNull IVirtualDisplayCallback callback) {
checkCallerIsDeviceOwner();
- int displayId;
- boolean showPointer;
- boolean isTrustedDisplay;
- GenericWindowPolicyController gwpc;
- synchronized (mVirtualDeviceLock) {
- gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
- displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig,
+ final boolean isTrustedDisplay =
+ (virtualDisplayConfig.getFlags() & DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED)
+ == DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+ if (!isTrustedDisplay && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
+ throw new SecurityException(
+ "All displays must be trusted for devices with custom clipboard policy.");
+ }
+
+ GenericWindowPolicyController gwpc =
+ createWindowPolicyController(virtualDisplayConfig.getDisplayCategories());
+
+ // Create the display outside of the lock to avoid deadlock. DisplayManagerService will
+ // acquire the global WM lock while creating the display. At the same time, WM may query
+ // VDM and this virtual device to get policies, display ownership, etc.
+ int displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig,
callback, this, gwpc, mOwnerPackageName);
- boolean isMirrorDisplay =
- mDisplayManagerInternal.getDisplayIdToMirror(displayId)
- != Display.INVALID_DISPLAY;
- gwpc.setDisplayId(displayId, isMirrorDisplay);
- isTrustedDisplay =
- (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
- == Display.FLAG_TRUSTED;
- if (!isTrustedDisplay
- && getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
- throw new SecurityException("All displays must be trusted for devices with "
- + "custom clipboard policy.");
- }
+ if (displayId == Display.INVALID_DISPLAY) {
+ return displayId;
+ }
- if (mVirtualDisplays.contains(displayId)) {
- gwpc.unregisterRunningAppsChangedListener(this);
- throw new IllegalStateException(
- "Virtual device already has a virtual display with ID " + displayId);
+ // DisplayManagerService will call onVirtualDisplayCreated() after the display is created,
+ // while holding its own lock to ensure that this device knows about the display before any
+ // other display listeners are notified about the display creation.
+ VirtualDisplayWrapper displayWrapper;
+ boolean showPointer;
+ synchronized (mVirtualDeviceLock) {
+ if (!mVirtualDisplays.contains(displayId)) {
+ throw new IllegalStateException("Virtual device was not notified about the "
+ + "creation of display with ID " + displayId);
}
-
- PowerManager.WakeLock wakeLock =
- isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null;
- mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
- isTrustedDisplay, isMirrorDisplay));
+ displayWrapper = mVirtualDisplays.get(displayId);
showPointer = mDefaultShowPointerIcon;
}
+ displayWrapper.acquireWakeLock();
+ gwpc.registerRunningAppsChangedListener(/* listener= */ this);
- final long token = Binder.clearCallingIdentity();
- try {
+ Binder.withCleanCallingIdentity(() -> {
mInputController.setMouseScalingEnabled(false, displayId);
mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
displayId);
- if (isTrustedDisplay) {
+ if (displayWrapper.isTrusted()) {
mInputController.setShowPointerIcon(showPointer, displayId);
mInputController.setDisplayImePolicy(displayId,
WindowManager.DISPLAY_IME_POLICY_LOCAL);
} else {
gwpc.setShowInHostDeviceRecents(true);
}
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ });
Counter.logIncrementWithUid(
"virtual_devices.value_virtual_display_created_count",
@@ -1507,8 +1505,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return displayId;
}
- private PowerManager.WakeLock createAndAcquireWakeLockForDisplay(int displayId) {
- if (android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower()) {
+ private PowerManager.WakeLock createWakeLockForDisplay(int displayId) {
+ if (Flags.deviceAwareDisplayPower()) {
return null;
}
final long token = Binder.clearCallingIdentity();
@@ -1517,7 +1515,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
TAG + ":" + displayId, displayId);
- wakeLock.acquire();
return wakeLock;
} finally {
Binder.restoreCallingIdentity(token);
@@ -1531,7 +1528,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
// infinite blocking loop.
return false;
}
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return true;
}
// Do not show the dialog if disabled by policy.
@@ -1562,17 +1559,47 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return result;
}
+ /**
+ * DisplayManagerService is notifying this virtual device about the display creation. This
+ * should happen before the DisplayManagerInternal#createVirtualDisplay() call above
+ * returns.
+ * This is called while holding the DisplayManagerService lock, so no heavy-weight work must
+ * be done here and especially *** no calls to WindowManager! ***
+ */
+ public void onVirtualDisplayCreated(int displayId, IVirtualDisplayCallback callback,
+ DisplayWindowPolicyController dwpc) {
+ final boolean isMirrorDisplay =
+ mDisplayManagerInternal.getDisplayIdToMirror(displayId) != Display.INVALID_DISPLAY;
+ final boolean isTrustedDisplay =
+ (mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
+ == Display.FLAG_TRUSTED;
+
+ GenericWindowPolicyController gwpc = (GenericWindowPolicyController) dwpc;
+ gwpc.setDisplayId(displayId, isMirrorDisplay);
+ PowerManager.WakeLock wakeLock =
+ isTrustedDisplay ? createWakeLockForDisplay(displayId) : null;
+ synchronized (mVirtualDeviceLock) {
+ if (mVirtualDisplays.contains(displayId)) {
+ Slog.wtf(TAG, "Virtual device already has a virtual display with ID " + displayId);
+ return;
+ }
+ mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
+ isTrustedDisplay, isMirrorDisplay));
+ }
+ }
+
+ /**
+ * This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released
+ * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()).
+ * At this point, the display is already released, but we still need to release the
+ * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding
+ * WindowPolicyController.
+ *
+ * Note that when the display is destroyed during VirtualDeviceImpl.close() call,
+ * this callback won't be invoked because the display is removed from
+ * VirtualDeviceManagerService before any resources are released.
+ */
void onVirtualDisplayRemoved(int displayId) {
- /* This is callback invoked by VirtualDeviceManagerService when VirtualDisplay was released
- * by DisplayManager (most probably caused by someone calling VirtualDisplay.close()).
- * At this point, the display is already released, but we still need to release the
- * corresponding wakeLock and unregister the RunningAppsChangedListener from corresponding
- * WindowPolicyController.
- *
- * Note that when the display is destroyed during VirtualDeviceImpl.close() call,
- * this callback won't be invoked because the display is removed from
- * VirtualDeviceManagerService before any resources are released.
- */
VirtualDisplayWrapper virtualDisplayWrapper;
synchronized (mVirtualDeviceLock) {
virtualDisplayWrapper = mVirtualDisplays.removeReturnOld(displayId);
@@ -1848,6 +1875,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return mWindowPolicyController;
}
+ void acquireWakeLock() {
+ if (mWakeLock != null && !mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ }
+ }
+
void releaseWakeLock() {
if (mWakeLock != null && mWakeLock.isHeld()) {
mWakeLock.release();
@@ -1868,8 +1901,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
private static boolean isVirtualCameraEnabled() {
- return Flags.virtualCamera() && virtualCameraServiceDiscovery()
- && nativeVirtualCameraServiceBuildFlagEnabled();
+ return nativeVirtualCameraServiceBuildFlagEnabled();
}
// Returns true if virtual_camera service is enabled in this build.
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 8a0b85859b66..ff82ca00b840 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -40,7 +40,6 @@ import android.companion.virtual.IVirtualDeviceSoundEffectListener;
import android.companion.virtual.VirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtualnative.IVirtualDeviceManagerNative;
import android.compat.annotation.ChangeId;
@@ -49,6 +48,7 @@ import android.content.AttributionSource;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManagerInternal;
+import android.hardware.display.IVirtualDisplayCallback;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
@@ -67,6 +67,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.widget.Toast;
+import android.window.DisplayWindowPolicyController;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -751,6 +752,16 @@ public class VirtualDeviceManagerService extends SystemService {
}
@Override
+ public void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId,
+ IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc) {
+ VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId(
+ ((VirtualDeviceImpl) virtualDevice).getDeviceId());
+ if (virtualDeviceImpl != null) {
+ virtualDeviceImpl.onVirtualDisplayCreated(displayId, callback, dwpc);
+ }
+ }
+
+ @Override
public void onVirtualDisplayRemoved(IVirtualDevice virtualDevice, int displayId) {
VirtualDeviceImpl virtualDeviceImpl = getVirtualDeviceForId(
((VirtualDeviceImpl) virtualDevice).getDeviceId());
diff --git a/services/core/Android.bp b/services/core/Android.bp
index f98076ab41e4..00db11e72dd9 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -292,9 +292,18 @@ java_genrule {
out: ["services.core.priorityboosted.jar"],
}
+java_genrule_combiner {
+ name: "services.core.combined",
+ static_libs: ["services.core.priorityboosted"],
+ headers: ["services.core.unboosted"],
+}
+
java_library {
name: "services.core",
- static_libs: ["services.core.priorityboosted"],
+ static_libs: select(release_flag("RELEASE_SERVICES_JAVA_GENRULE_COMBINER"), {
+ true: ["services.core.combined"],
+ default: ["services.core.priorityboosted"],
+ }),
}
java_library_host {
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 350ecab1dd5f..d2a5734f323f 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -163,6 +163,10 @@ import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
import com.android.server.storage.StorageSessionController;
import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
+import com.android.server.storage.WatchedVolumeInfo;
+import com.android.server.utils.Watchable;
+import com.android.server.utils.WatchedArrayMap;
+import com.android.server.utils.Watcher;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver;
@@ -452,7 +456,7 @@ class StorageManagerService extends IStorageManager.Stub
private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>();
/** Map from volume ID to disk */
@GuardedBy("mLock")
- private final ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>();
+ private final WatchedArrayMap<String, WatchedVolumeInfo> mVolumes = new WatchedArrayMap<>();
/** Map from UUID to record */
@GuardedBy("mLock")
@@ -503,9 +507,9 @@ class StorageManagerService extends IStorageManager.Stub
"(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?");
- private VolumeInfo findVolumeByIdOrThrow(String id) {
+ private WatchedVolumeInfo findVolumeByIdOrThrow(String id) {
synchronized (mLock) {
- final VolumeInfo vol = mVolumes.get(id);
+ final WatchedVolumeInfo vol = mVolumes.get(id);
if (vol != null) {
return vol;
}
@@ -516,9 +520,9 @@ class StorageManagerService extends IStorageManager.Stub
private VolumeRecord findRecordForPath(String path) {
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
- if (vol.path != null && path.startsWith(vol.path)) {
- return mRecords.get(vol.fsUuid);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
+ if (vol.getFsPath() != null && path.startsWith(vol.getFsPath())) {
+ return mRecords.get(vol.getFsUuid());
}
}
}
@@ -764,7 +768,7 @@ class StorageManagerService extends IStorageManager.Stub
break;
}
case H_VOLUME_MOUNT: {
- final VolumeInfo vol = (VolumeInfo) msg.obj;
+ final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
if (isMountDisallowed(vol)) {
Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy");
break;
@@ -774,7 +778,7 @@ class StorageManagerService extends IStorageManager.Stub
break;
}
case H_VOLUME_UNMOUNT: {
- final VolumeInfo vol = (VolumeInfo) msg.obj;
+ final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
unmount(vol);
break;
}
@@ -828,7 +832,8 @@ class StorageManagerService extends IStorageManager.Stub
}
case H_VOLUME_STATE_CHANGED: {
final SomeArgs args = (SomeArgs) msg.obj;
- onVolumeStateChangedAsync((VolumeInfo) args.arg1, args.argi1, args.argi2);
+ onVolumeStateChangedAsync((WatchedVolumeInfo) args.arg1, args.argi1,
+ args.argi2);
args.recycle();
break;
}
@@ -892,9 +897,9 @@ class StorageManagerService extends IStorageManager.Stub
synchronized (mLock) {
final int size = mVolumes.size();
for (int i = 0; i < size; i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
- if (vol.mountUserId == userId) {
- vol.mountUserId = UserHandle.USER_NULL;
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
+ if (vol.getMountUserId() == userId) {
+ vol.setMountUserId(UserHandle.USER_NULL);
mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
}
}
@@ -1084,7 +1089,7 @@ class StorageManagerService extends IStorageManager.Stub
VolumeInfo.TYPE_PRIVATE, null, null);
internal.state = VolumeInfo.STATE_MOUNTED;
internal.path = Environment.getDataDirectory().getAbsolutePath();
- mVolumes.put(internal.id, internal);
+ mVolumes.put(internal.id, WatchedVolumeInfo.fromVolumeInfo(internal));
}
private void resetIfBootedAndConnected() {
@@ -1242,7 +1247,7 @@ class StorageManagerService extends IStorageManager.Stub
}
}
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) {
final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false);
mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget();
@@ -1291,21 +1296,21 @@ class StorageManagerService extends IStorageManager.Stub
}
private void maybeRemountVolumes(int userId) {
- List<VolumeInfo> volumesToRemount = new ArrayList<>();
+ List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (!vol.isPrimary() && vol.isMountedWritable() && vol.isVisible()
&& vol.getMountUserId() != mCurrentUserId) {
// If there's a visible secondary volume mounted,
// we need to update the currentUserId and remount
- vol.mountUserId = mCurrentUserId;
+ vol.setMountUserId(mCurrentUserId);
volumesToRemount.add(vol);
}
}
}
- for (VolumeInfo vol : volumesToRemount) {
+ for (WatchedVolumeInfo vol : volumesToRemount) {
Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol);
mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
@@ -1317,12 +1322,12 @@ class StorageManagerService extends IStorageManager.Stub
* trying to mount doesn't have the same mount user id as the current user being maintained by
* StorageManagerService and change the mount Id. The checks are same as
* {@link StorageManagerService#maybeRemountVolumes(int)}
- * @param VolumeInfo object to consider for changing the mountId
+ * @param vol {@link WatchedVolumeInfo} object to consider for changing the mountId
*/
- private void updateVolumeMountIdIfRequired(VolumeInfo vol) {
+ private void updateVolumeMountIdIfRequired(WatchedVolumeInfo vol) {
synchronized (mLock) {
if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) {
- vol.mountUserId = mCurrentUserId;
+ vol.setMountUserId(mCurrentUserId);
}
}
}
@@ -1485,20 +1490,21 @@ class StorageManagerService extends IStorageManager.Stub
final DiskInfo disk = mDisks.get(diskId);
final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid);
vol.mountUserId = userId;
- mVolumes.put(volId, vol);
- onVolumeCreatedLocked(vol);
+ WatchedVolumeInfo watchedVol = WatchedVolumeInfo.fromVolumeInfo(vol);
+ mVolumes.put(volId, watchedVol);
+ onVolumeCreatedLocked(watchedVol);
}
}
@Override
public void onVolumeStateChanged(String volId, final int newState, final int userId) {
synchronized (mLock) {
- final VolumeInfo vol = mVolumes.get(volId);
+ final WatchedVolumeInfo vol = mVolumes.get(volId);
if (vol != null) {
- final int oldState = vol.state;
- vol.state = newState;
- final VolumeInfo vInfo = new VolumeInfo(vol);
- vInfo.mountUserId = userId;
+ final int oldState = vol.getState();
+ vol.setState(newState);
+ final WatchedVolumeInfo vInfo = new WatchedVolumeInfo(vol);
+ vInfo.setMountUserId(userId);
final SomeArgs args = SomeArgs.obtain();
args.arg1 = vInfo;
args.argi1 = oldState;
@@ -1513,11 +1519,11 @@ class StorageManagerService extends IStorageManager.Stub
public void onVolumeMetadataChanged(String volId, String fsType, String fsUuid,
String fsLabel) {
synchronized (mLock) {
- final VolumeInfo vol = mVolumes.get(volId);
+ final WatchedVolumeInfo vol = mVolumes.get(volId);
if (vol != null) {
- vol.fsType = fsType;
- vol.fsUuid = fsUuid;
- vol.fsLabel = fsLabel;
+ vol.setFsType(fsType);
+ vol.setFsUuid(fsUuid);
+ vol.setFsLabel(fsLabel);
}
}
}
@@ -1525,9 +1531,9 @@ class StorageManagerService extends IStorageManager.Stub
@Override
public void onVolumePathChanged(String volId, String path) {
synchronized (mLock) {
- final VolumeInfo vol = mVolumes.get(volId);
+ final WatchedVolumeInfo vol = mVolumes.get(volId);
if (vol != null) {
- vol.path = path;
+ vol.setFsPath(path);
}
}
}
@@ -1535,24 +1541,24 @@ class StorageManagerService extends IStorageManager.Stub
@Override
public void onVolumeInternalPathChanged(String volId, String internalPath) {
synchronized (mLock) {
- final VolumeInfo vol = mVolumes.get(volId);
+ final WatchedVolumeInfo vol = mVolumes.get(volId);
if (vol != null) {
- vol.internalPath = internalPath;
+ vol.setInternalPath(internalPath);
}
}
}
@Override
public void onVolumeDestroyed(String volId) {
- VolumeInfo vol = null;
+ WatchedVolumeInfo vol = null;
synchronized (mLock) {
vol = mVolumes.remove(volId);
}
if (vol != null) {
- mStorageSessionController.onVolumeRemove(vol);
+ mStorageSessionController.onVolumeRemove(vol.getImmutableVolumeInfo());
try {
- if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+ if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
mInstaller.onPrivateVolumeRemoved(vol.getFsUuid());
}
} catch (Installer.InstallerException e) {
@@ -1566,7 +1572,7 @@ class StorageManagerService extends IStorageManager.Stub
private void onDiskScannedLocked(DiskInfo disk) {
int volumeCount = 0;
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (Objects.equals(disk.id, vol.getDiskId())) {
volumeCount++;
}
@@ -1589,19 +1595,19 @@ class StorageManagerService extends IStorageManager.Stub
}
@GuardedBy("mLock")
- private void onVolumeCreatedLocked(VolumeInfo vol) {
+ private void onVolumeCreatedLocked(WatchedVolumeInfo vol) {
final ActivityManagerInternal amInternal =
LocalServices.getService(ActivityManagerInternal.class);
- if (vol.mountUserId >= 0 && !amInternal.isUserRunning(vol.mountUserId, 0)) {
+ if (vol.getMountUserId() >= 0 && !amInternal.isUserRunning(vol.getMountUserId(), 0)) {
Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user "
- + Integer.toString(vol.mountUserId) + " is no longer running.");
+ + Integer.toString(vol.getMountUserId()) + " is no longer running.");
return;
}
- if (vol.type == VolumeInfo.TYPE_EMULATED) {
+ if (vol.getType() == VolumeInfo.TYPE_EMULATED) {
final Context volumeUserContext = mContext.createContextAsUser(
- UserHandle.of(vol.mountUserId), 0);
+ UserHandle.of(vol.getMountUserId()), 0);
boolean isMediaSharedWithParent =
(volumeUserContext != null) ? volumeUserContext.getSystemService(
@@ -1611,60 +1617,60 @@ class StorageManagerService extends IStorageManager.Stub
// should not be skipped even if media provider instance is not running in that user
// space
if (!isMediaSharedWithParent
- && !mStorageSessionController.supportsExternalStorage(vol.mountUserId)) {
+ && !mStorageSessionController.supportsExternalStorage(vol.getMountUserId())) {
Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user "
- + Integer.toString(vol.mountUserId)
+ + Integer.toString(vol.getMountUserId())
+ " does not support external storage.");
return;
}
final StorageManager storage = mContext.getSystemService(StorageManager.class);
- final VolumeInfo privateVol = storage.findPrivateForEmulated(vol);
+ final VolumeInfo privateVol = storage.findPrivateForEmulated(vol.getVolumeInfo());
if ((Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)
&& VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id))
- || Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) {
+ || Objects.equals(privateVol.getFsUuid(), mPrimaryStorageUuid)) {
Slog.v(TAG, "Found primary storage at " + vol);
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY);
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
}
- } else if (vol.type == VolumeInfo.TYPE_PUBLIC) {
+ } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
// TODO: only look at first public partition
if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
- && vol.disk.isDefaultPrimary()) {
+ && vol.getDisk().isDefaultPrimary()) {
Slog.v(TAG, "Found primary storage at " + vol);
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY);
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
}
// Adoptable public disks are visible to apps, since they meet
// public API requirement of being in a stable location.
- if (vol.disk.isAdoptable()) {
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+ if (vol.getDisk().isAdoptable()) {
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
}
- vol.mountUserId = mCurrentUserId;
+ vol.setMountUserId(mCurrentUserId);
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
- } else if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+ } else if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
- } else if (vol.type == VolumeInfo.TYPE_STUB) {
- if (vol.disk.isStubVisible()) {
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE;
+ } else if (vol.getType() == VolumeInfo.TYPE_STUB) {
+ if (vol.getDisk().isStubVisible()) {
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
} else {
- vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ;
+ vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ);
}
- vol.mountUserId = mCurrentUserId;
+ vol.setMountUserId(mCurrentUserId);
mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
} else {
Slog.d(TAG, "Skipping automatic mounting of " + vol);
}
}
- private boolean isBroadcastWorthy(VolumeInfo vol) {
+ private boolean isBroadcastWorthy(WatchedVolumeInfo vol) {
switch (vol.getType()) {
case VolumeInfo.TYPE_PRIVATE:
case VolumeInfo.TYPE_PUBLIC:
@@ -1691,8 +1697,8 @@ class StorageManagerService extends IStorageManager.Stub
}
@GuardedBy("mLock")
- private void onVolumeStateChangedLocked(VolumeInfo vol, int newState) {
- if (vol.type == VolumeInfo.TYPE_EMULATED) {
+ private void onVolumeStateChangedLocked(WatchedVolumeInfo vol, int newState) {
+ if (vol.getType() == VolumeInfo.TYPE_EMULATED) {
if (newState != VolumeInfo.STATE_MOUNTED) {
mFuseMountedUser.remove(vol.getMountUserId());
} else if (mVoldAppDataIsolationEnabled){
@@ -1741,7 +1747,7 @@ class StorageManagerService extends IStorageManager.Stub
}
}
- private void onVolumeStateChangedAsync(VolumeInfo vol, int oldState, int newState) {
+ private void onVolumeStateChangedAsync(WatchedVolumeInfo vol, int oldState, int newState) {
if (newState == VolumeInfo.STATE_MOUNTED) {
// Private volumes can be unmounted and re-mounted even after a user has
// been unlocked; on devices that support encryption keys tied to the filesystem,
@@ -1751,7 +1757,7 @@ class StorageManagerService extends IStorageManager.Stub
} catch (Exception e) {
// Unusable partition, unmount.
try {
- mVold.unmount(vol.id);
+ mVold.unmount(vol.getId());
} catch (Exception ee) {
Slog.wtf(TAG, ee);
}
@@ -1762,20 +1768,20 @@ class StorageManagerService extends IStorageManager.Stub
synchronized (mLock) {
// Remember that we saw this volume so we're ready to accept user
// metadata, or so we can annoy them when a private volume is ejected
- if (!TextUtils.isEmpty(vol.fsUuid)) {
- VolumeRecord rec = mRecords.get(vol.fsUuid);
+ if (!TextUtils.isEmpty(vol.getFsUuid())) {
+ VolumeRecord rec = mRecords.get(vol.getFsUuid());
if (rec == null) {
- rec = new VolumeRecord(vol.type, vol.fsUuid);
- rec.partGuid = vol.partGuid;
+ rec = new VolumeRecord(vol.getType(), vol.getFsUuid());
+ rec.partGuid = vol.getPartGuid();
rec.createdMillis = System.currentTimeMillis();
- if (vol.type == VolumeInfo.TYPE_PRIVATE) {
- rec.nickname = vol.disk.getDescription();
+ if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
+ rec.nickname = vol.getDisk().getDescription();
}
mRecords.put(rec.fsUuid, rec);
} else {
// Handle upgrade case where we didn't store partition GUID
if (TextUtils.isEmpty(rec.partGuid)) {
- rec.partGuid = vol.partGuid;
+ rec.partGuid = vol.getPartGuid();
}
}
@@ -1788,7 +1794,7 @@ class StorageManagerService extends IStorageManager.Stub
// before notifying other listeners.
// Intentionally called without the mLock to avoid deadlocking from the Storage Service.
try {
- mStorageSessionController.notifyVolumeStateChanged(vol);
+ mStorageSessionController.notifyVolumeStateChanged(vol.getImmutableVolumeInfo());
} catch (ExternalStorageServiceException e) {
Log.e(TAG, "Failed to notify volume state changed to the Storage Service", e);
}
@@ -1799,9 +1805,9 @@ class StorageManagerService extends IStorageManager.Stub
// processes that receive the intent unnecessarily.
if (mBootCompleted && isBroadcastWorthy(vol)) {
final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED);
- intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
+ intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState);
- intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.fsUuid);
+ intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget();
@@ -1826,8 +1832,8 @@ class StorageManagerService extends IStorageManager.Stub
}
}
- if ((vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_STUB)
- && vol.state == VolumeInfo.STATE_EJECTING) {
+ if ((vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB)
+ && vol.getState() == VolumeInfo.STATE_EJECTING) {
// TODO: this should eventually be handled by new ObbVolume state changes
/*
* Some OBBs might have been unmounted when this volume was
@@ -1835,7 +1841,7 @@ class StorageManagerService extends IStorageManager.Stub
* remove those from the list of mounted OBBS.
*/
mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(
- OBB_FLUSH_MOUNT_STATE, vol.path));
+ OBB_FLUSH_MOUNT_STATE, vol.getFsPath()));
}
maybeLogMediaMount(vol, newState);
}
@@ -1860,7 +1866,7 @@ class StorageManagerService extends IStorageManager.Stub
}
}
- private void maybeLogMediaMount(VolumeInfo vol, int newState) {
+ private void maybeLogMediaMount(WatchedVolumeInfo vol, int newState) {
if (!SecurityLog.isLoggingEnabled()) {
return;
}
@@ -1875,10 +1881,10 @@ class StorageManagerService extends IStorageManager.Stub
if (newState == VolumeInfo.STATE_MOUNTED
|| newState == VolumeInfo.STATE_MOUNTED_READ_ONLY) {
- SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.path, label);
+ SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.getFsPath(), label);
} else if (newState == VolumeInfo.STATE_UNMOUNTED
|| newState == VolumeInfo.STATE_BAD_REMOVAL) {
- SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.path, label);
+ SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.getFsPath(), label);
}
}
@@ -1920,18 +1926,18 @@ class StorageManagerService extends IStorageManager.Stub
/**
* Decide if volume is mountable per device policies.
*/
- private boolean isMountDisallowed(VolumeInfo vol) {
+ private boolean isMountDisallowed(WatchedVolumeInfo vol) {
UserManager userManager = mContext.getSystemService(UserManager.class);
boolean isUsbRestricted = false;
- if (vol.disk != null && vol.disk.isUsb()) {
+ if (vol.getDisk() != null && vol.getDisk().isUsb()) {
isUsbRestricted = userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER,
Binder.getCallingUserHandle());
}
boolean isTypeRestricted = false;
- if (vol.type == VolumeInfo.TYPE_PUBLIC || vol.type == VolumeInfo.TYPE_PRIVATE
- || vol.type == VolumeInfo.TYPE_STUB) {
+ if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_PRIVATE
+ || vol.getType() == VolumeInfo.TYPE_STUB) {
isTypeRestricted = userManager
.hasUserRestriction(UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
Binder.getCallingUserHandle());
@@ -1967,6 +1973,13 @@ class StorageManagerService extends IStorageManager.Stub
mContext = context;
mCallbacks = new Callbacks(FgThread.get().getLooper());
+ mVolumes.registerObserver(new Watcher() {
+ @Override
+ public void onChange(Watchable what) {
+ // When we change the list or any volume contained in it, invalidate the cache
+ StorageManager.invalidateVolumeListCache();
+ }
+ });
HandlerThread hthread = new HandlerThread(TAG);
hthread.start();
mHandler = new StorageManagerServiceHandler(hthread.getLooper());
@@ -2339,7 +2352,7 @@ class StorageManagerService extends IStorageManager.Stub
super.mount_enforcePermission();
- final VolumeInfo vol = findVolumeByIdOrThrow(volId);
+ final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
if (isMountDisallowed(vol)) {
throw new SecurityException("Mounting " + volId + " restricted by policy");
}
@@ -2365,23 +2378,24 @@ class StorageManagerService extends IStorageManager.Stub
}
}
- private void mount(VolumeInfo vol) {
+ private void mount(WatchedVolumeInfo vol) {
try {
- Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id);
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.getId());
// TODO(b/135341433): Remove cautious logging when FUSE is stable
Slog.i(TAG, "Mounting volume " + vol);
extendWatchdogTimeout("#mount might be slow");
- mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() {
+ mVold.mount(vol.getId(), vol.getMountFlags(), vol.getMountUserId(),
+ new IVoldMountCallback.Stub() {
@Override
public boolean onVolumeChecking(FileDescriptor fd, String path,
String internalPath) {
- vol.path = path;
- vol.internalPath = internalPath;
+ vol.setFsPath(path);
+ vol.setInternalPath(internalPath);
ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd);
try {
Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
- "SMS.startFuseFileSystem: " + vol.id);
- mStorageSessionController.onVolumeMount(pfd, vol);
+ "SMS.startFuseFileSystem: " + vol.getId());
+ mStorageSessionController.onVolumeMount(pfd, vol.getImmutableVolumeInfo());
return true;
} catch (ExternalStorageServiceException e) {
Slog.e(TAG, "Failed to mount volume " + vol, e);
@@ -2416,21 +2430,21 @@ class StorageManagerService extends IStorageManager.Stub
super.unmount_enforcePermission();
- final VolumeInfo vol = findVolumeByIdOrThrow(volId);
+ final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
unmount(vol);
}
- private void unmount(VolumeInfo vol) {
+ private void unmount(WatchedVolumeInfo vol) {
try {
try {
- if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+ if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
mInstaller.onPrivateVolumeRemoved(vol.getFsUuid());
}
} catch (Installer.InstallerException e) {
Slog.e(TAG, "Failed unmount mirror data", e);
}
- mVold.unmount(vol.id);
- mStorageSessionController.onVolumeUnmount(vol);
+ mVold.unmount(vol.getId());
+ mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo());
} catch (Exception e) {
Slog.wtf(TAG, e);
}
@@ -2442,10 +2456,10 @@ class StorageManagerService extends IStorageManager.Stub
super.format_enforcePermission();
- final VolumeInfo vol = findVolumeByIdOrThrow(volId);
- final String fsUuid = vol.fsUuid;
+ final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
+ final String fsUuid = vol.getFsUuid();
try {
- mVold.format(vol.id, "auto");
+ mVold.format(vol.getId(), "auto");
// After a successful format above, we should forget about any
// records for the old partition, since it'll never appear again
@@ -3105,7 +3119,7 @@ class StorageManagerService extends IStorageManager.Stub
private void warnOnNotMounted() {
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (vol.isPrimary() && vol.isMountedWritable()) {
// Cool beans, we have a mounted primary volume
return;
@@ -3392,8 +3406,8 @@ class StorageManagerService extends IStorageManager.Stub
}
}
- private void prepareUserStorageIfNeeded(VolumeInfo vol) throws Exception {
- if (vol.type != VolumeInfo.TYPE_PRIVATE) {
+ private void prepareUserStorageIfNeeded(WatchedVolumeInfo vol) throws Exception {
+ if (vol.getType() != VolumeInfo.TYPE_PRIVATE) {
return;
}
@@ -3411,7 +3425,7 @@ class StorageManagerService extends IStorageManager.Stub
continue;
}
- prepareUserStorageInternal(vol.fsUuid, user.id, flags);
+ prepareUserStorageInternal(vol.getFsUuid(), user.id, flags);
}
}
@@ -3960,7 +3974,7 @@ class StorageManagerService extends IStorageManager.Stub
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
final String volId = mVolumes.keyAt(i);
- final VolumeInfo vol = mVolumes.valueAt(i);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
switch (vol.getType()) {
case VolumeInfo.TYPE_PUBLIC:
case VolumeInfo.TYPE_STUB:
@@ -4112,7 +4126,7 @@ class StorageManagerService extends IStorageManager.Stub
synchronized (mLock) {
final VolumeInfo[] res = new VolumeInfo[mVolumes.size()];
for (int i = 0; i < mVolumes.size(); i++) {
- res[i] = mVolumes.valueAt(i);
+ res[i] = mVolumes.valueAt(i).getVolumeInfo();
}
return res;
}
@@ -4708,7 +4722,8 @@ class StorageManagerService extends IStorageManager.Stub
break;
}
case MSG_VOLUME_STATE_CHANGED: {
- callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3);
+ VolumeInfo volInfo = ((WatchedVolumeInfo) args.arg1).getVolumeInfo();
+ callback.onVolumeStateChanged(volInfo, args.argi2, args.argi3);
break;
}
case MSG_VOLUME_RECORD_CHANGED: {
@@ -4738,7 +4753,7 @@ class StorageManagerService extends IStorageManager.Stub
obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget();
}
- private void notifyVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
+ private void notifyVolumeStateChanged(WatchedVolumeInfo vol, int oldState, int newState) {
final SomeArgs args = SomeArgs.obtain();
args.arg1 = vol.clone();
args.argi2 = oldState;
@@ -4790,8 +4805,8 @@ class StorageManagerService extends IStorageManager.Stub
pw.println("Volumes:");
pw.increaseIndent();
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
- if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) continue;
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
+ if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) continue;
vol.dump(pw);
}
pw.decreaseIndent();
@@ -5088,7 +5103,7 @@ class StorageManagerService extends IStorageManager.Stub
final List<String> primaryVolumeIds = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
- final VolumeInfo vol = mVolumes.valueAt(i);
+ final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (vol.isPrimary()) {
primaryVolumeIds.add(vol.getId());
}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 6cca7d16842a..cce29592d912 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8302,8 +8302,6 @@ public final class ActiveServices {
if ((allowWiu == REASON_DENIED) || (allowStart == REASON_DENIED)) {
@ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges);
- // We store them to compare the old and new while-in-use logics to each other.
- // (They're not used for any other purposes.)
if (allowWiu == REASON_DENIED) {
allowWiu = allowWhileInUse;
}
@@ -8706,6 +8704,7 @@ public final class ActiveServices {
+ ",duration:" + tempAllowListReason.mDuration
+ ",callingUid:" + tempAllowListReason.mCallingUid))
+ ">"
+ + "; allowWiu:" + allowWhileInUse
+ "; targetSdkVersion:" + r.appInfo.targetSdkVersion
+ "; callerTargetSdkVersion:" + callerTargetSdkVersion
+ "; startForegroundCount:" + r.mStartForegroundCount
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index c8b0a57fe9f0..5ff6999e40b3 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -3705,8 +3705,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub
@Override
public void takeUidSnapshotsAsync(int[] requestUids, ResultReceiver resultReceiver) {
if (!onlyCaller(requestUids)) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.BATTERY_STATS, null);
+ try {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BATTERY_STATS, null);
+ } catch (SecurityException ex) {
+ resultReceiver.send(IBatteryStats.RESULT_SECURITY_EXCEPTION,
+ Bundle.forPair(IBatteryStats.KEY_EXCEPTION_MESSAGE, ex.getMessage()));
+ return;
+ }
}
if (shouldCollectExternalStats()) {
@@ -3727,13 +3733,14 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
Bundle resultData = new Bundle(1);
resultData.putParcelableArray(IBatteryStats.KEY_UID_SNAPSHOTS, results);
- resultReceiver.send(0, resultData);
+ resultReceiver.send(IBatteryStats.RESULT_OK, resultData);
} catch (Exception ex) {
if (DBG) {
Slog.d(TAG, "Crashed while returning results for takeUidSnapshots("
+ Arrays.toString(requestUids) + ") i=" + i, ex);
}
- throw ex;
+ resultReceiver.send(IBatteryStats.RESULT_RUNTIME_EXCEPTION,
+ Bundle.forPair(IBatteryStats.KEY_EXCEPTION_MESSAGE, ex.getMessage()));
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index e0fbaf43ea43..27e9e44f1090 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -31,7 +31,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
import static android.os.PowerWhitelistManager.REASON_BOOT_COMPLETED;
import static android.os.PowerWhitelistManager.REASON_LOCKED_BOOT_COMPLETED;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
@@ -3905,6 +3904,10 @@ class UserController implements Handler.Callback {
return mService.mWindowManager;
}
+ ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+ return mService.mAtmInternal;
+ }
+
void activityManagerOnUserStopped(@UserIdInt int userId) {
LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId);
}
@@ -4119,25 +4122,40 @@ class UserController implements Handler.Callback {
}
void lockDeviceNowAndWaitForKeyguardShown() {
+ if (getWindowManager().isKeyguardLocked()) {
+ Slogf.w(TAG, "Not locking the device since the keyguard is already locked");
+ return;
+ }
+
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("lockDeviceNowAndWaitForKeyguardShown");
final CountDownLatch latch = new CountDownLatch(1);
- Bundle bundle = new Bundle();
- bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() {
- public void sendResult(Bundle data) {
- latch.countDown();
- }
- });
- getWindowManager().lockNow(bundle);
+ ActivityTaskManagerInternal.ScreenObserver screenObserver =
+ new ActivityTaskManagerInternal.ScreenObserver() {
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) {
+
+ }
+
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
+ if (isShowing) {
+ latch.countDown();
+ }
+ }
+ };
+
+ getActivityTaskManagerInternal().registerScreenObserver(screenObserver);
+ getWindowManager().lockDeviceNow();
try {
if (!latch.await(20, TimeUnit.SECONDS)) {
- throw new RuntimeException("User controller expected a callback while waiting "
- + "to show the keyguard. Timed out after 20 seconds.");
+ throw new RuntimeException("Keyguard is not shown in 20 seconds");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
+ getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver);
t.traceEnd();
}
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 2219ecc77167..b48d0a6ed547 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -66,7 +66,6 @@ import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
import static com.android.media.audio.Flags.replaceStreamBtSco;
import static com.android.media.audio.Flags.ringMyCar;
import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
-import static com.android.media.audio.Flags.vgsVssSyncMuteOrder;
import static com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl;
import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
import static com.android.server.utils.EventLogger.Event.ALOGE;
@@ -4977,9 +4976,8 @@ public class AudioService extends IAudioService.Stub
+ roForegroundAudioControl());
pw.println("\tandroid.media.audio.scoManagedByAudio:"
+ scoManagedByAudio());
- pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
- + vgsVssSyncMuteOrder());
pw.println("\tcom.android.media.audio.absVolumeIndexFix - EOL");
+ pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder - EOL");
pw.println("\tcom.android.media.audio.replaceStreamBtSco:"
+ replaceStreamBtSco());
pw.println("\tcom.android.media.audio.equalScoLeaVcIndexRange:"
@@ -9010,22 +9008,13 @@ public class AudioService extends IAudioService.Stub
synced = true;
continue;
}
- if (vgsVssSyncMuteOrder()) {
- if ((isMuted() != streamMuted) && isVssMuteBijective(
- stream)) {
- vss.mute(isMuted(), "VGS.applyAllVolumes#1");
- }
+ if ((isMuted() != streamMuted) && isVssMuteBijective(stream)) {
+ vss.mute(isMuted(), "VGS.applyAllVolumes#1");
}
if (indexForStream != index) {
vss.setIndex(index * 10, device,
caller, true /*hasModifyAudioSettings*/);
}
- if (!vgsVssSyncMuteOrder()) {
- if ((isMuted() != streamMuted) && isVssMuteBijective(
- stream)) {
- vss.mute(isMuted(), "VGS.applyAllVolumes#1");
- }
- }
}
}
}
@@ -15093,11 +15082,13 @@ public class AudioService extends IAudioService.Stub
final String key = "additional_output_device_delay";
final String reply = AudioSystem.getParameters(
key + "=" + device.getInternalType() + "," + device.getAddress());
- long delayMillis;
- try {
- delayMillis = Long.parseLong(reply.substring(key.length() + 1));
- } catch (NullPointerException e) {
- delayMillis = 0;
+ long delayMillis = 0;
+ if (reply.contains(key)) {
+ try {
+ delayMillis = Long.parseLong(reply.substring(key.length() + 1));
+ } catch (NullPointerException e) {
+ delayMillis = 0;
+ }
}
return delayMillis;
}
@@ -15123,11 +15114,13 @@ public class AudioService extends IAudioService.Stub
final String key = "max_additional_output_device_delay";
final String reply = AudioSystem.getParameters(
key + "=" + device.getInternalType() + "," + device.getAddress());
- long delayMillis;
- try {
- delayMillis = Long.parseLong(reply.substring(key.length() + 1));
- } catch (NullPointerException e) {
- delayMillis = 0;
+ long delayMillis = 0;
+ if (reply.contains(key)) {
+ try {
+ delayMillis = Long.parseLong(reply.substring(key.length() + 1));
+ } catch (NullPointerException e) {
+ delayMillis = 0;
+ }
}
return delayMillis;
}
diff --git a/services/core/java/com/android/server/backup/InputBackupHelper.java b/services/core/java/com/android/server/backup/InputBackupHelper.java
new file mode 100644
index 000000000000..af9606c6e70f
--- /dev/null
+++ b/services/core/java/com/android/server/backup/InputBackupHelper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 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.backup;
+
+import static com.android.server.input.InputManagerInternal.BACKUP_CATEGORY_INPUT_GESTURES;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.backup.BlobBackupHelper;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+import com.android.server.input.InputManagerInternal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class InputBackupHelper extends BlobBackupHelper {
+ private static final String TAG = "InputBackupHelper"; // must be < 23 chars
+
+ // Current version of the blob schema
+ private static final int BLOB_VERSION = 1;
+
+ // Key under which the payload blob is stored
+ private static final String KEY_INPUT_GESTURES = "input_gestures";
+
+ private final @UserIdInt int mUserId;
+
+ private final @NonNull InputManagerInternal mInputManagerInternal;
+
+ public InputBackupHelper(int userId) {
+ super(BLOB_VERSION, KEY_INPUT_GESTURES);
+ mUserId = userId;
+ mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+ }
+
+ @Override
+ protected byte[] getBackupPayload(String key) {
+ Map<Integer, byte[]> payloads;
+ try {
+ payloads = mInputManagerInternal.getBackupPayload(mUserId);
+ } catch (Exception exception) {
+ Slog.e(TAG, "Failed to get backup payload for input gestures", exception);
+ return null;
+ }
+
+ if (KEY_INPUT_GESTURES.equals(key)) {
+ return payloads.getOrDefault(BACKUP_CATEGORY_INPUT_GESTURES, null);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void applyRestoredPayload(String key, byte[] payload) {
+ Map<Integer, byte[]> payloads = new HashMap<>();
+ if (KEY_INPUT_GESTURES.equals(key)) {
+ payloads.put(BACKUP_CATEGORY_INPUT_GESTURES, payload);
+ }
+
+ try {
+ mInputManagerInternal.applyBackupPayload(payloads, mUserId);
+ } catch (Exception exception) {
+ Slog.e(TAG, "Failed to apply input backup payload", exception);
+ }
+ }
+
+}
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index 677e0c055455..b11267ef8634 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -68,6 +68,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final String COMPANION_HELPER = "companion";
private static final String SYSTEM_GENDER_HELPER = "system_gender";
private static final String DISPLAY_HELPER = "display";
+ private static final String INPUT_HELPER = "input";
// These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME
// are also used in the full-backup file format, so must not change unless steps are
@@ -112,7 +113,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final Set<String> sEligibleHelpersForNonSystemUser =
SetUtils.union(sEligibleHelpersForProfileUser,
Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER,
- SHORTCUT_MANAGER_HELPER));
+ SHORTCUT_MANAGER_HELPER, INPUT_HELPER));
private int mUserId = UserHandle.USER_SYSTEM;
private boolean mIsProfileUser = false;
@@ -149,6 +150,9 @@ public class SystemBackupAgent extends BackupAgentHelper {
addHelperIfEligibleForUser(SYSTEM_GENDER_HELPER,
new SystemGrammaticalGenderBackupHelper(mUserId));
addHelperIfEligibleForUser(DISPLAY_HELPER, new DisplayBackupHelper(mUserId));
+ if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) {
+ addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId));
+ }
}
@Override
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 471b7b4ddfc8..d412277d2605 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -24,8 +24,10 @@ import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
import android.companion.virtual.sensor.VirtualSensor;
import android.content.Context;
+import android.hardware.display.IVirtualDisplayCallback;
import android.os.LocaleList;
import android.util.ArraySet;
+import android.window.DisplayWindowPolicyController;
import java.util.Set;
import java.util.function.Consumer;
@@ -104,6 +106,17 @@ public abstract class VirtualDeviceManagerInternal {
public abstract @NonNull ArraySet<Integer> getDeviceIdsForUid(int uid);
/**
+ * Notifies that a virtual display was created.
+ *
+ * @param virtualDevice The virtual device that owns the virtual display.
+ * @param displayId The display id of the created virtual display.
+ * @param callback The callback of the virtual display.
+ * @param dwpc The DisplayWindowPolicyController of the created virtual display.
+ */
+ public abstract void onVirtualDisplayCreated(IVirtualDevice virtualDevice, int displayId,
+ IVirtualDisplayCallback callback, DisplayWindowPolicyController dwpc);
+
+ /**
* Notifies that a virtual display is removed.
*
* @param virtualDevice The virtual device where the virtual display located.
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e83efc573ea8..854b0dd7676b 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2041,6 +2041,7 @@ public final class DisplayManagerService extends SystemService {
packageName,
displayUniqueId,
virtualDevice,
+ dwpc,
surface,
flags,
virtualDisplayConfig);
@@ -2135,6 +2136,7 @@ public final class DisplayManagerService extends SystemService {
String packageName,
String uniqueId,
IVirtualDevice virtualDevice,
+ DisplayWindowPolicyController dwpc,
Surface surface,
int flags,
VirtualDisplayConfig virtualDisplayConfig) {
@@ -2188,6 +2190,16 @@ public final class DisplayManagerService extends SystemService {
final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
if (display != null) {
+ // Notify the virtual device that the display has been created. This needs to be called
+ // in this locked section before the repository had the chance to notify any listeners
+ // to ensure that the device is aware of the new display before others know about it.
+ if (virtualDevice != null) {
+ final VirtualDeviceManagerInternal vdm =
+ getLocalService(VirtualDeviceManagerInternal.class);
+ vdm.onVirtualDisplayCreated(
+ virtualDevice, display.getDisplayIdLocked(), callback, dwpc);
+ }
+
return display.getDisplayIdLocked();
}
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b49c01b3e2a8..83ca563e0534 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -781,6 +781,11 @@ final class LocalDisplayAdapter extends DisplayAdapter {
if (isDisplayPrivate(physicalAddress)) {
mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE;
}
+
+ if (isDisplayStealTopFocusDisabled(physicalAddress)) {
+ mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_FOCUS;
+ mInfo.flags |= DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED;
+ }
}
if (DisplayCutout.getMaskBuiltInDisplayCutout(res, mInfo.uniqueId)) {
@@ -1467,6 +1472,23 @@ final class LocalDisplayAdapter extends DisplayAdapter {
}
return false;
}
+
+ private boolean isDisplayStealTopFocusDisabled(DisplayAddress.Physical physicalAddress) {
+ if (physicalAddress == null) {
+ return false;
+ }
+ final Resources res = getOverlayContext().getResources();
+ int[] ports = res.getIntArray(R.array.config_localNotStealTopFocusDisplayPorts);
+ if (ports != null) {
+ int port = physicalAddress.getPort();
+ for (int p : ports) {
+ if (p == port) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
private boolean hdrTypesEqual(int[] modeHdrTypes, int[] recordHdrTypes) {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index a1e8f08db0a6..aab2760dbc66 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -122,11 +122,6 @@ public class DisplayManagerFlags {
Flags.FLAG_ALWAYS_ROTATE_DISPLAY_DEVICE,
Flags::alwaysRotateDisplayDevice);
- private final FlagState mRefreshRateVotingTelemetry = new FlagState(
- Flags.FLAG_REFRESH_RATE_VOTING_TELEMETRY,
- Flags::refreshRateVotingTelemetry
- );
-
private final FlagState mPixelAnisotropyCorrectionEnabled = new FlagState(
Flags.FLAG_ENABLE_PIXEL_ANISOTROPY_CORRECTION,
Flags::enablePixelAnisotropyCorrection
@@ -403,10 +398,6 @@ public class DisplayManagerFlags {
return mAlwaysRotateDisplayDevice.isEnabled();
}
- public boolean isRefreshRateVotingTelemetryEnabled() {
- return mRefreshRateVotingTelemetry.isEnabled();
- }
-
public boolean isPixelAnisotropyCorrectionInLogicalDisplayEnabled() {
return mPixelAnisotropyCorrectionEnabled.isEnabled();
}
@@ -626,7 +617,6 @@ public class DisplayManagerFlags {
pw.println(" " + mAutoBrightnessModesFlagState);
pw.println(" " + mFastHdrTransitions);
pw.println(" " + mAlwaysRotateDisplayDevice);
- pw.println(" " + mRefreshRateVotingTelemetry);
pw.println(" " + mPixelAnisotropyCorrectionEnabled);
pw.println(" " + mSensorBasedBrightnessThrottling);
pw.println(" " + mIdleScreenRefreshRateTimeout);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index cc0bbde370fe..8211febade60 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -191,14 +191,6 @@ flag {
}
flag {
- name: "refresh_rate_voting_telemetry"
- namespace: "display_manager"
- description: "Feature flag for enabling telemetry for refresh rate voting in DisplayManager"
- bug: "310029108"
- is_fixed_read_only: true
-}
-
-flag {
name: "enable_pixel_anisotropy_correction"
namespace: "display_manager"
description: "Feature flag for enabling display anisotropy correction through LogicalDisplay upscaling"
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 1dd4a9b93277..c37733b05fba 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -229,8 +229,7 @@ public class DisplayModeDirector {
mContext = context;
mHandler = new DisplayModeDirectorHandler(handler.getLooper());
mInjector = injector;
- mVotesStatsReporter = injector.getVotesStatsReporter(
- displayManagerFlags.isRefreshRateVotingTelemetryEnabled());
+ mVotesStatsReporter = injector.getVotesStatsReporter();
mSupportedModesByDisplay = new SparseArray<>();
mAppSupportedModesByDisplay = new SparseArray<>();
mDefaultModeByDisplay = new SparseArray<>();
@@ -3141,7 +3140,7 @@ public class DisplayModeDirector {
SensorManagerInternal getSensorManagerInternal();
@Nullable
- VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled);
+ VotesStatsReporter getVotesStatsReporter();
}
@VisibleForTesting
@@ -3281,10 +3280,9 @@ public class DisplayModeDirector {
}
@Override
- public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) {
+ public VotesStatsReporter getVotesStatsReporter() {
// if frame rate override supported, renderRates will be ignored in mode selection
- return new VotesStatsReporter(supportsFrameRateOverride(),
- refreshRateVotingTelemetryEnabled);
+ return new VotesStatsReporter(supportsFrameRateOverride());
}
private DisplayManager getDisplayManager() {
diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
index 7562a525b5f6..d3d49c272338 100644
--- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
+++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
@@ -36,13 +36,11 @@ class VotesStatsReporter {
private static final String TAG = "VotesStatsReporter";
private static final int REFRESH_RATE_NOT_LIMITED = 1000;
private final boolean mIgnoredRenderRate;
- private final boolean mFrameworkStatsLogReportingEnabled;
private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1;
- public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) {
+ VotesStatsReporter(boolean ignoreRenderRate) {
mIgnoredRenderRate = ignoreRenderRate;
- mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled;
}
void reportVoteChanged(int displayId, int priority, @Nullable Vote vote) {
@@ -57,29 +55,22 @@ class VotesStatsReporter {
int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
Trace.traceCounter(Trace.TRACE_TAG_POWER,
TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate);
- if (mFrameworkStatsLogReportingEnabled) {
- FrameworkStatsLog.write(
- DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
- DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
- maxRefreshRate, -1);
- }
+ FrameworkStatsLog.write(
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+ maxRefreshRate, -1);
}
private void reportVoteRemoved(int displayId, int priority) {
Trace.traceCounter(Trace.TRACE_TAG_POWER,
TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1);
- if (mFrameworkStatsLogReportingEnabled) {
- FrameworkStatsLog.write(
- DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
- DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
- }
+ FrameworkStatsLog.write(
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+ DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
}
void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode,
SparseArray<Vote> votes) {
- if (!mFrameworkStatsLogReportingEnabled) {
- return;
- }
int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) {
if (priority < mLastMinPriorityReported && priority < minPriority) {
diff --git a/services/core/java/com/android/server/input/InputDataStore.java b/services/core/java/com/android/server/input/InputDataStore.java
index e8f21fe8fb74..834f8154240e 100644
--- a/services/core/java/com/android/server/input/InputDataStore.java
+++ b/services/core/java/com/android/server/input/InputDataStore.java
@@ -125,8 +125,20 @@ public final class InputDataStore {
}
}
- @VisibleForTesting
- List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
+ /**
+ * Parses the given input stream and returns the list of {@link InputGestureData} objects.
+ * This parsing happens on a best effort basis. If invalid data exists in the given payload
+ * it will be skipped. An example of this would be a keycode that does not exist in the
+ * present version of Android. If the payload is malformed, instead this will throw an
+ * exception and require the caller to handel this appropriately for its situation.
+ *
+ * @param stream stream of the input payload of XML data
+ * @param utf8Encoded whether or not the input data is UTF-8 encoded
+ * @return list of {@link InputGestureData} objects pulled from the payload
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ public List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
throws XmlPullParserException, IOException {
List<InputGestureData> inputGestureDataList = new ArrayList<>();
TypedXmlPullParser parser;
@@ -153,6 +165,31 @@ public final class InputDataStore {
return inputGestureDataList;
}
+ /**
+ * Serializes the given list of {@link InputGestureData} objects to XML in the provided output
+ * stream.
+ *
+ * @param stream output stream to put serialized data.
+ * @param utf8Encoded whether or not to encode the serialized data in UTF-8 format.
+ * @param inputGestureDataList the list of {@link InputGestureData} objects to serialize.
+ */
+ public void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
+ List<InputGestureData> inputGestureDataList) throws IOException {
+ final TypedXmlSerializer serializer;
+ if (utf8Encoded) {
+ serializer = Xml.newFastSerializer();
+ serializer.setOutput(stream, StandardCharsets.UTF_8.name());
+ } else {
+ serializer = Xml.resolveSerializer(stream);
+ }
+
+ serializer.startDocument(null, true);
+ serializer.startTag(null, TAG_ROOT);
+ writeInputGestureListToXml(serializer, inputGestureDataList);
+ serializer.endTag(null, TAG_ROOT);
+ serializer.endDocument();
+ }
+
private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException, IllegalArgumentException {
InputGestureData.Builder builder = new InputGestureData.Builder();
@@ -239,24 +276,6 @@ public final class InputDataStore {
return inputGestureDataList;
}
- @VisibleForTesting
- void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
- List<InputGestureData> inputGestureDataList) throws IOException {
- final TypedXmlSerializer serializer;
- if (utf8Encoded) {
- serializer = Xml.newFastSerializer();
- serializer.setOutput(stream, StandardCharsets.UTF_8.name());
- } else {
- serializer = Xml.resolveSerializer(stream);
- }
-
- serializer.startDocument(null, true);
- serializer.startTag(null, TAG_ROOT);
- writeInputGestureListToXml(serializer, inputGestureDataList);
- serializer.endTag(null, TAG_ROOT);
- serializer.endDocument();
- }
-
private void writeInputGestureToXml(TypedXmlSerializer serializer,
InputGestureData inputGestureData) throws IOException {
serializer.startTag(null, TAG_INPUT_GESTURE);
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index d2486fe8bd66..87f693cc7291 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -16,6 +16,7 @@
package com.android.server.input;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -32,7 +33,11 @@ import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.policy.IShortcutService;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
import java.util.List;
+import java.util.Map;
/**
* Input manager local system service interface.
@@ -41,6 +46,15 @@ import java.util.List;
*/
public abstract class InputManagerInternal {
+ // Backup and restore information for custom input gestures.
+ public static final int BACKUP_CATEGORY_INPUT_GESTURES = 0;
+
+ // Backup and Restore categories for sending map of data back and forth to backup and restore
+ // infrastructure.
+ @IntDef({BACKUP_CATEGORY_INPUT_GESTURES})
+ public @interface BackupCategory {
+ }
+
/**
* Called by the display manager to set information about the displays as needed
* by the input system. The input system must copy this information to retain it.
@@ -312,4 +326,22 @@ public abstract class InputManagerInternal {
* @return true if setting power wakeup was successful.
*/
public abstract boolean setKernelWakeEnabled(int deviceId, boolean enabled);
+
+ /**
+ * Retrieves the input gestures backup payload data.
+ *
+ * @param userId the user ID of the backup data.
+ * @return byte array of UTF-8 encoded backup data.
+ */
+ public abstract Map<Integer, byte[]> getBackupPayload(int userId) throws IOException;
+
+ /**
+ * Applies the given UTF-8 encoded byte array payload to the given user's input data
+ * on a best effort basis.
+ *
+ * @param payload UTF-8 encoded map of byte arrays of restored data
+ * @param userId the user ID for which to apply the payload data
+ */
+ public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
+ throws XmlPullParserException, IOException;
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 2ad5a1538da9..4a5f4a19893a 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -24,6 +24,7 @@ import static android.provider.DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT;
import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.keyEventActivityDetection;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
@@ -153,6 +154,8 @@ import com.android.server.wm.WindowManagerInternal;
import libcore.io.IoUtils;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
@@ -3805,6 +3808,26 @@ public class InputManagerService extends IInputManager.Stub
public boolean setKernelWakeEnabled(int deviceId, boolean enabled) {
return mNative.setKernelWakeEnabled(deviceId, enabled);
}
+
+ @Override
+ public Map<Integer, byte[]> getBackupPayload(int userId) throws IOException {
+ final Map<Integer, byte[]> payload = new HashMap<>();
+ if (enableCustomizableInputGestures()) {
+ payload.put(BACKUP_CATEGORY_INPUT_GESTURES,
+ mKeyGestureController.getInputGestureBackupPayload(userId));
+ }
+ return payload;
+ }
+
+ @Override
+ public void applyBackupPayload(Map<Integer, byte[]> payload, int userId)
+ throws XmlPullParserException, IOException {
+ if (enableCustomizableInputGestures() && payload.containsKey(
+ BACKUP_CATEGORY_INPUT_GESTURES)) {
+ mKeyGestureController.applyInputGesturesBackupPayload(
+ payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId);
+ }
+ }
}
@Override
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 41f58ae76a4d..5770a09e3b92 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -69,6 +69,11 @@ import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.KeyCombinationManager;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.List;
@@ -1191,6 +1196,29 @@ final class KeyGestureController {
}
}
+ byte[] getInputGestureBackupPayload(int userId) throws IOException {
+ final List<InputGestureData> inputGestureDataList =
+ mInputGestureManager.getCustomInputGestures(userId, null);
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ synchronized (mInputDataStore) {
+ mInputDataStore.writeInputGestureXml(byteArrayOutputStream, true, inputGestureDataList);
+ }
+ return byteArrayOutputStream.toByteArray();
+ }
+
+ void applyInputGesturesBackupPayload(byte[] payload, int userId)
+ throws XmlPullParserException, IOException {
+ final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
+ List<InputGestureData> inputGestureDataList;
+ synchronized (mInputDataStore) {
+ inputGestureDataList = mInputDataStore.readInputGesturesXml(byteArrayInputStream, true);
+ }
+ for (final InputGestureData inputGestureData : inputGestureDataList) {
+ mInputGestureManager.addCustomInputGesture(userId, inputGestureData);
+ }
+ mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget();
+ }
+
// A record of a registered key gesture event listener from one process.
private class KeyGestureEventListenerRecord implements IBinder.DeathRecipient {
public final int mPid;
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 12495bb4f2cc..d7d0eb40af70 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -612,25 +612,23 @@ class GnssNetworkConnectivityHandler {
networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType));
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
- if (com.android.internal.telephony.flags.Flags.satelliteInternet()) {
- // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network.
- TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
- if (telephonyManager != null) {
- ServiceState state = telephonyManager.getServiceState();
- if (state != null && state.isUsingNonTerrestrialNetwork()) {
- networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
- try {
- networkRequestBuilder.addTransportType(NetworkCapabilities
- .TRANSPORT_SATELLITE);
- networkRequestBuilder.removeCapability(NetworkCapabilities
- .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
- } catch (IllegalArgumentException ignored) {
- // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
- // are not recognized, meaning an old connectivity module runs on new
- // android in which case no network with such capabilities will be brought
- // up, so it's safe to ignore the exception.
- // TODO: Can remove the try-catch in next quarter release.
- }
+ // Add transport type NetworkCapabilities.TRANSPORT_SATELLITE on satellite network.
+ TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
+ if (telephonyManager != null) {
+ ServiceState state = telephonyManager.getServiceState();
+ if (state != null && state.isUsingNonTerrestrialNetwork()) {
+ networkRequestBuilder.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+ try {
+ networkRequestBuilder.addTransportType(NetworkCapabilities
+ .TRANSPORT_SATELLITE);
+ networkRequestBuilder.removeCapability(NetworkCapabilities
+ .NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED);
+ } catch (IllegalArgumentException ignored) {
+ // In case TRANSPORT_SATELLITE or NET_CAPABILITY_NOT_BANDWIDTH_CONSTRAINED
+ // are not recognized, meaning an old connectivity module runs on new
+ // android in which case no network with such capabilities will be brought
+ // up, so it's safe to ignore the exception.
+ // TODO: Can remove the try-catch in next quarter release.
}
}
}
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
index b529853c63a4..058bbc08a9ef 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
@@ -267,6 +267,50 @@ import java.util.stream.Stream;
notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
}
+ @Override
+ public void selectRoute(long requestId, String sessionId, String routeId) {
+ if (SYSTEM_SESSION_ID.equals(sessionId)) {
+ super.selectRoute(requestId, sessionId, routeId);
+ return;
+ }
+ synchronized (mLock) {
+ var sessionRecord = getSessionRecordByOriginalId(sessionId);
+ var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null;
+ if (proxyRecord != null) {
+ var targetSourceRouteId =
+ proxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(routeId);
+ if (targetSourceRouteId != null) {
+ proxyRecord.mProxy.selectRoute(
+ requestId, sessionRecord.getServiceSessionId(), targetSourceRouteId);
+ }
+ return;
+ }
+ }
+ notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
+ }
+
+ @Override
+ public void deselectRoute(long requestId, String sessionId, String routeId) {
+ if (SYSTEM_SESSION_ID.equals(sessionId)) {
+ super.selectRoute(requestId, sessionId, routeId);
+ return;
+ }
+ synchronized (mLock) {
+ var sessionRecord = getSessionRecordByOriginalId(sessionId);
+ var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null;
+ if (proxyRecord != null) {
+ var targetSourceRouteId =
+ proxyRecord.mNewOriginalIdToSourceOriginalIdMap.get(routeId);
+ if (targetSourceRouteId != null) {
+ proxyRecord.mProxy.deselectRoute(
+ requestId, sessionRecord.getServiceSessionId(), targetSourceRouteId);
+ }
+ return;
+ }
+ }
+ notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
+ }
+
@GuardedBy("mLock")
private SystemMediaSessionRecord getSessionRecordByOriginalId(String sessionOriginalId) {
if (FORCE_GLOBAL_ROUTING_SESSION) {
diff --git a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
index e2889fa9cbf6..18bccd8411d7 100644
--- a/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationChannelExtractor.java
@@ -91,7 +91,7 @@ public class NotificationChannelExtractor implements NotificationSignalExtractor
updateAttributes = true;
}
if (restrictAudioAttributesAlarm()
- && record.getNotification().category != CATEGORY_ALARM
+ && !CATEGORY_ALARM.equals(record.getNotification().category)
&& attributes.getUsage() == AudioAttributes.USAGE_ALARM) {
updateAttributes = true;
}
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index cc4c2b5bf893..068d68d25017 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -40,6 +40,7 @@ import static com.android.server.pm.AppsFilterUtils.canQueryViaUsesLibrary;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ApplicationPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
@@ -173,6 +174,10 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable,
* Report a change to observers.
*/
private void onChanged() {
+ // App visibility may have changed, which means that earlier fetches from these caches may
+ // be invalid.
+ PackageManager.invalidatePackageInfoCache();
+ ApplicationPackageManager.invalidateGetPackagesForUidCache();
dispatchChange(this);
}
diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
index dc1f93664f79..f060e4d11e82 100644
--- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
+++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java
@@ -17,8 +17,8 @@
package com.android.server.security;
import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_BOOT_STATE;
-import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS;
import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS;
+import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_KEYSTORE_REQUIREMENTS;
import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS;
import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_PATCH_LEVEL_DIFF;
import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN;
@@ -47,12 +47,8 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.security.AttestationVerificationManagerService.DumpLogger;
-import org.json.JSONObject;
-
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
@@ -60,7 +56,6 @@ import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
-import java.security.cert.PKIXCertPathChecker;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
@@ -69,7 +64,6 @@ import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -126,6 +120,7 @@ class AttestationVerificationPeerDeviceVerifier {
private final LocalDate mTestLocalPatchDate;
private final CertificateFactory mCertificateFactory;
private final CertPathValidator mCertPathValidator;
+ private final CertificateRevocationStatusManager mCertificateRevocationStatusManager;
private final DumpLogger mDumpLogger;
AttestationVerificationPeerDeviceVerifier(@NonNull Context context,
@@ -135,6 +130,7 @@ class AttestationVerificationPeerDeviceVerifier {
mCertificateFactory = CertificateFactory.getInstance("X.509");
mCertPathValidator = CertPathValidator.getInstance("PKIX");
mTrustAnchors = getTrustAnchors();
+ mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext);
mRevocationEnabled = true;
mTestSystemDate = null;
mTestLocalPatchDate = null;
@@ -150,6 +146,7 @@ class AttestationVerificationPeerDeviceVerifier {
mCertificateFactory = CertificateFactory.getInstance("X.509");
mCertPathValidator = CertPathValidator.getInstance("PKIX");
mTrustAnchors = trustAnchors;
+ mCertificateRevocationStatusManager = new CertificateRevocationStatusManager(mContext);
mRevocationEnabled = revocationEnabled;
mTestSystemDate = systemDate;
mTestLocalPatchDate = localPatchDate;
@@ -300,15 +297,14 @@ class AttestationVerificationPeerDeviceVerifier {
CertPath certificatePath = mCertificateFactory.generateCertPath(certificates);
PKIXParameters validationParams = new PKIXParameters(mTrustAnchors);
+ // Do not use built-in revocation status checker.
+ validationParams.setRevocationEnabled(false);
+ mCertPathValidator.validate(certificatePath, validationParams);
if (mRevocationEnabled) {
// Checks Revocation Status List based on
// https://developer.android.com/training/articles/security-key-attestation#certificate_status
- PKIXCertPathChecker checker = new AndroidRevocationStatusListChecker();
- validationParams.addCertPathChecker(checker);
+ mCertificateRevocationStatusManager.checkRevocationStatus(certificates);
}
- // Do not use built-in revocation status checker.
- validationParams.setRevocationEnabled(false);
- mCertPathValidator.validate(certificatePath, validationParams);
}
private Set<TrustAnchor> getTrustAnchors() throws CertPathValidatorException {
@@ -574,96 +570,6 @@ class AttestationVerificationPeerDeviceVerifier {
<= maxPatchLevelDiffMonths;
}
- /**
- * Checks certificate revocation status.
- *
- * Queries status list from android.googleapis.com/attestation/status and checks for
- * the existence of certificate's serial number. If serial number exists in map, then fail.
- */
- private final class AndroidRevocationStatusListChecker extends PKIXCertPathChecker {
- private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
- private static final String STATUS_PROPERTY_KEY = "status";
- private static final String REASON_PROPERTY_KEY = "reason";
- private String mStatusUrl;
- private JSONObject mJsonStatusMap;
-
- @Override
- public void init(boolean forward) throws CertPathValidatorException {
- mStatusUrl = getRevocationListUrl();
- if (mStatusUrl == null || mStatusUrl.isEmpty()) {
- throw new CertPathValidatorException(
- "R.string.vendor_required_attestation_revocation_list_url is empty.");
- }
- // TODO(b/221067843): Update to only pull status map on non critical path and if
- // out of date (24hrs).
- mJsonStatusMap = getStatusMap(mStatusUrl);
- }
-
- @Override
- public boolean isForwardCheckingSupported() {
- return false;
- }
-
- @Override
- public Set<String> getSupportedExtensions() {
- return null;
- }
-
- @Override
- public void check(Certificate cert, Collection<String> unresolvedCritExts)
- throws CertPathValidatorException {
- X509Certificate x509Certificate = (X509Certificate) cert;
- // The json key is the certificate's serial number converted to lowercase hex.
- String serialNumber = x509Certificate.getSerialNumber().toString(16);
-
- if (serialNumber == null) {
- throw new CertPathValidatorException("Certificate serial number can not be null.");
- }
-
- if (mJsonStatusMap.has(serialNumber)) {
- JSONObject revocationStatus;
- String status;
- String reason;
- try {
- revocationStatus = mJsonStatusMap.getJSONObject(serialNumber);
- status = revocationStatus.getString(STATUS_PROPERTY_KEY);
- reason = revocationStatus.getString(REASON_PROPERTY_KEY);
- } catch (Throwable t) {
- throw new CertPathValidatorException("Unable get properties for certificate "
- + "with serial number " + serialNumber);
- }
- throw new CertPathValidatorException(
- "Invalid certificate with serial number " + serialNumber
- + " has status " + status
- + " because reason " + reason);
- }
- }
-
- private JSONObject getStatusMap(String stringUrl) throws CertPathValidatorException {
- URL url;
- try {
- url = new URL(stringUrl);
- } catch (Throwable t) {
- throw new CertPathValidatorException(
- "Unable to get revocation status from " + mStatusUrl, t);
- }
-
- try (InputStream inputStream = url.openStream()) {
- JSONObject statusListJson = new JSONObject(
- new String(inputStream.readAllBytes(), UTF_8));
- return statusListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
- } catch (Throwable t) {
- throw new CertPathValidatorException(
- "Unable to parse revocation status from " + mStatusUrl, t);
- }
- }
-
- private String getRevocationListUrl() {
- return mContext.getResources().getString(
- R.string.vendor_required_attestation_revocation_list_url);
- }
- }
-
/* Mutable data class for tracking dump data from verifications. */
private static class MyDumpData extends AttestationVerificationManagerService.DumpData {
diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
new file mode 100644
index 000000000000..d36d9f5f6636
--- /dev/null
+++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.security;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Environment;
+import android.os.PersistableBundle;
+import android.util.Slog;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.X509Certificate;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Manages the revocation status of certificates used in remote attestation. */
+class CertificateRevocationStatusManager {
+ private static final String TAG = "AVF_CRL";
+ // Must be unique within system server
+ private static final int JOB_ID = 1737671340;
+ private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt";
+ private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ",";
+
+ /**
+ * The number of days since last update for which a stored revocation status can be accepted.
+ */
+ @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30;
+
+ /**
+ * The number of days since issue date for an intermediary certificate to be considered fresh
+ * and not require a revocation list check.
+ */
+ private static final int FRESH_INTERMEDIARY_CERT_DAYS = 70;
+
+ /**
+ * The expected number of days between a certificate's issue date and notBefore date. Used to
+ * infer a certificate's issue date from its notBefore date.
+ */
+ private static final int DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES = 2;
+
+ private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
+ private static final Object sFileLock = new Object();
+
+ private final Context mContext;
+ private final String mTestRemoteRevocationListUrl;
+ private final File mTestRevocationStatusFile;
+ private final boolean mShouldScheduleJob;
+
+ CertificateRevocationStatusManager(Context context) {
+ this(context, null, null, true);
+ }
+
+ @VisibleForTesting
+ CertificateRevocationStatusManager(
+ Context context,
+ String testRemoteRevocationListUrl,
+ File testRevocationStatusFile,
+ boolean shouldScheduleJob) {
+ mContext = context;
+ mTestRemoteRevocationListUrl = testRemoteRevocationListUrl;
+ mTestRevocationStatusFile = testRevocationStatusFile;
+ mShouldScheduleJob = shouldScheduleJob;
+ }
+
+ /**
+ * Check the revocation status of the provided {@link X509Certificate}s.
+ *
+ * <p>The provided certificates should have been validated and ordered from leaf to a
+ * certificate issued by the trust anchor, per the convention specified in the javadoc of {@link
+ * java.security.cert.CertPath}.
+ *
+ * @param certificates List of certificates to be checked
+ * @throws CertPathValidatorException if the check failed
+ */
+ void checkRevocationStatus(List<X509Certificate> certificates)
+ throws CertPathValidatorException {
+ if (!needToCheckRevocationStatus(certificates)) {
+ return;
+ }
+ List<String> serialNumbers = new ArrayList<>();
+ for (X509Certificate certificate : certificates) {
+ String serialNumber = certificate.getSerialNumber().toString(16);
+ if (serialNumber == null) {
+ throw new CertPathValidatorException("Certificate serial number cannot be null.");
+ }
+ serialNumbers.add(serialNumber);
+ }
+ try {
+ JSONObject revocationList = fetchRemoteRevocationList();
+ Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
+ for (String serialNumber : serialNumbers) {
+ areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
+ }
+ updateLastRevocationCheckData(areCertificatesRevoked);
+ for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
+ if (entry.getValue()) {
+ throw new CertPathValidatorException(
+ "Certificate " + entry.getKey() + " has been revoked.");
+ }
+ }
+ } catch (IOException | JSONException ex) {
+ Slog.d(TAG, "Fallback to check stored revocation status", ex);
+ if (ex instanceof IOException && mShouldScheduleJob) {
+ scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers);
+ }
+ for (X509Certificate certificate : certificates) {
+ // Assume recently issued certificates are not revoked.
+ if (isIssuedWithinDays(certificate, MAX_DAYS_SINCE_LAST_CHECK)) {
+ String serialNumber = certificate.getSerialNumber().toString(16);
+ serialNumbers.remove(serialNumber);
+ }
+ }
+ Map<String, LocalDateTime> lastRevocationCheckData;
+ try {
+ lastRevocationCheckData = getLastRevocationCheckData();
+ } catch (IOException ex2) {
+ throw new CertPathValidatorException(
+ "Unable to load stored revocation status", ex2);
+ }
+ for (String serialNumber : serialNumbers) {
+ if (!lastRevocationCheckData.containsKey(serialNumber)
+ || lastRevocationCheckData
+ .get(serialNumber)
+ .isBefore(
+ LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) {
+ throw new CertPathValidatorException(
+ "Unable to verify the revocation status of certificate "
+ + serialNumber);
+ }
+ }
+ }
+ }
+
+ private static boolean needToCheckRevocationStatus(
+ List<X509Certificate> certificatesOrderedLeafFirst) {
+ if (certificatesOrderedLeafFirst.isEmpty()) {
+ return false;
+ }
+ // A certificate isn't revoked when it is first issued, so we treat it as checked on its
+ // issue date.
+ if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) {
+ return true;
+ }
+ for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) {
+ if (!isIssuedWithinDays(
+ certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isIssuedWithinDays(X509Certificate certificate, int days) {
+ LocalDate notBeforeDate =
+ LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
+ LocalDate expectedIssueData =
+ notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES);
+ return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData);
+ }
+
+ void updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
+ JSONObject revocationList, Collection<String> otherCertificatesToCheck) {
+ Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck);
+ try {
+ allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet());
+ } catch (IOException ex) {
+ Slog.e(TAG, "Unable to update last check date of stored data.", ex);
+ }
+ Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
+ for (String serialNumber : allCertificatesToCheck) {
+ areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
+ }
+ updateLastRevocationCheckData(areCertificatesRevoked);
+ }
+
+ /**
+ * Update the last revocation check data stored on this device.
+ *
+ * @param areCertificatesRevoked A Map whose keys are certificate serial numbers and values are
+ * whether that certificate has been revoked
+ */
+ void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) {
+ LocalDateTime now = LocalDateTime.now();
+ synchronized (sFileLock) {
+ Map<String, LocalDateTime> lastRevocationCheckData;
+ try {
+ lastRevocationCheckData = getLastRevocationCheckData();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex);
+ return;
+ }
+ for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
+ if (entry.getValue()) {
+ lastRevocationCheckData.remove(entry.getKey());
+ } else {
+ lastRevocationCheckData.put(entry.getKey(), now);
+ }
+ }
+ storeLastRevocationCheckData(lastRevocationCheckData);
+ }
+ }
+
+ Map<String, LocalDateTime> getLastRevocationCheckData() throws IOException {
+ Map<String, LocalDateTime> data = new HashMap<>();
+ File dataFile = getLastRevocationCheckDataFile();
+ synchronized (sFileLock) {
+ if (!dataFile.exists()) {
+ return data;
+ }
+ String dataString;
+ try (FileInputStream in = new FileInputStream(dataFile)) {
+ dataString = new String(in.readAllBytes(), UTF_8);
+ }
+ for (String line : dataString.split(System.lineSeparator())) {
+ String[] elements = line.split(REVOCATION_STATUS_FILE_FIELD_DELIMITER);
+ if (elements.length != 2) {
+ continue;
+ }
+ try {
+ data.put(elements[0], LocalDateTime.parse(elements[1]));
+ } catch (DateTimeParseException ex) {
+ Slog.e(
+ TAG,
+ "Unable to parse last checked LocalDateTime from file. Deleting the"
+ + " potentially corrupted file.",
+ ex);
+ dataFile.delete();
+ return data;
+ }
+ }
+ }
+ return data;
+ }
+
+ @VisibleForTesting
+ void storeLastRevocationCheckData(Map<String, LocalDateTime> lastRevocationCheckData) {
+ StringBuilder dataStringBuilder = new StringBuilder();
+ for (Map.Entry<String, LocalDateTime> entry : lastRevocationCheckData.entrySet()) {
+ dataStringBuilder
+ .append(entry.getKey())
+ .append(REVOCATION_STATUS_FILE_FIELD_DELIMITER)
+ .append(entry.getValue())
+ .append(System.lineSeparator());
+ }
+ synchronized (sFileLock) {
+ try (FileOutputStream fileOutputStream =
+ new FileOutputStream(getLastRevocationCheckDataFile())) {
+ fileOutputStream.write(dataStringBuilder.toString().getBytes(UTF_8));
+ Slog.d(TAG, "Successfully stored revocation status data.");
+ } catch (IOException ex) {
+ Slog.e(TAG, "Failed to store revocation status data.", ex);
+ }
+ }
+ }
+
+ private File getLastRevocationCheckDataFile() {
+ if (mTestRevocationStatusFile != null) {
+ return mTestRevocationStatusFile;
+ }
+ return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME);
+ }
+
+ private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) {
+ JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
+ if (jobScheduler == null) {
+ Slog.e(TAG, "Unable to get job scheduler.");
+ return;
+ }
+ Slog.d(TAG, "Scheduling job to fetch remote CRL.");
+ PersistableBundle extras = new PersistableBundle();
+ extras.putStringArray(
+ UpdateCertificateRevocationStatusJobService.EXTRA_KEY_CERTIFICATES_TO_CHECK,
+ serialNumbers.toArray(new String[0]));
+ jobScheduler.schedule(
+ new JobInfo.Builder(
+ JOB_ID,
+ new ComponentName(
+ mContext,
+ UpdateCertificateRevocationStatusJobService.class))
+ .setExtras(extras)
+ .setRequiredNetwork(
+ new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ .build())
+ .build());
+ }
+
+ /**
+ * Fetches the revocation list from the URL specified in
+ * R.string.vendor_required_attestation_revocation_list_url
+ *
+ * @return The remote revocation list entries in a JSONObject
+ * @throws CertPathValidatorException if the URL is not defined or is malformed.
+ * @throws IOException if the URL is valid but the fetch failed.
+ * @throws JSONException if the revocation list content cannot be parsed
+ */
+ JSONObject fetchRemoteRevocationList()
+ throws CertPathValidatorException, IOException, JSONException {
+ String urlString = getRemoteRevocationListUrl();
+ if (urlString == null || urlString.isEmpty()) {
+ throw new CertPathValidatorException(
+ "R.string.vendor_required_attestation_revocation_list_url is empty.");
+ }
+ URL url;
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException ex) {
+ throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex);
+ }
+ byte[] revocationListBytes;
+ try (InputStream inputStream = url.openStream()) {
+ revocationListBytes = inputStream.readAllBytes();
+ }
+ JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8));
+ return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
+ }
+
+ private String getRemoteRevocationListUrl() {
+ if (mTestRemoteRevocationListUrl != null) {
+ return mTestRemoteRevocationListUrl;
+ }
+ return mContext.getResources()
+ .getString(R.string.vendor_required_attestation_revocation_list_url);
+ }
+}
diff --git a/services/core/java/com/android/server/security/OWNERS b/services/core/java/com/android/server/security/OWNERS
index fa4bf228c683..7a31a0006bb9 100644
--- a/services/core/java/com/android/server/security/OWNERS
+++ b/services/core/java/com/android/server/security/OWNERS
@@ -3,5 +3,6 @@
include /core/java/android/security/OWNERS
per-file *AttestationVerification* = file:/core/java/android/security/attestationverification/OWNERS
+per-file *CertificateRevocationStatus* = file:/core/java/android/security/attestationverification/OWNERS
per-file FileIntegrity*.java = victorhsieh@google.com
per-file KeyChainSystemService.java = file:platform/packages/apps/KeyChain:/OWNERS
diff --git a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
new file mode 100644
index 000000000000..768c812f47a3
--- /dev/null
+++ b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.security;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.util.Slog;
+
+import org.json.JSONObject;
+
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/** A {@link JobService} that fetches the certificate revocation list from a remote location. */
+public class UpdateCertificateRevocationStatusJobService extends JobService {
+
+ static final String EXTRA_KEY_CERTIFICATES_TO_CHECK =
+ "com.android.server.security.extra.CERTIFICATES_TO_CHECK";
+ private static final String TAG = "AVF_CRL";
+ private ExecutorService mExecutorService;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mExecutorService = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ mExecutorService.execute(
+ () -> {
+ try {
+ CertificateRevocationStatusManager certificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(this);
+ Slog.d(TAG, "Starting to fetch remote CRL from job service.");
+ JSONObject revocationList =
+ certificateRevocationStatusManager.fetchRemoteRevocationList();
+ String[] certificatesToCheckFromJobParams =
+ params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK);
+ if (certificatesToCheckFromJobParams == null) {
+ Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK);
+ return;
+ }
+ certificateRevocationStatusManager
+ .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
+ revocationList,
+ Arrays.asList(certificatesToCheckFromJobParams));
+ } catch (Throwable t) {
+ Slog.e(TAG, "Unable to update the stored revocation status.", t);
+ }
+ jobFinished(params, false);
+ });
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mExecutorService.shutdown();
+ }
+}
diff --git a/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java
new file mode 100644
index 000000000000..9d60a576d9bc
--- /dev/null
+++ b/services/core/java/com/android/server/storage/ImmutableVolumeInfo.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.storage;
+
+import android.content.Context;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageVolume;
+import android.os.storage.VolumeInfo;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.File;
+
+/**
+ * An immutable version of {@link VolumeInfo} with only getters.
+ *
+ * @hide
+ */
+public final class ImmutableVolumeInfo {
+ private final VolumeInfo mVolumeInfo;
+
+ private ImmutableVolumeInfo(VolumeInfo volumeInfo) {
+ mVolumeInfo = new VolumeInfo(volumeInfo);
+ }
+
+ public static ImmutableVolumeInfo fromVolumeInfo(VolumeInfo info) {
+ return new ImmutableVolumeInfo(info);
+ }
+
+ public ImmutableVolumeInfo clone() {
+ return fromVolumeInfo(mVolumeInfo.clone());
+ }
+
+ public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
+ return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ mVolumeInfo.dump(pw);
+ }
+
+ public DiskInfo getDisk() {
+ return mVolumeInfo.getDisk();
+ }
+
+ public String getDiskId() {
+ return mVolumeInfo.getDiskId();
+ }
+
+ public String getFsLabel() {
+ return mVolumeInfo.fsLabel;
+ }
+
+ public String getFsPath() {
+ return mVolumeInfo.path;
+ }
+
+ public String getFsType() {
+ return mVolumeInfo.fsType;
+ }
+
+ public String getFsUuid() {
+ return mVolumeInfo.fsUuid;
+ }
+
+ public String getId() {
+ return mVolumeInfo.id;
+ }
+
+ public File getInternalPath() {
+ return mVolumeInfo.getInternalPath();
+ }
+
+ public int getMountFlags() {
+ return mVolumeInfo.mountFlags;
+ }
+
+ public int getMountUserId() {
+ return mVolumeInfo.mountUserId;
+ }
+
+ public String getPartGuid() {
+ return mVolumeInfo.partGuid;
+ }
+
+ public File getPath() {
+ return mVolumeInfo.getPath();
+ }
+
+ public int getState() {
+ return mVolumeInfo.state;
+ }
+
+ public int getType() {
+ return mVolumeInfo.type;
+ }
+
+ public VolumeInfo getVolumeInfo() {
+ return new VolumeInfo(mVolumeInfo); // Return a copy, not the original
+ }
+
+ public boolean isMountedReadable() {
+ return mVolumeInfo.isMountedReadable();
+ }
+
+ public boolean isMountedWritable() {
+ return mVolumeInfo.isMountedWritable();
+ }
+
+ public boolean isPrimary() {
+ return mVolumeInfo.isPrimary();
+ }
+
+ public boolean isVisible() {
+ return mVolumeInfo.isVisible();
+ }
+
+ public boolean isVisibleForUser(int userId) {
+ return mVolumeInfo.isVisibleForUser(userId);
+ }
+
+ public boolean isVisibleForWrite(int userId) {
+ return mVolumeInfo.isVisibleForWrite(userId);
+ }
+}
diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java
index b9c9b64cd2c6..342b864c6473 100644
--- a/services/core/java/com/android/server/storage/StorageSessionController.java
+++ b/services/core/java/com/android/server/storage/StorageSessionController.java
@@ -45,6 +45,7 @@ import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.server.storage.ImmutableVolumeInfo;
import java.util.Objects;
@@ -79,18 +80,18 @@ public final class StorageSessionController {
* @param vol for which the storage session has to be started
* @return userId for connection for this volume
*/
- public int getConnectionUserIdForVolume(VolumeInfo vol) {
+ public int getConnectionUserIdForVolume(ImmutableVolumeInfo vol) {
final Context volumeUserContext = mContext.createContextAsUser(
- UserHandle.of(vol.mountUserId), 0);
+ UserHandle.of(vol.getMountUserId()), 0);
boolean isMediaSharedWithParent = volumeUserContext.getSystemService(
UserManager.class).isMediaSharedWithParent();
- UserInfo userInfo = mUserManager.getUserInfo(vol.mountUserId);
+ UserInfo userInfo = mUserManager.getUserInfo(vol.getMountUserId());
if (userInfo != null && isMediaSharedWithParent) {
// Clones use the same connection as their parent
return userInfo.profileGroupId;
} else {
- return vol.mountUserId;
+ return vol.getMountUserId();
}
}
@@ -108,7 +109,7 @@ public final class StorageSessionController {
* @throws ExternalStorageServiceException if the session fails to start
* @throws IllegalStateException if a session has already been created for {@code vol}
*/
- public void onVolumeMount(ParcelFileDescriptor deviceFd, VolumeInfo vol)
+ public void onVolumeMount(ParcelFileDescriptor deviceFd, ImmutableVolumeInfo vol)
throws ExternalStorageServiceException {
if (!shouldHandle(vol)) {
return;
@@ -144,7 +145,8 @@ public final class StorageSessionController {
*
* @throws ExternalStorageServiceException if it fails to connect to ExternalStorageService
*/
- public void notifyVolumeStateChanged(VolumeInfo vol) throws ExternalStorageServiceException {
+ public void notifyVolumeStateChanged(ImmutableVolumeInfo vol)
+ throws ExternalStorageServiceException {
if (!shouldHandle(vol)) {
return;
}
@@ -214,7 +216,7 @@ public final class StorageSessionController {
* @return the connection that was removed or {@code null} if nothing was removed
*/
@Nullable
- public StorageUserConnection onVolumeRemove(VolumeInfo vol) {
+ public StorageUserConnection onVolumeRemove(ImmutableVolumeInfo vol) {
if (!shouldHandle(vol)) {
return null;
}
@@ -246,7 +248,7 @@ public final class StorageSessionController {
*
* Call {@link #onVolumeRemove} to remove the connection without waiting for exit
*/
- public void onVolumeUnmount(VolumeInfo vol) {
+ public void onVolumeUnmount(ImmutableVolumeInfo vol) {
String sessionId = vol.getId();
final long token = Binder.clearCallingIdentity();
try {
@@ -457,9 +459,9 @@ public final class StorageSessionController {
* Returns {@code true} if {@code vol} is an emulated or visible public volume,
* {@code false} otherwise
**/
- public static boolean isEmulatedOrPublic(VolumeInfo vol) {
- return vol.type == VolumeInfo.TYPE_EMULATED
- || (vol.type == VolumeInfo.TYPE_PUBLIC && vol.isVisible());
+ public static boolean isEmulatedOrPublic(ImmutableVolumeInfo vol) {
+ return vol.getType() == VolumeInfo.TYPE_EMULATED
+ || (vol.getType() == VolumeInfo.TYPE_PUBLIC && vol.isVisible());
}
/** Exception thrown when communication with the {@link ExternalStorageService} fails. */
@@ -477,11 +479,11 @@ public final class StorageSessionController {
}
}
- private static boolean isSupportedVolume(VolumeInfo vol) {
- return isEmulatedOrPublic(vol) || vol.type == VolumeInfo.TYPE_STUB;
+ private static boolean isSupportedVolume(ImmutableVolumeInfo vol) {
+ return isEmulatedOrPublic(vol) || vol.getType() == VolumeInfo.TYPE_STUB;
}
- private boolean shouldHandle(@Nullable VolumeInfo vol) {
+ private boolean shouldHandle(@Nullable ImmutableVolumeInfo vol) {
return !mIsResetting && (vol == null || isSupportedVolume(vol));
}
diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
new file mode 100644
index 000000000000..4124cfb4f092
--- /dev/null
+++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
@@ -0,0 +1,206 @@
+/*
+ * 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.storage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.storage.DiskInfo;
+import android.os.storage.StorageVolume;
+import android.os.storage.VolumeInfo;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.utils.Watchable;
+import com.android.server.utils.WatchableImpl;
+
+import java.io.File;
+
+/**
+ * A wrapper for {@link VolumeInfo} implementing the {@link Watchable} interface.
+ *
+ * The {@link VolumeInfo} class itself cannot safely implement Watchable, because it has several
+ * UnsupportedAppUsage annotations and public fields, which allow it to be modified without
+ * notifying watchers.
+ *
+ * @hide
+ */
+public class WatchedVolumeInfo extends WatchableImpl {
+ private final VolumeInfo mVolumeInfo;
+
+ private WatchedVolumeInfo(VolumeInfo volumeInfo) {
+ mVolumeInfo = volumeInfo;
+ }
+
+ public WatchedVolumeInfo(WatchedVolumeInfo watchedVolumeInfo) {
+ mVolumeInfo = new VolumeInfo(watchedVolumeInfo.mVolumeInfo);
+ }
+
+ public static WatchedVolumeInfo fromVolumeInfo(VolumeInfo info) {
+ return new WatchedVolumeInfo(info);
+ }
+
+ /**
+ * Returns a copy of the embedded VolumeInfo object, to be used by components
+ * that just need it for retrieving some state from it.
+ *
+ * @return A copy of the embedded VolumeInfo object
+ */
+
+ public WatchedVolumeInfo clone() {
+ return fromVolumeInfo(mVolumeInfo.clone());
+ }
+
+ public ImmutableVolumeInfo getImmutableVolumeInfo() {
+ return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo);
+ }
+
+ public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
+ return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ mVolumeInfo.dump(pw);
+ }
+
+ public DiskInfo getDisk() {
+ return mVolumeInfo.getDisk();
+ }
+
+ public String getDiskId() {
+ return mVolumeInfo.getDiskId();
+ }
+
+ public String getFsLabel() {
+ return mVolumeInfo.fsLabel;
+ }
+
+ public void setFsLabel(String fsLabel) {
+ mVolumeInfo.fsLabel = fsLabel;
+ dispatchChange(this);
+ }
+
+ public String getFsPath() {
+ return mVolumeInfo.path;
+ }
+
+ public void setFsPath(String path) {
+ mVolumeInfo.path = path;
+ dispatchChange(this);
+ }
+
+ public String getFsType() {
+ return mVolumeInfo.fsType;
+ }
+
+ public void setFsType(String fsType) {
+ mVolumeInfo.fsType = fsType;
+ dispatchChange(this);
+ }
+
+ public @Nullable String getFsUuid() {
+ return mVolumeInfo.fsUuid;
+ }
+
+ public void setFsUuid(String fsUuid) {
+ mVolumeInfo.fsUuid = fsUuid;
+ dispatchChange(this);
+ }
+
+ public @NonNull String getId() {
+ return mVolumeInfo.id;
+ }
+
+ public File getInternalPath() {
+ return mVolumeInfo.getInternalPath();
+ }
+
+ public void setInternalPath(String internalPath) {
+ mVolumeInfo.internalPath = internalPath;
+ dispatchChange(this);
+ }
+
+ public int getMountFlags() {
+ return mVolumeInfo.mountFlags;
+ }
+
+ public void setMountFlags(int mountFlags) {
+ mVolumeInfo.mountFlags = mountFlags;
+ dispatchChange(this);
+ }
+
+ public int getMountUserId() {
+ return mVolumeInfo.mountUserId;
+ }
+
+ public void setMountUserId(int mountUserId) {
+ mVolumeInfo.mountUserId = mountUserId;
+ dispatchChange(this);
+ }
+
+ public String getPartGuid() {
+ return mVolumeInfo.partGuid;
+ }
+
+ public File getPath() {
+ return mVolumeInfo.getPath();
+ }
+
+ public int getState() {
+ return mVolumeInfo.state;
+ }
+
+ public int getState(int state) {
+ return mVolumeInfo.state;
+ }
+
+ public void setState(int state) {
+ mVolumeInfo.state = state;
+ dispatchChange(this);
+ }
+
+ public int getType() {
+ return mVolumeInfo.type;
+ }
+
+ public VolumeInfo getVolumeInfo() {
+ return new VolumeInfo(mVolumeInfo);
+ }
+
+ public boolean isMountedReadable() {
+ return mVolumeInfo.isMountedReadable();
+ }
+
+ public boolean isMountedWritable() {
+ return mVolumeInfo.isMountedWritable();
+ }
+
+ public boolean isPrimary() {
+ return mVolumeInfo.isPrimary();
+ }
+
+ public boolean isVisible() {
+ return mVolumeInfo.isVisible();
+ }
+
+ public boolean isVisibleForUser(int userId) {
+ return mVolumeInfo.isVisibleForUser(userId);
+ }
+
+ public boolean isVisibleForWrite(int userId) {
+ return mVolumeInfo.isVisibleForWrite(userId);
+ }
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index ef63229f55e2..69b2b9b326ba 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -777,7 +777,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mWallpaperCompatibleDisplaysForTest.remove(displayId);
}
- private void updateFallbackConnection() {
+ private void updateFallbackConnection(int clientUid) {
if (mLastWallpaper == null || mFallbackWallpaper == null) return;
final WallpaperConnection systemConnection = mLastWallpaper.connection;
final WallpaperConnection fallbackConnection = mFallbackWallpaper.connection;
@@ -793,8 +793,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
if (isDeviceEligibleForDesktopExperienceWallpaper(mContext)) {
- mWallpaperDisplayHelper.forEachDisplayData(displayData -> {
- int displayId = displayData.mDisplayId;
+ Display[] displays = mWallpaperDisplayHelper.getDisplays();
+ for (int i = displays.length - 1; i >= 0; i--) {
+ int displayId = displays[i].getDisplayId();
+ if (!mWallpaperDisplayHelper.isUsableDisplay(displayId, clientUid)) {
+ continue;
+ }
// If the display is already connected to the desired wallpaper(s), either the
// same wallpaper for both lock and system, or different wallpapers for each,
// any existing fallback wallpaper connection will be removed.
@@ -802,11 +806,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
&& (lockConnection == null || lockConnection.containsDisplay(displayId))) {
DisplayConnector fallbackConnector =
mFallbackWallpaper.connection.mDisplayConnector.get(displayId);
- if (fallbackConnector != null && fallbackConnector.mEngine != null) {
- fallbackConnector.disconnectLocked(mFallbackWallpaper.connection);
+ if (fallbackConnector != null) {
+ if (fallbackConnector.mEngine != null) {
+ fallbackConnector.disconnectLocked(mFallbackWallpaper.connection);
+ }
mFallbackWallpaper.connection.mDisplayConnector.remove(displayId);
}
- return;
+ continue;
}
// Identify if the fallback wallpaper should be use for lock or system or both.
@@ -844,7 +850,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mFallbackWallpaper);
}
}
- });
+ }
} else if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) {
if (fallbackConnection.mDisplayConnector.size() != 0) {
fallbackConnection.forEachDisplayConnector(connector -> {
@@ -3787,7 +3793,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
wallpaper.connection = newConn;
newConn.mReply = reply;
updateCurrentWallpapers(wallpaper);
- updateFallbackConnection();
+ updateFallbackConnection(componentUid);
} catch (RemoteException e) {
String msg = "Remote exception for " + componentName + "\n" + e;
if (fromUser) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b17eef85f93d..d84016b3816e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -246,9 +246,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WIND
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
-import static com.android.server.wm.WindowManagerService.sEnableShellTransitions;
import static com.android.server.wm.WindowState.LEGACY_POLICY_VISIBILITY;
-import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
@@ -764,13 +762,6 @@ final class ActivityRecord extends WindowToken {
boolean mLastImeShown;
/**
- * When set to true, the IME insets will be frozen until the next app becomes IME input target.
- * @see InsetsPolicy#adjustVisibilityForIme
- * @see ImeInsetsSourceProvider#updateClientVisibility
- */
- boolean mImeInsetsFrozenUntilStartInput;
-
- /**
* A flag to determine if this AR is in the process of closing or entering PIP. This is needed
* to help AR know that the app is in the process of closing but hasn't yet started closing on
* the WM side.
@@ -1175,8 +1166,6 @@ final class ActivityRecord extends WindowToken {
pw.print(" launchMode="); pw.println(launchMode);
pw.print(prefix); pw.print("mActivityType=");
pw.println(activityTypeToString(getActivityType()));
- pw.print(prefix); pw.print("mImeInsetsFrozenUntilStartInput=");
- pw.println(mImeInsetsFrozenUntilStartInput);
if (requestedVrComponent != null) {
pw.print(prefix);
pw.print("requestedVrComponent=");
@@ -5239,7 +5228,8 @@ final class ActivityRecord extends WindowToken {
pendingOptions.getWidth(), pendingOptions.getHeight());
options = AnimationOptions.makeScaleUpAnimOptions(
pendingOptions.getStartX(), pendingOptions.getStartY(),
- pendingOptions.getWidth(), pendingOptions.getHeight());
+ pendingOptions.getWidth(), pendingOptions.getHeight(),
+ pendingOptions.getOverrideTaskTransition());
if (intent.getSourceBounds() == null) {
intent.setSourceBounds(new Rect(pendingOptions.getStartX(),
pendingOptions.getStartY(),
@@ -5771,19 +5761,16 @@ final class ActivityRecord extends WindowToken {
return;
}
- final int windowsCount = mChildren.size();
- // With Shell-Transition, the activity will running a transition when it is visible.
- // It won't be included when fromTransition is true means the call from finishTransition.
- final boolean runningAnimation = sEnableShellTransitions ? visible
- : isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION);
- for (int i = 0; i < windowsCount; i++) {
- mChildren.get(i).onAppVisibilityChanged(visible, runningAnimation);
+ if (!visible) {
+ for (int i = mChildren.size() - 1; i >= 0; --i) {
+ mChildren.get(i).onAppCommitInvisible();
+ }
}
setVisible(visible);
setVisibleRequested(visible);
ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "commitVisibility: %s: visible=%b"
- + " visibleRequested=%b, isInTransition=%b, runningAnimation=%b, caller=%s",
- this, isVisible(), mVisibleRequested, isInTransition(), runningAnimation,
+ + " visibleRequested=%b, inTransition=%b, caller=%s",
+ this, visible, mVisibleRequested, inTransition(),
Debug.getCallers(5));
if (visible) {
// If we are being set visible, and the starting window is not yet displayed,
@@ -5873,10 +5860,6 @@ final class ActivityRecord extends WindowToken {
}
final DisplayContent displayContent = getDisplayContent();
- if (!visible) {
- mImeInsetsFrozenUntilStartInput = true;
- }
-
if (!displayContent.mClosingApps.contains(this)
&& !displayContent.mOpeningApps.contains(this)
&& !fromTransition) {
@@ -6224,13 +6207,8 @@ final class ActivityRecord extends WindowToken {
return false;
}
- // Hide all activities on the presenting display so that malicious apps can't do tap
- // jacking (b/391466268).
- // For now, this should only be applied to external displays because presentations can only
- // be shown on them.
- // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
- // the presentation won't stop its controlling activity.
- if (enablePresentationForConnectedDisplays() && mDisplayContent.mIsPresenting) {
+ // A presentation stopps all activities behind on the same display.
+ if (mWmService.mPresentationController.shouldOccludeActivities(getDisplayId())) {
return false;
}
@@ -6952,14 +6930,6 @@ final class ActivityRecord extends WindowToken {
// closing activity having to wait until idle timeout to be stopped or destroyed if the
// next activity won't report idle (e.g. repeated view animation).
mTaskSupervisor.scheduleProcessStoppingAndFinishingActivitiesIfNeeded();
-
- // If the activity is visible, but no windows are eligible to start input, unfreeze
- // to avoid permanently frozen IME insets.
- if (mImeInsetsFrozenUntilStartInput && getWindow(
- win -> WindowManager.LayoutParams.mayUseInputMethod(win.mAttrs.flags))
- == null) {
- mImeInsetsFrozenUntilStartInput = false;
- }
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index ddb9f178cb8b..254127dee7a8 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4324,10 +4324,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
task = mRootWindowContainer.getDefaultTaskDisplayArea().getRootTask(
t -> t.isActivityTypeStandard());
}
- if (task != null && task.getTopMostActivity() != null
- && !task.getTopMostActivity().isState(FINISHING, DESTROYING, DESTROYED)) {
+ final ActivityRecord topActivity = task != null
+ ? task.getTopMostActivity()
+ : null;
+ if (topActivity != null && !topActivity.isState(FINISHING, DESTROYING, DESTROYED)) {
mWindowManager.mAtmService.mActivityClientController
- .onPictureInPictureUiStateChanged(task.getTopMostActivity(), pipState);
+ .onPictureInPictureUiStateChanged(topActivity, pipState);
}
}
}
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index 576e5d5d0cd2..439b503c0c57 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -506,6 +506,10 @@ class AppWarnings {
context = new ContextThemeWrapper(context, context.getThemeResId()) {
@Override
public void startActivity(Intent intent) {
+ // PageSizeMismatch dialog stays on top of the browser even after opening link
+ // set broadcast to close the dialog when link has been clicked.
+ sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
super.startActivity(intent);
}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 79bed3d8453d..e76a83453a9d 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -388,8 +388,7 @@ class BackNavigationController {
removedWindowContainer);
mBackAnimationInProgress = builder != null;
if (mBackAnimationInProgress) {
- if (removedWindowContainer.mTransitionController.inTransition()
- || mWindowManagerService.mSyncEngine.hasPendingSyncSets()) {
+ if (removedWindowContainer.mTransitionController.inTransition()) {
ProtoLog.w(WM_DEBUG_BACK_PREVIEW,
"Pending back animation due to another animation is running");
mPendingAnimationBuilder = builder;
@@ -817,6 +816,8 @@ class BackNavigationController {
if (openingTransition && !visible && mAnimationHandler.isTarget(ar, false /* open */)
&& ar.mTransitionController.isCollecting(ar)) {
final TransitionController controller = ar.mTransitionController;
+ final Transition transition = controller.getCollectingTransition();
+ final int switchType = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType;
boolean collectTask = false;
ActivityRecord changedActivity = null;
for (int i = mAnimationHandler.mOpenActivities.length - 1; i >= 0; --i) {
@@ -829,8 +830,16 @@ class BackNavigationController {
changedActivity = next;
}
}
- if (collectTask && mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].mSwitchType
- == AnimationHandler.TASK_SWITCH) {
+ if (Flags.unifyBackNavigationTransition()) {
+ for (int i = mAnimationHandler.mOpenAnimAdaptor.mAdaptors.length - 1; i >= 0; --i) {
+ collectAnimatableTarget(transition, switchType,
+ mAnimationHandler.mOpenAnimAdaptor.mAdaptors[i].mTarget,
+ false /* isTop */);
+ }
+ collectAnimatableTarget(transition, switchType,
+ mAnimationHandler.mCloseAdaptor.mTarget, true /* isTop */);
+ }
+ if (collectTask && switchType == AnimationHandler.TASK_SWITCH) {
final Task topTask = mAnimationHandler.mOpenAnimAdaptor.mAdaptors[0].getTopTask();
if (topTask != null) {
WindowContainer parent = mAnimationHandler.mOpenActivities[0].getParent();
@@ -848,6 +857,18 @@ class BackNavigationController {
}
}
+ private static void collectAnimatableTarget(Transition transition, int switchType,
+ WindowContainer animatingTarget, boolean isTop) {
+ if ((switchType == AnimationHandler.ACTIVITY_SWITCH
+ && (animatingTarget.asActivityRecord() != null
+ || animatingTarget.asTaskFragment() != null))
+ || (switchType == AnimationHandler.TASK_SWITCH
+ && animatingTarget.asTask() != null)) {
+ transition.collect(animatingTarget);
+ transition.setBackGestureAnimation(animatingTarget, isTop);
+ }
+ }
+
// For shell transition
/**
* Check whether the transition targets was animated by back gesture animation.
@@ -992,8 +1013,8 @@ class BackNavigationController {
return;
}
- if (mWindowManagerService.mRoot.mTransitionController.isCollecting()) {
- Slog.v(TAG, "Skip predictive back transition, another transition is collecting");
+ if (mWindowManagerService.mRoot.mTransitionController.inTransition()) {
+ Slog.v(TAG, "Skip predictive back transition, another transition is playing");
cancelPendingAnimation();
return;
}
@@ -1098,7 +1119,7 @@ class BackNavigationController {
}
final Transition prepareTransition = builder.prepareTransitionIfNeeded(
- openingActivities);
+ openingActivities, close, open);
final SurfaceControl.Transaction st = openingActivities[0].getSyncTransaction();
final SurfaceControl.Transaction ct = prepareTransition != null
? st : close.getPendingTransaction();
@@ -1790,7 +1811,8 @@ class BackNavigationController {
return wc == mCloseTarget || mCloseTarget.hasChild(wc) || wc.hasChild(mCloseTarget);
}
- private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities) {
+ private Transition prepareTransitionIfNeeded(ActivityRecord[] visibleOpenActivities,
+ WindowContainer promoteToClose, WindowContainer[] promoteToOpen) {
if (Flags.unifyBackNavigationTransition()) {
if (mCloseTarget.asWindowState() != null) {
return null;
@@ -1806,11 +1828,11 @@ class BackNavigationController {
final TransitionController tc = visibleOpenActivities[0].mTransitionController;
final Transition prepareOpen = tc.createTransition(
TRANSIT_PREPARE_BACK_NAVIGATION);
- tc.collect(mCloseTarget);
- prepareOpen.setBackGestureAnimation(mCloseTarget, true /* isTop */);
- for (int i = mOpenTargets.length - 1; i >= 0; --i) {
- tc.collect(mOpenTargets[i]);
- prepareOpen.setBackGestureAnimation(mOpenTargets[i], false /* isTop */);
+ tc.collect(promoteToClose);
+ prepareOpen.setBackGestureAnimation(promoteToClose, true /* isTop */);
+ for (int i = promoteToOpen.length - 1; i >= 0; --i) {
+ tc.collect(promoteToOpen[i]);
+ prepareOpen.setBackGestureAnimation(promoteToOpen[i], false /* isTop */);
}
if (!makeVisibles.isEmpty()) {
setLaunchBehind(visibleOpenActivities);
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index a1faa7573a0c..f35930700653 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -51,8 +51,13 @@ public final class DesktopModeHelper {
}
/**
- * Return {@code true} if the current device supports desktop mode.
+ * Return {@code true} if the current device can hosts desktop sessions on its internal display.
*/
+ @VisibleForTesting
+ static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
+ }
+
// TODO(b/337819319): use a companion object instead.
private static boolean isDesktopModeSupported(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
@@ -67,12 +72,12 @@ public final class DesktopModeHelper {
*/
private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) {
return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported(
- context) || isDeviceEligibleForDesktopMode(context));
+ context) || isInternalDisplayEligibleToHostDesktops(context));
}
@VisibleForTesting
- static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
- return !shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context) || (
+ static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) {
+ return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || (
Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported(
context));
}
@@ -81,12 +86,14 @@ public final class DesktopModeHelper {
* Return {@code true} if desktop mode can be entered on the current device.
*/
static boolean canEnterDesktopMode(@NonNull Context context) {
- return (isDesktopModeEnabled() && isDeviceEligibleForDesktopMode(context))
+ return (isInternalDisplayEligibleToHostDesktops(context)
+ && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()
+ && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions()))
|| isDesktopModeEnabledByDevOption(context);
}
/** Returns {@code true} if desktop experience wallpaper is supported on this device. */
public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) {
- return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context);
+ return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 5cb39d44586f..c87087f84399 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -547,9 +547,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
// TODO(multi-display): remove some of the usages.
boolean isDefaultDisplay;
- /** Indicates whether any presentation is shown on this display. */
- boolean mIsPresenting;
-
/** Save allocating when calculating rects */
private final Rect mTmpRect = new Rect();
private final Region mTmpRegion = new Region();
@@ -4661,35 +4658,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
}
}
- /**
- * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to
- * judge whether or not to notify the IME insets provider to dispatch this reported IME client
- * visibility state to the app clients when needed.
- */
- boolean onImeInsetsClientVisibilityUpdate() {
- boolean[] changed = new boolean[1];
-
- // Unlike the IME layering target or the control target can be updated during the layout
- // change, the IME input target requires to be changed after gaining the input focus.
- // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze
- // when activities going to be visible until the input target changed, or the
- // activity was the current input target that has to unfreeze after updating the IME
- // client visibility.
- final ActivityRecord inputTargetActivity =
- mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null;
- final boolean targetChanged = mImeInputTarget != mLastImeInputTarget;
- if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested()
- && inputTargetActivity.mImeInsetsFrozenUntilStartInput) {
- forAllActivities(r -> {
- if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) {
- r.mImeInsetsFrozenUntilStartInput = false;
- changed[0] = true;
- }
- });
- }
- return changed[0];
- }
-
void updateImeControlTarget() {
updateImeControlTarget(false /* forceUpdateImeParent */);
}
@@ -6635,6 +6603,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
.getKeyguardController().isKeyguardLocked(mDisplayId);
}
+ boolean isKeyguardLockedOrAodShowing() {
+ return isKeyguardLocked() || isAodShowing();
+ }
+
+ /**
+ * @return whether aod is showing for this display
+ */
+ boolean isAodShowing() {
+ final boolean isAodShowing = mRootWindowContainer.mTaskSupervisor
+ .getKeyguardController().isAodShowing(mDisplayId);
+ if (mDisplayId == DEFAULT_DISPLAY && isAodShowing) {
+ return !isKeyguardGoingAway();
+ }
+ return isAodShowing;
+ }
+
/**
* @return whether keyguard is going away on this display
*/
@@ -7125,14 +7109,19 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
}
/**
+ * @return an integer as the changed requested visible insets types.
* @see #getRequestedVisibleTypes()
*/
- void updateRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
- int newRequestedVisibleTypes =
+ @InsetsType int updateRequestedVisibleTypes(
+ @InsetsType int visibleTypes, @InsetsType int mask) {
+ final int newRequestedVisibleTypes =
(mRequestedVisibleTypes & ~mask) | (visibleTypes & mask);
if (mRequestedVisibleTypes != newRequestedVisibleTypes) {
+ final int changedTypes = mRequestedVisibleTypes ^ newRequestedVisibleTypes;
mRequestedVisibleTypes = newRequestedVisibleTypes;
+ return changedTypes;
}
+ return 0;
}
}
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index 907d0dc2e183..7b6fc9e5694d 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -34,6 +34,7 @@ import android.util.proto.ProtoOutputStream;
import android.view.InputApplicationHandle;
import android.view.InputChannel;
import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
import android.window.InputTransferToken;
import com.android.internal.protolog.ProtoLog;
@@ -260,7 +261,7 @@ class EmbeddedWindowController {
// The EmbeddedWindow can only request the IME. All other insets types are requested by
// the host window.
- private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0;
+ private @InsetsType int mRequestedVisibleTypes = 0;
/** Whether the gesture is transferred to embedded window. */
boolean mGestureToEmbedded = false;
@@ -354,24 +355,28 @@ class EmbeddedWindowController {
}
@Override
- public boolean isRequestedVisible(@WindowInsets.Type.InsetsType int types) {
+ public boolean isRequestedVisible(@InsetsType int types) {
return (mRequestedVisibleTypes & types) != 0;
}
@Override
- public @WindowInsets.Type.InsetsType int getRequestedVisibleTypes() {
+ public @InsetsType int getRequestedVisibleTypes() {
return mRequestedVisibleTypes;
}
/**
* Only the IME can be requested from the EmbeddedWindow.
- * @param requestedVisibleTypes other types than {@link WindowInsets.Type.IME} are
+ * @param requestedVisibleTypes other types than {@link WindowInsets.Type#ime()} are
* not sent to system server via WindowlessWindowManager.
+ * @return an integer as the changed requested visible insets types.
*/
- void setRequestedVisibleTypes(@WindowInsets.Type.InsetsType int requestedVisibleTypes) {
+ @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
if (mRequestedVisibleTypes != requestedVisibleTypes) {
+ final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes;
mRequestedVisibleTypes = requestedVisibleTypes;
+ return changedTypes;
}
+ return 0;
}
@Override
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index cf16204f93a1..f52446ff494c 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -282,7 +282,14 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
// TODO(b/353463205) investigate if we should fail the statsToken, or if it's only
// temporary null.
if (target != null) {
- invokeOnImeRequestedChangedListener(target.getWindow(), statsToken);
+ // If insets target is not available (e.g. RemoteInsetsControlTarget), use current
+ // IME input target to update IME request state. For example, switch from a task
+ // with showing IME to a split-screen task without showing IME.
+ InsetsTarget insetsTarget = target.getWindow();
+ if (insetsTarget == null && mServerVisible) {
+ insetsTarget = mDisplayContent.getImeInputTarget();
+ }
+ invokeOnImeRequestedChangedListener(insetsTarget, statsToken);
}
}
}
@@ -314,7 +321,6 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
reportImeDrawnForOrganizerIfNeeded((InsetsControlTarget) caller);
}
}
- changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate();
if (Flags.refactorInsetsController()) {
if (changed) {
ImeTracker.forLogging().onProgress(statsToken,
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 4bcba13448e9..b4d55a160631 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -387,22 +387,6 @@ class InsetsPolicy {
state.addSource(navSource);
}
return state;
- } else if (w.mActivityRecord != null && w.mActivityRecord.mImeInsetsFrozenUntilStartInput) {
- // During switching tasks with gestural navigation, before the next IME input target
- // starts the input, we should adjust and freeze the last IME visibility of the window
- // in case delivering obsoleted IME insets state during transitioning.
- final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
-
- if (originalImeSource != null) {
- final boolean imeVisibility = w.isRequestedVisible(Type.ime());
- final InsetsState state = copyState
- ? new InsetsState(originalState)
- : originalState;
- final InsetsSource imeSource = new InsetsSource(originalImeSource);
- imeSource.setVisible(imeVisibility);
- state.addSource(imeSource);
- return state;
- }
} else if (w.mImeInsetsConsumed) {
// Set the IME source (if there is one) to be invisible if it has been consumed.
final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
@@ -453,9 +437,9 @@ class InsetsPolicy {
return originalState;
}
- void onRequestedVisibleTypesChanged(InsetsTarget caller,
+ void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes,
@Nullable ImeTracker.Token statsToken) {
- mStateController.onRequestedVisibleTypesChanged(caller, statsToken);
+ mStateController.onRequestedVisibleTypesChanged(caller, changedTypes, statsToken);
checkAbortTransient(caller);
updateBarControlTarget(mFocusedWin);
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 9202cf2d5792..164abab992d8 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -219,14 +219,20 @@ class InsetsStateController {
}
}
- void onRequestedVisibleTypesChanged(InsetsTarget caller,
+ void onRequestedVisibleTypesChanged(InsetsTarget caller, @InsetsType int changedTypes,
@Nullable ImeTracker.Token statsToken) {
boolean changed = false;
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
- final boolean isImeProvider = provider.getSource().getType() == WindowInsets.Type.ime();
- changed |= provider.updateClientVisibility(caller,
- isImeProvider ? statsToken : null);
+ final @InsetsType int type = provider.getSource().getType();
+ if ((type & changedTypes) != 0) {
+ final boolean isImeProvider = type == WindowInsets.Type.ime();
+ changed |= provider.updateClientVisibility(
+ caller, isImeProvider ? statsToken : null)
+ // Fake control target cannot change the client visibility, but it should
+ // change the insets with its newly requested visibility.
+ || (caller == provider.getFakeControlTarget());
+ }
}
if (changed) {
notifyInsetsChanged();
@@ -435,7 +441,8 @@ class InsetsStateController {
for (int i = newControlTargets.size() - 1; i >= 0; i--) {
// TODO(b/353463205) the statsToken shouldn't be null as it is used later in the
// IME provider. Check if we have to create a new request here
- onRequestedVisibleTypesChanged(newControlTargets.valueAt(i), null /* statsToken */);
+ onRequestedVisibleTypesChanged(newControlTargets.valueAt(i),
+ WindowInsets.Type.all(), null /* statsToken */);
}
newControlTargets.clear();
if (!android.view.inputmethod.Flags.refactorInsetsController()) {
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 6091b8334438..dd2f49e171a8 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -18,6 +18,7 @@ package com.android.server.wm;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
@@ -216,6 +217,9 @@ class KeyguardController {
} else if (keyguardShowing && !state.mKeyguardShowing) {
transition.addFlag(TRANSIT_FLAG_KEYGUARD_APPEARING);
}
+ if (mWindowManager.mFlags.mAodTransition && aodShowing && !state.mAodShowing) {
+ transition.addFlag(TRANSIT_FLAG_AOD_APPEARING);
+ }
}
}
// Update the task snapshot if the screen will not be turned off. To make sure that the
@@ -238,19 +242,27 @@ class KeyguardController {
state.mAodShowing = aodShowing;
state.writeEventLog("setKeyguardShown");
- if (keyguardChanged) {
- // Irrelevant to AOD.
- state.mKeyguardGoingAway = false;
- if (keyguardShowing) {
- state.mDismissalRequested = false;
+ if (keyguardChanged || aodChanged) {
+ if (keyguardChanged) {
+ // Irrelevant to AOD.
+ state.mKeyguardGoingAway = false;
+ if (keyguardShowing) {
+ state.mDismissalRequested = false;
+ }
}
if (goingAwayRemoved
- || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) {
+ || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))
+ || (mWindowManager.mFlags.mAodTransition && aodShowing)) {
// Keyguard decided to show or stopped going away. Send a transition to animate back
// to the locked state before holding the sleep token again
if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) {
dc.requestTransitionAndLegacyPrepare(
TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING);
+ if (mWindowManager.mFlags.mAodTransition && aodShowing
+ && dc.mTransitionController.isCollecting()) {
+ dc.mTransitionController.getCollectingTransition().addFlag(
+ TRANSIT_FLAG_AOD_APPEARING);
+ }
}
dc.mWallpaperController.adjustWallpaperWindows();
dc.executeAppTransition();
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java
new file mode 100644
index 000000000000..69463433827f
--- /dev/null
+++ b/services/core/java/com/android/server/wm/PresentationController.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
+
+import android.annotation.NonNull;
+import android.util.IntArray;
+
+import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.WmProtoLogGroups;
+
+/**
+ * Manages presentation windows.
+ */
+class PresentationController {
+
+ // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
+ private final IntArray mPresentingDisplayIds = new IntArray();
+
+ PresentationController() {}
+
+ private boolean isPresenting(int displayId) {
+ return mPresentingDisplayIds.contains(displayId);
+ }
+
+ boolean shouldOccludeActivities(int displayId) {
+ // All activities on the presenting display must be hidden so that malicious apps can't do
+ // tap jacking (b/391466268).
+ // For now, this should only be applied to external displays because presentations can only
+ // be shown on them.
+ // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
+ // the presentation won't stop its controlling activity.
+ return enablePresentationForConnectedDisplays() && isPresenting(displayId);
+ }
+
+ void onPresentationAdded(@NonNull WindowState win) {
+ final int displayId = win.getDisplayId();
+ if (isPresenting(displayId)) {
+ return;
+ }
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
+ win.getDisplayId(), win);
+ mPresentingDisplayIds.add(win.getDisplayId());
+ if (enablePresentationForConnectedDisplays()) {
+ // A presentation hides all activities behind on the same display.
+ win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
+ /*notifyClients=*/ true);
+ }
+ win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
+ }
+
+ void onPresentationRemoved(@NonNull WindowState win) {
+ final int displayId = win.getDisplayId();
+ if (!isPresenting(displayId)) {
+ return;
+ }
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
+ "Presentation removed from display %d: %s", win.getDisplayId(), win);
+ // TODO(b/393945496): Make sure that there's one presentation at most per display.
+ final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
+ if (displayIdIndex != -1) {
+ mPresentingDisplayIds.remove(displayIdIndex);
+ }
+ if (enablePresentationForConnectedDisplays()) {
+ // A presentation hides all activities behind on the same display.
+ win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
+ /*notifyClients=*/ true);
+ }
+ win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
+ }
+}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 1ad5988e3c2e..8d198b26f396 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -704,9 +704,10 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
ImeTracker.forLogging().onProgress(imeStatsToken,
ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES);
}
- win.setRequestedVisibleTypes(requestedVisibleTypes);
+ final @InsetsType int changedTypes =
+ win.setRequestedVisibleTypes(requestedVisibleTypes);
win.getDisplayContent().getInsetsPolicy().onRequestedVisibleTypesChanged(win,
- imeStatsToken);
+ changedTypes, imeStatsToken);
final Task task = win.getTask();
if (task != null) {
task.dispatchTaskInfoChangedIfNeeded(/* forced= */ true);
@@ -723,10 +724,11 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
// TODO(b/353463205) Use different phase here
ImeTracker.forLogging().onProgress(imeStatsToken,
ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES);
- embeddedWindow.setRequestedVisibleTypes(
+ final @InsetsType int changedTypes = embeddedWindow.setRequestedVisibleTypes(
requestedVisibleTypes & WindowInsets.Type.ime());
embeddedWindow.getDisplayContent().getInsetsPolicy()
- .onRequestedVisibleTypesChanged(embeddedWindow, imeStatsToken);
+ .onRequestedVisibleTypesChanged(
+ embeddedWindow, changedTypes, imeStatsToken);
} else {
ImeTracker.forLogging().onFailed(imeStatsToken,
ImeTracker.PHASE_WM_UPDATE_REQUESTED_VISIBLE_TYPES);
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index cc14383fc9f9..ae3a015a690d 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -460,7 +460,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
// If the previous front-most task is moved to the back, then notify of the new
// front-most task.
- final ActivityRecord topMost = getTopMostActivity();
+ final ActivityRecord topMost = getTopNonFinishingActivity();
if (topMost != null) {
mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront(
topMost.getTask().getTaskInfo());
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 64105f634f84..324852d1a410 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1224,6 +1224,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */);
}
+ @Override
ActivityRecord getTopNonFinishingActivity() {
return getTopNonFinishingActivity(
true /* includeOverlays */, true /* includeLaunchedFromBubble */);
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 5217a759c6ae..fe653e454d6c 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -36,6 +36,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -973,6 +974,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
return false;
}
+ boolean isInAodAppearTransition() {
+ return (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0;
+ }
+
/**
* Specifies configuration change explicitly for the window container, so it can be chosen as
* transition target. This is usually used with transition mode
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index ff9e5a2aad99..25b513d85384 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -525,6 +525,19 @@ class TransitionController {
return false;
}
+ boolean isInAodAppearTransition() {
+ if (mCollectingTransition != null && mCollectingTransition.isInAodAppearTransition()) {
+ return true;
+ }
+ for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
+ if (mWaitingTransitions.get(i).isInAodAppearTransition()) return true;
+ }
+ for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+ if (mPlayingTransitions.get(i).isInAodAppearTransition()) return true;
+ }
+ return false;
+ }
+
/**
* @return A pair of the transition and restore-behind target for the given {@param container}.
* @param container An ancestor of a transient-launch activity
@@ -542,6 +555,23 @@ class TransitionController {
return null;
}
+ /**
+ * @return The playing transition that is transiently-hiding the given {@param container}, or
+ * null if there isn't one
+ * @param container A participant of a transient-hide transition
+ */
+ @Nullable
+ Transition getTransientHideTransitionForContainer(
+ @NonNull WindowContainer container) {
+ for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+ final Transition transition = mPlayingTransitions.get(i);
+ if (transition.isInTransientHide(container)) {
+ return transition;
+ }
+ }
+ return null;
+ }
+
/** Returns {@code true} if the display contains a transient-launch transition. */
boolean hasTransientLaunch(@NonNull DisplayContent dc) {
if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch()
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index c1ef208d1d4d..70948e1264c4 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -166,6 +166,14 @@ class WallpaperController {
mFindResults.setWallpaperTarget(w);
return false;
}
+ } else if (mService.mFlags.mAodTransition
+ && mDisplayContent.isKeyguardLockedOrAodShowing()) {
+ if (mService.mPolicy.isKeyguardHostWindow(w.mAttrs)
+ && w.mTransitionController.isInAodAppearTransition()) {
+ if (DEBUG_WALLPAPER) Slog.v(TAG, "Found aod transition wallpaper target: " + w);
+ mFindResults.setWallpaperTarget(w);
+ return true;
+ }
}
final boolean animationWallpaper = animatingContainer != null
@@ -684,7 +692,8 @@ class WallpaperController {
private WallpaperWindowToken getTokenForTarget(WindowState target) {
if (target == null) return null;
WindowState window = mFindResults.getTopWallpaper(
- target.canShowWhenLocked() && mService.isKeyguardLocked());
+ (target.canShowWhenLocked() && mService.isKeyguardLocked())
+ || (mService.mFlags.mAodTransition && mDisplayContent.isAodShowing()));
return window == null ? null : window.mToken.asWallpaperToken();
}
@@ -727,7 +736,9 @@ class WallpaperController {
if (mFindResults.wallpaperTarget == null && mFindResults.useTopWallpaperAsTarget) {
mFindResults.setWallpaperTarget(
- mFindResults.getTopWallpaper(mDisplayContent.isKeyguardLocked()));
+ mFindResults.getTopWallpaper(mService.mFlags.mAodTransition
+ ? mDisplayContent.isKeyguardLockedOrAodShowing()
+ : mDisplayContent.isKeyguardLocked()));
}
}
@@ -899,11 +910,17 @@ class WallpaperController {
if (mDisplayContent.mWmService.mFlags.mEnsureWallpaperInTransitions) {
visibleRequested = mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested();
}
- updateWallpaperTokens(visibleRequested, mDisplayContent.isKeyguardLocked());
+ updateWallpaperTokens(visibleRequested,
+ mService.mFlags.mAodTransition
+ ? mDisplayContent.isKeyguardLockedOrAodShowing()
+ : mDisplayContent.isKeyguardLocked());
ProtoLog.v(WM_DEBUG_WALLPAPER,
"Wallpaper at display %d - visibility: %b, keyguardLocked: %b",
- mDisplayContent.getDisplayId(), visible, mDisplayContent.isKeyguardLocked());
+ mDisplayContent.getDisplayId(), visible,
+ mService.mFlags.mAodTransition
+ ? mDisplayContent.isKeyguardLockedOrAodShowing()
+ : mDisplayContent.isKeyguardLocked());
if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) {
mLastFrozen = mFindResults.isWallpaperTargetForLetterbox;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 225951dbd345..55c2668f62d0 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2079,6 +2079,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return getActivity(alwaysTruePredicate(), true /* traverseTopToBottom */);
}
+ ActivityRecord getTopNonFinishingActivity() {
+ return getActivity(r -> !r.finishing, true /* traverseTopToBottom */);
+ }
+
ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) {
// Break down into 4 calls to avoid object creation due to capturing input params.
if (includeFinishing) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 04ddb3cacf27..bb669915e366 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -157,7 +157,6 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY;
import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER;
import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID;
import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
-import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import static com.android.window.flags.Flags.multiCrop;
import static com.android.window.flags.Flags.setScPropertiesInClient;
@@ -503,6 +502,8 @@ public class WindowManagerService extends IWindowManager.Stub
final StartingSurfaceController mStartingSurfaceController;
+ final PresentationController mPresentationController;
+
private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
@Override
public void onVrStateChanged(boolean enabled) {
@@ -732,8 +733,14 @@ public class WindowManagerService extends IWindowManager.Stub
new WallpaperVisibilityListeners();
IDisplayChangeWindowController mDisplayChangeController = null;
- private final DeathRecipient mDisplayChangeControllerDeath =
- () -> mDisplayChangeController = null;
+ private final DeathRecipient mDisplayChangeControllerDeath = new DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mGlobalLock) {
+ mDisplayChangeController = null;
+ }
+ }
+ };
final DisplayWindowListenerController mDisplayNotificationController;
final TaskSystemBarsListenerController mTaskSystemBarsListenerController;
@@ -1427,6 +1434,7 @@ public class WindowManagerService extends IWindowManager.Stub
setGlobalShadowSettings();
mAnrController = new AnrController(this);
mStartingSurfaceController = new StartingSurfaceController(this);
+ mPresentationController = new PresentationController();
mBlurController = new BlurController(mContext, mPowerManager);
mTaskFpsCallbackController = new TaskFpsCallbackController(mContext);
@@ -1931,16 +1939,8 @@ public class WindowManagerService extends IWindowManager.Stub
}
outSizeCompatScale[0] = win.getCompatScaleForClient();
- if (res >= ADD_OKAY
- && (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION)) {
- displayContent.mIsPresenting = true;
- if (enablePresentationForConnectedDisplays()) {
- // A presentation hides all activities behind on the same display.
- displayContent.ensureActivitiesVisible(/*starting=*/ null,
- /*notifyClients=*/ true);
- }
- mDisplayManagerInternal.onPresentation(displayContent.getDisplay().getDisplayId(),
- /*isShown=*/ true);
+ if (res >= ADD_OKAY && win.isPresentation()) {
+ mPresentationController.onPresentationAdded(win);
}
}
@@ -4726,11 +4726,13 @@ public class WindowManagerService extends IWindowManager.Stub
}
ImeTracker.forLogging().onProgress(statsToken,
ImeTracker.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES);
- dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(visibleTypes, mask);
+ final @InsetsType int changedTypes =
+ dc.mRemoteInsetsControlTarget.updateRequestedVisibleTypes(
+ visibleTypes, mask);
// TODO(b/353463205) the statsToken shouldn't be null as it is used later in the
// IME provider. Check if we have to create a new request here, if null.
dc.getInsetsStateController().onRequestedVisibleTypesChanged(
- dc.mRemoteInsetsControlTarget, statsToken);
+ dc.mRemoteInsetsControlTarget, changedTypes, statsToken);
}
} finally {
Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index a11f4b1f3fc3..924b9de5a562 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -702,9 +702,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
if ((entry.getValue().getChangeMask()
& WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) {
- // Disable entering pip (eg. when recents pretends to finish itself)
- if (chain.mTransition != null) {
- chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */);
+ if (com.android.wm.shell.Flags.enableRecentsBookendTransition()) {
+ // If we are using a bookend transition, then the transition that we need
+ // to disable pip on finish is the original transient transition, not the
+ // bookend transition
+ final Transition transientHideTransition =
+ mTransitionController.getTransientHideTransitionForContainer(wc);
+ if (transientHideTransition != null) {
+ transientHideTransition.setCanPipOnFinish(false);
+ } else {
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+ "Set do-not-pip: no task");
+ }
+ } else {
+ // Disable entering pip (eg. when recents pretends to finish itself)
+ if (chain.mTransition != null) {
+ chain.mTransition.setCanPipOnFinish(false /* canPipOnFinish */);
+ }
}
}
// A bit hacky, but we need to detect "remove PiP" so that we can "wrap" the
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 84d8f840d849..589724182980 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -182,7 +182,6 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE
import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY;
import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER;
import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES;
-import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import static com.android.window.flags.Flags.surfaceTrustedOverlay;
import android.annotation.CallSuper;
@@ -822,17 +821,23 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
/**
+ * @return an integer as the changed requested visible insets types.
* @see #getRequestedVisibleTypes()
*/
- void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
+ @InsetsType int setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes) {
if (mRequestedVisibleTypes != requestedVisibleTypes) {
+ final int changedTypes = mRequestedVisibleTypes ^ requestedVisibleTypes;
mRequestedVisibleTypes = requestedVisibleTypes;
+ return changedTypes;
}
+ return 0;
}
@VisibleForTesting
- void setRequestedVisibleTypes(@InsetsType int requestedVisibleTypes, @InsetsType int mask) {
- setRequestedVisibleTypes(mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
+ @InsetsType int setRequestedVisibleTypes(
+ @InsetsType int requestedVisibleTypes, @InsetsType int mask) {
+ return setRequestedVisibleTypes(
+ mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
}
/**
@@ -2069,38 +2074,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
super.onMovedByResize();
}
- void onAppVisibilityChanged(boolean visible, boolean runningAppAnimation) {
+ void onAppCommitInvisible() {
for (int i = mChildren.size() - 1; i >= 0; --i) {
- mChildren.get(i).onAppVisibilityChanged(visible, runningAppAnimation);
+ mChildren.get(i).onAppCommitInvisible();
}
-
- final boolean isVisibleNow = isVisibleNow();
- if (mAttrs.type == TYPE_APPLICATION_STARTING) {
- // Starting window that's exiting will be removed when the animation finishes.
- // Mark all relevant flags for that onExitAnimationDone will proceed all the way
- // to actually remove it.
- if (!visible && isVisibleNow && mActivityRecord.isAnimating(PARENTS | TRANSITION)) {
- ProtoLog.d(WM_DEBUG_ANIM,
- "Set animatingExit: reason=onAppVisibilityChanged win=%s", this);
- mAnimatingExit = true;
- mRemoveOnExit = true;
- mWindowRemovalAllowed = true;
- }
- } else if (visible != isVisibleNow) {
- // Run exit animation if:
- // 1. App visibility and WS visibility are different
- // 2. App is not running an animation
- // 3. WS is currently visible
- if (!runningAppAnimation && isVisibleNow) {
- final AccessibilityController accessibilityController =
- mWmService.mAccessibilityController;
- final int winTransit = TRANSIT_EXIT;
- mWinAnimator.applyAnimationLocked(winTransit, false /* isEntrance */);
- if (accessibilityController.hasCallbacks()) {
- accessibilityController.onWindowTransition(this, winTransit);
- }
- }
- setDisplayLayoutNeeded();
+ if (mAttrs.type != TYPE_APPLICATION_STARTING
+ && mWmService.mAccessibilityController.hasCallbacks()
+ // It is a change only if App visibility and WS visibility are different.
+ && isVisible()) {
+ mWmService.mAccessibilityController.onWindowTransition(this, TRANSIT_EXIT);
}
}
@@ -2317,15 +2299,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
final int type = mAttrs.type;
- if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) {
- // TODO(b/393945496): Make sure that there's one presentation at most per display.
- dc.mIsPresenting = false;
- if (enablePresentationForConnectedDisplays()) {
- // A presentation hides all activities behind on the same display.
- dc.ensureActivitiesVisible(/*starting=*/ null, /*notifyClients=*/ true);
- }
- mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(),
- /*isShown=*/ false);
+ if (isPresentation()) {
+ mWmService.mPresentationController.onPresentationRemoved(this);
}
// Check if window provides non decor insets before clearing its provided insets.
final boolean windowProvidesDisplayDecorInsets = providesDisplayDecorInsets();
@@ -3354,6 +3329,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
}
+ boolean isPresentation() {
+ return mAttrs.type == TYPE_PRESENTATION || mAttrs.type == TYPE_PRIVATE_PRESENTATION;
+ }
+
private boolean isOnVirtualDisplay() {
return getDisplayContent().mDisplay.getType() == Display.TYPE_VIRTUAL;
}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
index 66aaa562b873..a01df8bf108d 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
@@ -37,6 +37,7 @@ import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
import android.content.pm.SigningDetails;
import android.content.pm.UserInfo;
+import android.app.PropertyInvalidatedCache;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
@@ -50,6 +51,8 @@ import android.util.SparseArray;
import androidx.annotation.NonNull;
+import android.app.ApplicationPackageManager;
+import android.content.pm.PackageManager;
import com.android.internal.pm.parsing.pkg.PackageImpl;
import com.android.internal.pm.parsing.pkg.ParsedPackage;
import com.android.internal.pm.pkg.component.ParsedActivity;
@@ -64,8 +67,10 @@ import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl;
import com.android.internal.pm.pkg.parsing.ParsingPackage;
import com.android.server.om.OverlayReferenceMapper;
import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.utils.Watchable;
import com.android.server.utils.WatchableTester;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -244,6 +249,55 @@ public class AppsFilterImplTest {
(Answer<Boolean>) invocation ->
((AndroidPackage) invocation.getArgument(SYSTEM_USER)).getTargetSdkVersion()
>= Build.VERSION_CODES.R);
+ PropertyInvalidatedCache.setTestMode(true);
+ PackageManager.sApplicationInfoCache.testPropertyName();
+ ApplicationPackageManager.sGetPackagesForUidCache.testPropertyName();
+ }
+
+ @After
+ public void tearDown() {
+ PropertyInvalidatedCache.setTestMode(false);
+ }
+
+ /**
+ * A class to make it easier to verify that PM caches are properly invalidated by
+ * AppsFilterImpl operations. This extends WatchableTester to test the cache nonces along
+ * with change reporting.
+ */
+ private static class NonceTester extends WatchableTester {
+ // The nonces from caches under consideration. The no-parameter constructor fetches the
+ // values from the cacches.
+ private static record Nonces(long applicationInfo, long packageInfo) {
+ Nonces() {
+ this(ApplicationPackageManager.sGetPackagesForUidCache.getNonce(),
+ PackageManager.sApplicationInfoCache.getNonce());
+ }
+ }
+
+ // Track the latest cache nonces.
+ private Nonces mNonces;
+
+ NonceTester(Watchable w, String k) {
+ super(w, k);
+ mNonces = new Nonces();
+ }
+
+ @Override
+ public void verifyChangeReported(String msg) {
+ super.verifyChangeReported(msg);
+ Nonces update = new Nonces();
+ assertTrue(msg, update.applicationInfo != mNonces.applicationInfo);
+ assertTrue(msg, update.packageInfo != mNonces.packageInfo);
+ mNonces = update;
+ }
+
+ @Override
+ public void verifyNoChangeReported(String msg) {
+ super.verifyNoChangeReported(msg);
+ Nonces update = new Nonces();
+ assertTrue(msg, update.applicationInfo == mNonces.applicationInfo);
+ assertTrue(msg, update.packageInfo == mNonces.packageInfo);
+ }
}
@Test
@@ -1167,7 +1221,7 @@ public class AppsFilterImplTest {
final AppsFilterImpl appsFilter =
new AppsFilterImpl(mFeatureConfigMock, new String[]{}, /* systemAppsQueryable */
false, /* overlayProvider */ null, mMockHandler);
- final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
+ final WatchableTester watcher = new NonceTester(appsFilter, "onChange");
watcher.register();
simulateAddBasicAndroid(appsFilter);
watcher.verifyChangeReported("addBasicAndroid");
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 5393e20889c0..b9cea0c72306 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -388,6 +388,34 @@ public class LocalDisplayAdapterTest {
PORT_C, false);
}
+ /**
+ * Confirm that display is marked as trusted, has own focus, disables steal top focus when it
+ * is listed in com.android.internal.R.array.config_localNotStealTopFocusDisplayPorts.
+ */
+ @Test
+ public void testStealTopFocusDisabledDisplay() throws Exception {
+ setUpDisplay(new FakeDisplay(PORT_A));
+ setUpDisplay(new FakeDisplay(PORT_B));
+ setUpDisplay(new FakeDisplay(PORT_C));
+ updateAvailableDisplays();
+
+ doReturn(new int[]{ PORT_B }).when(mMockedResources).getIntArray(
+ com.android.internal.R.array.config_localNotStealTopFocusDisplayPorts);
+ mAdapter.registerLocked();
+
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+ // This should not have the flags
+ assertNotStealTopFocusFlag(mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(),
+ PORT_A, false);
+ // This should have the flags
+ assertNotStealTopFocusFlag(mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(),
+ PORT_B, true);
+ // This should not have the flags
+ assertNotStealTopFocusFlag(mListener.addedDisplays.get(2).getDisplayDeviceInfoLocked(),
+ PORT_C, false);
+ }
+
@Test
public void testSupportedDisplayModesGetOverriddenWhenDisplayIsUpdated()
throws InterruptedException {
@@ -452,6 +480,42 @@ public class LocalDisplayAdapterTest {
}
/**
+ * Confirm that all local displays are not trusted, do not have their own focus, and do not
+ * steal top focus when config_localNotStealTopFocusDisplayPorts is empty:
+ */
+ @Test
+ public void testDisplayFlagsForNoConfigLocalNotStealTopFocusDisplayPorts() throws Exception {
+ setUpDisplay(new FakeDisplay(PORT_A));
+ setUpDisplay(new FakeDisplay(PORT_C));
+ updateAvailableDisplays();
+
+ // config_localNotStealTopFocusDisplayPorts is null
+ mAdapter.registerLocked();
+
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+ // This should not have the flags
+ assertNotStealTopFocusFlag(mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(),
+ PORT_A, false);
+ // This should not have the flags
+ assertNotStealTopFocusFlag(mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(),
+ PORT_C, false);
+ }
+
+ private static void assertNotStealTopFocusFlag(
+ DisplayDeviceInfo info, int expectedPort, boolean shouldHaveFlags) {
+ final DisplayAddress.Physical address = (DisplayAddress.Physical) info.address;
+ assertNotNull(address);
+ assertEquals(expectedPort, address.getPort());
+ assertEquals(DISPLAY_MODEL, address.getModel());
+ assertEquals(shouldHaveFlags,
+ (info.flags & DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED) != 0);
+ assertEquals(shouldHaveFlags, (info.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0);
+ // display is always trusted since it is created by the system
+ assertEquals(true, (info.flags & DisplayDeviceInfo.FLAG_TRUSTED) != 0);
+ }
+
+ /**
* Confirm that external display uses physical density.
*/
@Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index f154dbcee21a..09ce263e9b2f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -3962,7 +3962,7 @@ public class DisplayModeDirectorTest {
}
@Override
- public VotesStatsReporter getVotesStatsReporter(boolean refreshRateVotingTelemetryEnabled) {
+ public VotesStatsReporter getVotesStatsReporter() {
return null;
}
diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml
index aa3930ac7c07..b509b0f9fd92 100644
--- a/services/tests/mockingservicestests/AndroidManifest.xml
+++ b/services/tests/mockingservicestests/AndroidManifest.xml
@@ -52,6 +52,16 @@
<uses-library android:name="android.test.runner" />
<activity
android:name="android.service.games.GameSessionTrampolineActivityTest$TestActivity" />
+ <service android:name="com.android.server.wallpaper.TestWallpaperService"
+ android:label="Test Wallpaper Service"
+ android:exported="true"
+ android:permission="android.permission.BIND_WALLPAPER">
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService"/>
+ </intent-filter>
+ <meta-data android:name="android.service.wallpaper"
+ android:resource="@xml/test_wallpaper"/>
+ </service>
</application>
<instrumentation
diff --git a/services/tests/mockingservicestests/res/xml/test_wallpaper.xml b/services/tests/mockingservicestests/res/xml/test_wallpaper.xml
new file mode 100644
index 000000000000..4eed477337b5
--- /dev/null
+++ b/services/tests/mockingservicestests/res/xml/test_wallpaper.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2025 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="Test Wallpaper"
+ android:supportsMultipleDisplays="true" />
diff --git a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java
index 2e4b97ef7dd2..371b0c926039 100644
--- a/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/StorageManagerServiceTest.java
@@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
+import android.app.PropertyInvalidatedCache;
import android.content.Context;
import android.multiuser.Flags;
import android.os.UserManager;
@@ -75,6 +76,8 @@ public class StorageManagerServiceTest {
@Before
public void setFixtures() {
+ PropertyInvalidatedCache.disableForTestMode();
+
// Called when WatchedUserStates is constructed
doNothing().when(() -> UserManager.invalidateIsUserUnlockedCache());
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
index 1e665c2c5c50..317e19abe511 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
@@ -1550,6 +1550,117 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest {
verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2));
}
+ @Test
+ public void testDeliveryGroupPolicy_sameAction_multiplePolicies() {
+ // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_GREEN
+ // package.
+ final Intent greenPackageChangedIntent = createPackageChangedIntent(
+ getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN));
+ // Create delivery group policy such that when there are multiple broadcasts within the
+ // delivery group identified by "com.example.green/10002", only the most recent one
+ // gets delivered and the rest get discarded.
+ final BroadcastOptions optionsMostRecentPolicyForPackageGreen =
+ BroadcastOptions.makeBasic();
+ optionsMostRecentPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed",
+ PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN));
+ optionsMostRecentPolicyForPackageGreen.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+ // Create a PACKAGE_CHANGED broadcast corresponding to a change in the whole PACKAGE_RED
+ // package.
+ final Intent redPackageChangedIntent = createPackageChangedIntent(
+ getUidForPackage(PACKAGE_RED), List.of(PACKAGE_RED));
+ // Create delivery group policy such that when there are multiple broadcasts within the
+ // delivery group identified by "com.example.red/10001", only the most recent one
+ // gets delivered and the rest get discarded.
+ final BroadcastOptions optionsMostRecentPolicyForPackageRed =
+ BroadcastOptions.makeBasic();
+ optionsMostRecentPolicyForPackageRed.setDeliveryGroupMatchingKey("package_changed",
+ PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED));
+ optionsMostRecentPolicyForPackageRed.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+
+ // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of
+ // PACKAGE_GREEN package.
+ final Intent greenPackageComponentsChangedIntent1 = createPackageChangedIntent(
+ getUidForPackage(PACKAGE_GREEN),
+ List.of(PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2"));
+ final Intent greenPackageComponentsChangedIntent2 = createPackageChangedIntent(
+ getUidForPackage(PACKAGE_GREEN),
+ List.of(PACKAGE_GREEN + ".comp3"));
+ // Create delivery group policy such that when there are multiple broadcasts within the
+ // delivery group identified by "components-com.example.green/10002", merge the extras
+ // within these broadcasts such that only one broadcast is sent and the rest are
+ // discarded. Couple of things to note here:
+ // 1. We are intentionally using a different policy group
+ // "components-com.example.green/10002" (as opposed to "com.example.green/10002" used
+ // earlier), because this is corresponding to a change in some particular components,
+ // rather than a change to the whole package and we want to keep these two types of
+ // broadcasts independent.
+ // 2. We are using 'extrasMerger' to indicate how we want the extras to be merged. This
+ // assumes that broadcasts belonging to the group 'components-com.example.green/10002'
+ // will have the same values for all the extras, except for the one extra
+ // 'EXTRA_CHANGED_COMPONENT_NAME_LIST'. So, we explicitly specify how to merge this
+ // extra by using 'STRATEGY_ARRAY_APPEND' strategy, which basically indicates that
+ // the extra values which are arrays should be concatenated.
+ final BundleMerger extrasMerger = new BundleMerger();
+ extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST,
+ BundleMerger.STRATEGY_ARRAY_APPEND);
+ final BroadcastOptions optionsMergedPolicyForPackageGreen = BroadcastOptions.makeBasic();
+ optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed",
+ "components-" + PACKAGE_GREEN + "/" + getUidForPackage(PACKAGE_GREEN));
+ optionsMergedPolicyForPackageGreen.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+ optionsMergedPolicyForPackageGreen.setDeliveryGroupExtrasMerger(extrasMerger);
+
+ // Create a PACKAGE_CHANGED broadcast corresponding to a change in some components of
+ // PACKAGE_RED package.
+ final Intent redPackageComponentsChangedIntent = createPackageChangedIntent(
+ getUidForPackage(PACKAGE_RED),
+ List.of(PACKAGE_RED + ".comp1", PACKAGE_RED + ".comp2"));
+ // Create delivery group policy such that when there are multiple broadcasts within the
+ // delivery group identified by "components-com.example.red/10001", merge the extras
+ // within these broadcasts such that only one broadcast is sent and the rest are
+ // discarded.
+ final BroadcastOptions optionsMergedPolicyForPackageRed = BroadcastOptions.makeBasic();
+ optionsMergedPolicyForPackageGreen.setDeliveryGroupMatchingKey("package_changed",
+ "components-" + PACKAGE_RED + "/" + getUidForPackage(PACKAGE_RED));
+ optionsMergedPolicyForPackageRed.setDeliveryGroupPolicy(
+ BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED);
+ optionsMergedPolicyForPackageRed.setDeliveryGroupExtrasMerger(extrasMerger);
+
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent,
+ optionsMostRecentPolicyForPackageGreen));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageChangedIntent,
+ optionsMostRecentPolicyForPackageRed));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent1,
+ optionsMergedPolicyForPackageGreen));
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(redPackageComponentsChangedIntent,
+ optionsMergedPolicyForPackageRed));
+ // Since this broadcast has DELIVERY_GROUP_MOST_RECENT policy set, the earlier
+ // greenPackageChangedIntent broadcast with the same policy will be discarded.
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageChangedIntent,
+ optionsMostRecentPolicyForPackageGreen));
+ // Since this broadcast has DELIVERY_GROUP_MERGED policy set, the earlier
+ // greenPackageComponentsChangedIntent1 broadcast with the same policy will be merged
+ // with this one and then will be discarded.
+ mImpl.enqueueBroadcastLocked(makeBroadcastRecord(greenPackageComponentsChangedIntent2,
+ optionsMergedPolicyForPackageGreen));
+
+ final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ // The extra EXTRA_CHANGED_COMPONENT_NAME_LIST values from
+ // greenPackageComponentsChangedIntent1 and
+ // greenPackageComponentsChangedIntent2 broadcasts would be merged, since
+ // STRATEGY_ARRAY_APPEND was used for this extra.
+ final Intent expectedGreenPackageComponentsChangedIntent = createPackageChangedIntent(
+ getUidForPackage(PACKAGE_GREEN), List.of(PACKAGE_GREEN + ".comp3",
+ PACKAGE_GREEN + ".comp1", PACKAGE_GREEN + ".comp2"));
+ verifyPendingRecords(queue, List.of(redPackageChangedIntent,
+ redPackageComponentsChangedIntent, greenPackageChangedIntent,
+ expectedGreenPackageComponentsChangedIntent));
+ }
+
private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs,
int droppedCount) {
final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
index 86bf203771ba..409b114100e7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -27,6 +27,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
@@ -73,6 +74,7 @@ public class SystemBackupAgentTest {
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_systemUser_addsAllHelpers() {
UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -94,10 +96,12 @@ public class SystemBackupAgentTest {
"app_gender",
"companion",
"system_gender",
- "display");
+ "display",
+ "input");
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_systemUser_slicesDisabled_addsAllNonSlicesHelpers() {
UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -120,10 +124,12 @@ public class SystemBackupAgentTest {
"app_gender",
"companion",
"system_gender",
- "display");
+ "display",
+ "input");
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_profileUser_addsProfileEligibleHelpers() {
UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
when(mUserManagerMock.isProfile()).thenReturn(true);
@@ -143,6 +149,7 @@ public class SystemBackupAgentTest {
}
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE_FOR_INPUT_GESTURES)
public void onCreate_nonSystemUser_addsNonSystemEligibleHelpers() {
UserHandle userHandle = new UserHandle(NON_SYSTEM_USER_ID);
when(mUserManagerMock.isProfile()).thenReturn(false);
@@ -162,7 +169,8 @@ public class SystemBackupAgentTest {
"companion",
"app_gender",
"system_gender",
- "display");
+ "display",
+ "input");
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java
new file mode 100644
index 000000000000..85ea5a0f2c2e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.wallpaper;
+
+import android.service.wallpaper.WallpaperService;
+
+public final class TestWallpaperService extends WallpaperService {
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index a5073599b29e..bc04fd94c719 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -136,6 +136,10 @@ public class WallpaperManagerServiceTests {
private static final String TAG = "WallpaperManagerServiceTests";
private static final int DISPLAY_SIZE_DIMENSION = 100;
+
+ private static final ComponentName TEST_WALLPAPER_COMPONENT = ComponentName.createRelative(
+ "com.android.frameworks.mockingservicestests",
+ "com.android.server.wallpaper.TestWallpaperService");
private static StaticMockitoSession sMockitoSession;
@ClassRule
@@ -144,6 +148,7 @@ public class WallpaperManagerServiceTests {
private static ComponentName sImageWallpaperComponentName;
private static ComponentName sDefaultWallpaperComponent;
+ private static WallpaperDescription sDefaultWallpaperDescription;
private static ComponentName sFallbackWallpaperComponentName;
@@ -210,8 +215,11 @@ public class WallpaperManagerServiceTests {
} else {
sContext.addMockService(sDefaultWallpaperComponent, sWallpaperService);
}
+ sDefaultWallpaperDescription = new WallpaperDescription.Builder().setComponent(
+ sDefaultWallpaperComponent).build();
sContext.addMockService(sImageWallpaperComponentName, sWallpaperService);
+ sContext.addMockService(TEST_WALLPAPER_COMPONENT, sWallpaperService);
if (sFallbackWallpaperComponentName != null) {
sContext.addMockService(sFallbackWallpaperComponentName, sWallpaperService);
}
@@ -484,11 +492,12 @@ public class WallpaperManagerServiceTests {
}
@Test
- @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+ @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+ Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
public void testSaveLoadSettings_withoutWallpaperDescription()
throws IOException, XmlPullParserException {
WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
- expectedData.setComponent(sDefaultWallpaperComponent);
+ expectedData.setDescription(sDefaultWallpaperDescription);
expectedData.primaryColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
expectedData.mWallpaperDimAmount = 0.5f;
@@ -524,11 +533,12 @@ public class WallpaperManagerServiceTests {
}
@Test
- @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+ @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+ Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
public void testSaveLoadSettings_withWallpaperDescription()
throws IOException, XmlPullParserException {
WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
- expectedData.setComponent(sDefaultWallpaperComponent);
+ expectedData.setDescription(sDefaultWallpaperDescription);
PersistableBundle content = new PersistableBundle();
content.putString("ckey", "cvalue");
WallpaperDescription description = new WallpaperDescription.Builder()
@@ -556,7 +566,8 @@ public class WallpaperManagerServiceTests {
}
@Test
- @DisableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+ @DisableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+ Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
public void testSaveLoadSettings_legacyNextComponent()
throws IOException, XmlPullParserException {
WallpaperData systemWallpaperData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
@@ -1033,35 +1044,33 @@ public class WallpaperManagerServiceTests {
}
// Verify a secondary display removes system decorations ended
- // Test setWallpaperComponent on multiple displays.
- // GIVEN 3 displays, 0, 2, 3, the new wallpaper is only compatible for display 0 and 3 but not
- // 2.
- // WHEN the new wallpaper is set for system and lock via setWallpaperComponent.
+ // Test fallback connection is correctly established for multiple displays after reboot.
+ // GIVEN 3 displays, 0, 2, 3, the wallpaper is only compatible for display 0 and 3 but not 2.
+ // WHEN the device is booted.
// THEN there are 2 connections in mLastWallpaper and 1 connection in mFallbackWallpaper.
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
- public void setWallpaperComponent_multiDisplays_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
+ public void deviceBooted_multiDisplays_shouldHaveExpectedConnections() {
final int incompatibleDisplayId = 2;
final int compatibleDisplayId = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, incompatibleDisplayId, compatibleDisplayId));
mService.removeWallpaperCompatibleDisplayForTest(incompatibleDisplayId);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
- FLAG_SYSTEM | FLAG_LOCK, testUserId);
+ final int testUserId = USER_SYSTEM;
+ // After reboot, a switch user triggers the wallpapers initialization.
+ mService.switchUser(testUserId, null);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(compatibleDisplayId))
.isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isFalse();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
+ .isFalse();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(compatibleDisplayId))
+ .isFalse();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId))
.isTrue();
assertThat(mService.mLastLockWallpaper).isNull();
@@ -1076,30 +1085,31 @@ public class WallpaperManagerServiceTests {
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void setWallpaperComponent_multiDisplays_displayBecomeCompatible_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
final int display2 = 2;
final int display3 = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, display2, display3));
mService.removeWallpaperCompatibleDisplayForTest(display2);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change.
+ mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
-
mService.addWallpaperCompatibleDisplayForTest(display2);
+
mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(3);
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(display2)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(display3)).isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(0);
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isFalse();
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(display2)).isFalse();
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(display3)).isFalse();
assertThat(mService.mLastLockWallpaper).isNull();
}
@@ -1112,28 +1122,27 @@ public class WallpaperManagerServiceTests {
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void setWallpaperComponent_multiDisplays_displayBecomeIncompatible_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
final int display2 = 2;
final int display3 = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, display2, display3));
mService.removeWallpaperCompatibleDisplayForTest(display2);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change.
+ mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
-
mService.removeWallpaperCompatibleDisplayForTest(display3);
+
mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(display2)).isFalse();
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(display3)).isFalse();
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isFalse();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display2)).isTrue();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display3)).isTrue();
assertThat(mService.mLastLockWallpaper).isNull();
@@ -1148,35 +1157,40 @@ public class WallpaperManagerServiceTests {
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void setWallpaperComponent_systemAndLockWallpapers_multiDisplays_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
final int incompatibleDisplayId = 2;
final int compatibleDisplayId = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, incompatibleDisplayId, compatibleDisplayId));
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change.
+ mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(),
+ FLAG_SYSTEM | FLAG_LOCK, testUserId);
mService.removeWallpaperCompatibleDisplayForTest(incompatibleDisplayId);
mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
FLAG_SYSTEM, testUserId);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
- FLAG_LOCK, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
- verifyLastLockWallpaperData(testUserId, sImageWallpaperComponentName);
+ verifyLastLockWallpaperData(testUserId, TEST_WALLPAPER_COMPONENT);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
+
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(compatibleDisplayId))
.isTrue();
- assertThat(mService.mLastLockWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isFalse();
+ // mLastLockWallpaper is TEST_WALLPAPER_COMPONENT, which declares external displays support
+ // in the wallpaper metadata.
assertThat(mService.mLastLockWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
.isTrue();
assertThat(mService.mLastLockWallpaper.connection.containsDisplay(compatibleDisplayId))
.isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
+ assertThat(mService.mLastLockWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isTrue();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
+ .isFalse();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(compatibleDisplayId))
+ .isFalse();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId))
.isTrue();
}
@@ -1281,4 +1295,6 @@ public class WallpaperManagerServiceTests {
assertEquals(pfdContents, fileContents);
}
}
+
+
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
index c824c3948e2d..c7c23f081044 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/accessibility/OWNERS
@@ -1,3 +1,6 @@
-# Bug component: 44215
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 44215.
include /core/java/android/view/accessibility/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 457fde8d74d0..0227ef1d2dc0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -85,7 +85,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_lazyInitClickScheduler() {
assertThat(mController.mClickScheduler).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mClickScheduler).isNotNull();
}
@@ -94,7 +94,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_nonMouseSource_notInitClickScheduler() {
assertThat(mController.mClickScheduler).isNull();
- injectFakeNonMouseActionDownEvent();
+ injectFakeNonMouseActionHoverMoveEvent();
assertThat(mController.mClickScheduler).isNull();
}
@@ -103,7 +103,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_lazyInitAutoclickSettingsObserver() {
assertThat(mController.mAutoclickSettingsObserver).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickSettingsObserver).isNotNull();
}
@@ -113,7 +113,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorScheduler() {
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorScheduler).isNotNull();
}
@@ -123,7 +123,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOff_notInitAutoclickIndicatorScheduler() {
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
}
@@ -133,7 +133,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorView() {
assertThat(mController.mAutoclickIndicatorView).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorView).isNotNull();
}
@@ -143,7 +143,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOff_notInitAutoclickIndicatorView() {
assertThat(mController.mAutoclickIndicatorView).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickIndicatorView).isNull();
}
@@ -153,7 +153,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOn_lazyInitAutoclickTypePanelView() {
assertThat(mController.mAutoclickTypePanel).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickTypePanel).isNotNull();
}
@@ -163,7 +163,7 @@ public class AutoclickControllerTest {
public void onMotionEvent_flagOff_notInitAutoclickTypePanelView() {
assertThat(mController.mAutoclickTypePanel).isNull();
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
assertThat(mController.mAutoclickTypePanel).isNull();
}
@@ -171,7 +171,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOn_addAutoclickIndicatorViewToWindowManager() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
verify(mMockWindowManager).addView(eq(mController.mAutoclickIndicatorView), any());
}
@@ -179,7 +179,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onDestroy_flagOn_removeAutoclickIndicatorViewToWindowManager() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
@@ -189,7 +189,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onDestroy_flagOn_removeAutoclickTypePanelViewToWindowManager() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
mController.mAutoclickTypePanel = mockAutoclickTypePanel;
@@ -200,7 +200,7 @@ public class AutoclickControllerTest {
@Test
public void onMotionEvent_initClickSchedulerDelayFromSetting() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
int delay =
Settings.Secure.getIntForUser(
@@ -214,7 +214,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOn_initCursorAreaSizeFromSetting() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
int size =
Settings.Secure.getIntForUser(
@@ -238,7 +238,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onKeyEvent_modifierKey_updateMetaStateWhenControllerNotNull() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
int metaState = KeyEvent.META_ALT_ON | KeyEvent.META_META_ON;
injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, metaState);
@@ -250,7 +250,7 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onKeyEvent_modifierKey_cancelAutoClickWhenAdditionalRegularKeyPresssed() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
injectFakeKeyEvent(KeyEvent.KEYCODE_J, KeyEvent.META_ALT_ON);
@@ -260,7 +260,7 @@ public class AutoclickControllerTest {
@Test
public void onDestroy_clearClickScheduler() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
@@ -269,7 +269,7 @@ public class AutoclickControllerTest {
@Test
public void onDestroy_clearAutoclickSettingsObserver() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
@@ -279,21 +279,61 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onDestroy_flagOn_clearAutoclickIndicatorScheduler() {
- injectFakeMouseActionDownEvent();
+ injectFakeMouseActionHoverMoveEvent();
mController.onDestroy();
assertThat(mController.mAutoclickIndicatorScheduler).isNull();
}
- private void injectFakeMouseActionDownEvent() {
- MotionEvent event = getFakeMotionDownEvent();
+ @Test
+ @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_hoverEnter_doesNotScheduleClick() {
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Send hover enter event.
+ MotionEvent hoverEnter = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_ENTER,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverEnter.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverEnter, hoverEnter, /* policyFlags= */ 0);
+
+ // Verify there is no pending click.
+ assertThat(mController.mClickScheduler.getIsActiveForTesting()).isFalse();
+ }
+
+ @Test
+ @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_hoverMove_scheduleClick() {
+ injectFakeMouseActionHoverMoveEvent();
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+
+ // Verify there is a pending click.
+ assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue();
+ }
+
+ private void injectFakeMouseActionHoverMoveEvent() {
+ MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_MOUSE);
mController.onMotionEvent(event, event, /* policyFlags= */ 0);
}
- private void injectFakeNonMouseActionDownEvent() {
- MotionEvent event = getFakeMotionDownEvent();
+ private void injectFakeNonMouseActionHoverMoveEvent() {
+ MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_KEYBOARD);
mController.onMotionEvent(event, event, /* policyFlags= */ 0);
}
@@ -309,11 +349,11 @@ public class AutoclickControllerTest {
mController.onKeyEvent(keyEvent, /* policyFlags= */ 0);
}
- private MotionEvent getFakeMotionDownEvent() {
+ private MotionEvent getFakeMotionHoverMoveEvent() {
return MotionEvent.obtain(
/* downTime= */ 0,
/* eventTime= */ 0,
- /* action= */ MotionEvent.ACTION_DOWN,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
/* x= */ 0,
/* y= */ 0,
/* metaState= */ 0);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
index f0334598bd30..7b8824cb0e3d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
@@ -21,6 +21,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import android.graphics.drawable.GradientDrawable;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
@@ -28,6 +29,8 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
+import androidx.annotation.NonNull;
+
import com.android.internal.R;
import org.junit.Before;
@@ -87,6 +90,11 @@ public class AutoclickTypePanelTest {
}
@Test
+ public void AutoclickTypePanel_initialState_correctButtonStyle() {
+ verifyButtonHasSelectedStyle(mLeftClickButton);
+ }
+
+ @Test
public void togglePanelExpansion_onClick_expandedTrue() {
// On clicking left click button, the panel is expanded and all buttons are visible.
mLeftClickButton.callOnClick();
@@ -116,4 +124,21 @@ public class AutoclickTypePanelTest {
assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.GONE);
assertThat(mDragButton.getVisibility()).isEqualTo(View.GONE);
}
+
+ @Test
+ public void togglePanelExpansion_selectButton_correctStyle() {
+ // By first click, the panel is expanded.
+ mLeftClickButton.callOnClick();
+
+ // Clicks any button in the expanded state to select a type button.
+ mScrollButton.callOnClick();
+
+ verifyButtonHasSelectedStyle(mScrollButton);
+ }
+
+ private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) {
+ GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
+ assertThat(gradientDrawable.getColor().getDefaultColor())
+ .isEqualTo(mTestableContext.getColor(R.color.materialColorPrimary));
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS
new file mode 100644
index 000000000000..9592bfdfa73b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 1530954
+#
+# The above component is for automated test bugs. If you are a human looking to report
+# a bug in this codebase then please use component 770744.
+
+include /services/accessibility/java/com/android/server/accessibility/magnification/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 1627f683cd3e..06958b81d846 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -25,7 +25,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
@@ -116,6 +115,7 @@ import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.UserTypeDetails;
import com.android.server.pm.UserTypeFactory;
+import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerService;
import com.google.common.collect.Range;
@@ -1563,11 +1563,11 @@ public class UserControllerTest {
// and the thread is still alive
assertTrue(threadStartUser.isAlive());
- // mock the binder response for the user switch completion
- ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
- verify(mInjector.mWindowManagerMock).lockNow(captor.capture());
- IRemoteCallback.Stub.asInterface(captor.getValue().getBinder(
- LOCK_ON_USER_SWITCH_CALLBACK)).sendResult(null);
+ // mock send the keyguard shown event
+ ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass(
+ ActivityTaskManagerInternal.ScreenObserver.class);
+ verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture());
+ captor.getValue().onKeyguardStateChanged(true);
// verify the switch now moves on...
Thread.sleep(1000);
@@ -1757,6 +1757,7 @@ public class UserControllerTest {
private final IStorageManager mStorageManagerMock;
private final UserManagerInternal mUserManagerInternalMock;
private final WindowManagerService mWindowManagerMock;
+ private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final PowerManagerInternal mPowerManagerInternal;
private final AlarmManagerInternal mAlarmManagerInternal;
private final KeyguardManager mKeyguardManagerMock;
@@ -1778,6 +1779,7 @@ public class UserControllerTest {
mUserManagerMock = mock(UserManagerService.class);
mUserManagerInternalMock = mock(UserManagerInternal.class);
mWindowManagerMock = mock(WindowManagerService.class);
+ mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class);
mStorageManagerMock = mock(IStorageManager.class);
mPowerManagerInternal = mock(PowerManagerInternal.class);
mAlarmManagerInternal = mock(AlarmManagerInternal.class);
@@ -1841,6 +1843,11 @@ public class UserControllerTest {
}
@Override
+ ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+ return mActivityTaskManagerInternal;
+ }
+
+ @Override
PowerManagerInternal getPowerManagerInternal() {
return mPowerManagerInternal;
}
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 ffcb96120b19..ab7b4da269db 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
@@ -73,6 +73,7 @@ import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.hardware.Sensor;
+import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerGlobal;
import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.IDisplayManager;
@@ -173,8 +174,7 @@ public class VirtualDeviceManagerServiceTest {
private static final int FLAG_CANNOT_DISPLAY_ON_REMOTE_DEVICES = 0x00000;
private static final int VIRTUAL_DEVICE_ID_1 = 42;
private static final int VIRTUAL_DEVICE_ID_2 = 43;
- private static final VirtualDisplayConfig VIRTUAL_DISPLAY_CONFIG =
- new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400).build();
+
private static final VirtualDpadConfig DPAD_CONFIG =
new VirtualDpadConfig.Builder()
.setVendorId(VENDOR_ID)
@@ -284,7 +284,12 @@ public class VirtualDeviceManagerServiceTest {
private Intent createRestrictedActivityBlockedIntent(Set<String> displayCategories,
String targetDisplayCategory) {
when(mDisplayManagerInternalMock.createVirtualDisplay(any(), any(), any(), any(),
- eq(VIRTUAL_DEVICE_OWNER_PACKAGE))).thenReturn(DISPLAY_ID_1);
+ eq(VIRTUAL_DEVICE_OWNER_PACKAGE)))
+ .thenAnswer(inv -> {
+ mLocalService.onVirtualDisplayCreated(
+ mDeviceImpl, DISPLAY_ID_1, inv.getArgument(1), inv.getArgument(3));
+ return DISPLAY_ID_1;
+ });
VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("display", 640, 480,
420).setDisplayCategories(displayCategories).build();
mDeviceImpl.createVirtualDisplay(config, mVirtualDisplayCallback);
@@ -997,8 +1002,7 @@ public class VirtualDeviceManagerServiceTest {
public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired()
throws RemoteException {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
- assertThrows(IllegalStateException.class,
- () -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1));
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
TestableLooper.get(this).processAllMessages();
verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(),
nullable(String.class), nullable(String.class), nullable(WorkSource.class),
@@ -1871,8 +1875,6 @@ public class VirtualDeviceManagerServiceTest {
}
private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) {
- when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
- eq(virtualDevice), any(), any())).thenReturn(displayId);
final String uniqueId = UNIQUE_ID + displayId;
doAnswer(inv -> {
final DisplayInfo displayInfo = new DisplayInfo();
@@ -1880,7 +1882,22 @@ public class VirtualDeviceManagerServiceTest {
displayInfo.flags = flags;
return displayInfo;
}).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
- virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
+
+ when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
+ eq(virtualDevice), any(), any())).thenAnswer(inv -> {
+ mLocalService.onVirtualDisplayCreated(
+ virtualDevice, displayId, mVirtualDisplayCallback, inv.getArgument(3));
+ return displayId;
+ });
+
+ final int virtualDisplayFlags = (flags & Display.FLAG_TRUSTED) == 0
+ ? 0
+ : DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
+ VirtualDisplayConfig virtualDisplayConfig =
+ new VirtualDisplayConfig.Builder("virtual_display", 640, 480, 400)
+ .setFlags(virtualDisplayFlags)
+ .build();
+ virtualDevice.createVirtualDisplay(virtualDisplayConfig, mVirtualDisplayCallback);
mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
index 770712a191fd..41011928f8b3 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java
@@ -204,7 +204,7 @@ public class NotificationChannelExtractorTest extends UiServiceTestCase {
.build());
final Notification n = new Notification.Builder(getContext())
.setContentTitle("foo")
- .setCategory(CATEGORY_ALARM)
+ .setCategory(new String("alarm"))
.setSmallIcon(android.R.drawable.sym_def_app_icon)
.build();
NotificationRecord r = getRecord(channel, n);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 65150e7b48fc..440f43e9b926 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -49,17 +49,13 @@ import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
import static android.os.Process.NOBODY_UID;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.InsetsSource.ID_IME;
-import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
-import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
@@ -125,7 +121,6 @@ import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.ClientTransactionItem;
import android.app.servertransaction.DestroyActivityItem;
import android.app.servertransaction.PauseActivityItem;
-import android.app.servertransaction.WindowStateResizeItem;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ComponentName;
import android.content.Intent;
@@ -149,8 +144,6 @@ import android.view.DisplayInfo;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner.Stub;
import android.view.IWindowManager;
-import android.view.InsetsSource;
-import android.view.InsetsState;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.Surface;
@@ -171,7 +164,6 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import java.util.ArrayList;
@@ -3370,178 +3362,6 @@ public class ActivityRecordTests extends WindowTestsBase {
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
}
- @SetupWindows(addWindows = W_INPUT_METHOD)
- @Test
- public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() {
- final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
- makeWindowVisibleAndDrawn(app, mImeWindow);
- mDisplayContent.setImeLayeringTarget(app);
- mDisplayContent.setImeInputTarget(app);
-
- // Simulate app is closing and expect the last IME is shown and IME insets is frozen.
- mDisplayContent.mOpeningApps.clear();
- app.mActivityRecord.commitVisibility(false, false);
- app.mActivityRecord.onWindowsGone();
-
- assertTrue(app.mActivityRecord.mLastImeShown);
- assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
- // Expect IME insets frozen state will reset when the activity has no IME focusable window.
- app.mActivityRecord.forAllWindows(w -> {
- w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
- return true;
- }, true);
-
- app.mActivityRecord.commitVisibility(true, false);
- app.mActivityRecord.onWindowsVisible();
-
- assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
- }
-
- @SetupWindows(addWindows = W_INPUT_METHOD)
- @Test
- public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() {
- final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
-
- mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
- mImeWindow, null, null);
- mImeWindow.getControllableInsetProvider().setServerVisible(true);
-
- InsetsSource imeSource = new InsetsSource(ID_IME, ime());
- app.mAboveInsetsState.addSource(imeSource);
- mDisplayContent.setImeLayeringTarget(app);
- mDisplayContent.updateImeInputAndControlTarget(app);
-
- InsetsState state = app.getInsetsState();
- assertFalse(state.getOrCreateSource(imeSource.getId(), ime()).isVisible());
- assertTrue(state.getOrCreateSource(imeSource.getId(), ime()).getFrame().isEmpty());
-
- // Simulate app is closing and expect IME insets is frozen.
- mDisplayContent.mOpeningApps.clear();
- app.mActivityRecord.commitVisibility(false, false);
- app.mActivityRecord.onWindowsGone();
- assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
- // Simulate app re-start input or turning screen off/on then unlocked by un-secure
- // keyguard to back to the app, expect IME insets is not frozen
- app.mActivityRecord.commitVisibility(true, false);
- mDisplayContent.updateImeInputAndControlTarget(app);
- performSurfacePlacementAndWaitForWindowAnimator();
-
- assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
- imeSource.setVisible(true);
- imeSource.setFrame(new Rect(100, 400, 500, 500));
- app.mAboveInsetsState.addSource(imeSource);
-
- // Verify when IME is visible and the app can receive the right IME insets from policy.
- makeWindowVisibleAndDrawn(app, mImeWindow);
- state = app.getInsetsState();
- assertTrue(state.peekSource(ID_IME).isVisible());
- assertEquals(state.peekSource(ID_IME).getFrame(), imeSource.getFrame());
- }
-
- @SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
- @Test
- public void testImeInsetsFrozenFlag_noDispatchVisibleInsetsWhenAppNotRequest()
- throws RemoteException {
- final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).build();
- final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build();
-
- mDisplayContent.getInsetsStateController().getImeSourceProvider().setWindowContainer(
- mImeWindow, null, null);
- mImeWindow.getControllableInsetProvider().setServerVisible(true);
-
- // Simulate app2 is closing and let app1 is visible to be IME targets.
- makeWindowVisibleAndDrawn(app1, mImeWindow);
- mDisplayContent.setImeLayeringTarget(app1);
- mDisplayContent.updateImeInputAndControlTarget(app1);
- app2.mActivityRecord.commitVisibility(false, false);
-
- // app1 requests IME visible.
- app1.setRequestedVisibleTypes(ime(), ime());
- mDisplayContent.getInsetsStateController().onRequestedVisibleTypesChanged(app1,
- null /* statsToken */);
-
- // Verify app1's IME insets is visible and app2's IME insets frozen flag set.
- assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
- assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
- // Simulate switching to app2 to make it visible to be IME targets.
- spyOn(app2);
- spyOn(app2.mClient);
- spyOn(app2.getProcess());
- ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class);
- doReturn(true).when(app2).isReadyToDispatchInsetsState();
- mDisplayContent.setImeLayeringTarget(app2);
- app2.mActivityRecord.commitVisibility(true, false);
- mDisplayContent.updateImeInputAndControlTarget(app2);
- performSurfacePlacementAndWaitForWindowAnimator();
-
- // Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets
- // to client if the app didn't request IME visible.
- assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
-
- verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
- isA(WindowStateResizeItem.class));
- assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
- }
-
- @Test
- public void testImeInsetsFrozenFlag_multiWindowActivities() {
- final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent);
- final WindowState ime = newWindowBuilder("ime", TYPE_INPUT_METHOD).setWindowToken(
- imeToken).build();
- makeWindowVisibleAndDrawn(ime);
-
- // Create a split-screen root task with activity1 and activity 2.
- final Task task = new TaskBuilder(mSupervisor)
- .setCreateParentTask(true).setCreateActivity(true).build();
- task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
- final ActivityRecord activity1 = task.getTopNonFinishingActivity();
- activity1.getTask().setResumedActivity(activity1, "testApp1");
-
- final ActivityRecord activity2 = new TaskBuilder(mSupervisor)
- .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
- .setCreateActivity(true).build().getTopMostActivity();
- activity2.getTask().setResumedActivity(activity2, "testApp2");
- activity2.getTask().setParent(task.getRootTask());
-
- // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when
- // invisible to user.
- activity1.mImeInsetsFrozenUntilStartInput = true;
- activity2.mImeInsetsFrozenUntilStartInput = true;
-
- final WindowState app1 = newWindowBuilder("app1", TYPE_APPLICATION).setWindowToken(
- activity1).build();
- final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowToken(
- activity2).build();
- makeWindowVisibleAndDrawn(app1, app2);
-
- final InsetsStateController controller = mDisplayContent.getInsetsStateController();
- controller.getImeSourceProvider().setWindowContainer(
- ime, null, null);
- ime.getControllableInsetProvider().setServerVisible(true);
-
- // app1 starts input and expect IME insets for all activities in split-screen will be
- // frozen until the input started.
- mDisplayContent.setImeLayeringTarget(app1);
- mDisplayContent.updateImeInputAndControlTarget(app1);
- mDisplayContent.computeImeTarget(true /* updateImeTarget */);
- performSurfacePlacementAndWaitForWindowAnimator();
-
- assertEquals(app1, mDisplayContent.getImeInputTarget());
- assertFalse(activity1.mImeInsetsFrozenUntilStartInput);
- assertFalse(activity2.mImeInsetsFrozenUntilStartInput);
-
- app1.setRequestedVisibleTypes(ime());
- controller.onRequestedVisibleTypesChanged(app1, null /* statsToken */);
-
- // Expect all activities in split-screen will get IME insets visible state
- assertTrue(app1.getInsetsState().peekSource(ID_IME).isVisible());
- assertTrue(app2.getInsetsState().peekSource(ID_IME).isVisible());
- }
-
@Test
public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() {
final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
index e0b700a4ffe3..eaffc481098e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
@@ -97,6 +97,7 @@ public class DesktopModeHelperTest {
public void canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceCheck_returnsFalse()
throws Exception {
doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+ doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
doReturn(true).when(mMockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported));
disableEnforceDeviceRestriction();
@@ -148,6 +149,7 @@ public class DesktopModeHelperTest {
@Test
public void canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() {
doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+ doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
assertThat(DesktopModeHelper.canEnterDesktopMode(mMockContext)).isTrue();
}
@@ -176,21 +178,21 @@ public class DesktopModeHelperTest {
@Test
public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() {
- doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
+ doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops));
- assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
+ assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue();
}
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() {
- assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
+ assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse();
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() {
- assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse();
+ assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse();
}
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@@ -200,7 +202,7 @@ public class DesktopModeHelperTest {
eq(R.bool.config_isDesktopModeDevOptionSupported)
);
- assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
+ assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue();
}
private void resetEnforceDeviceRestriction() throws Exception {
@@ -234,4 +236,4 @@ public class DesktopModeHelperTest {
Settings.Global.putInt(mContext.getContentResolver(),
DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting());
}
-}
+} \ No newline at end of file
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index fdde3b38f19f..d305c2f54456 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -1345,7 +1345,7 @@ public class DesktopModeLaunchParamsModifierTests extends
private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported,
boolean enforceDeviceRestrictions) {
doReturn(isDesktopModeSupported)
- .when(() -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
+ .when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
doReturn(enforceDeviceRestrictions)
.when(DesktopModeHelper::shouldEnforceDeviceRestrictions);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java
index 285a5e246e0c..ea21bb34597d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopWindowingRobot.java
@@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.any;
/** Robot for changing desktop windowing properties. */
class DesktopWindowingRobot {
void allowEnterDesktopMode(boolean isAllowed) {
- doReturn(isAllowed).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
+ doReturn(isAllowed).when(() ->
+ DesktopModeHelper.canEnterDesktopMode(any()));
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 6c5fe1d8551e..71e34ef220d3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -53,6 +53,7 @@ import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.WindowInsets;
+import android.view.WindowInsets.Type.InsetsType;
import androidx.test.filters.SmallTest;
@@ -400,9 +401,9 @@ public class InsetsPolicyTest extends WindowTestsBase {
assertTrue(state.isSourceOrDefaultVisible(statusBarSource.getId(), statusBars()));
assertTrue(state.isSourceOrDefaultVisible(navBarSource.getId(), navigationBars()));
- mAppWindow.setRequestedVisibleTypes(
+ final @InsetsType int changedTypes = mAppWindow.setRequestedVisibleTypes(
navigationBars() | statusBars(), navigationBars() | statusBars());
- policy.onRequestedVisibleTypesChanged(mAppWindow, null /* statsToken */);
+ policy.onRequestedVisibleTypesChanged(mAppWindow, changedTypes, null /* statsToken */);
waitUntilWindowAnimatorIdle();
controls = mDisplayContent.getInsetsStateController().getControlsForDispatch(mAppWindow);
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 973c8d0a8464..5525bae89138 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -52,6 +52,7 @@ import android.util.SparseArray;
import android.view.InsetsSource;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
+import android.view.WindowInsets.Type.InsetsType;
import androidx.test.filters.SmallTest;
@@ -201,8 +202,8 @@ public class InsetsStateControllerTest extends WindowTestsBase {
getController().getOrCreateSourceProvider(ID_IME, ime())
.setWindowContainer(mImeWindow, null, null);
getController().onImeControlTargetChanged(base);
- base.setRequestedVisibleTypes(ime(), ime());
- getController().onRequestedVisibleTypesChanged(base, null /* statsToken */);
+ final @InsetsType int changedTypes = base.setRequestedVisibleTypes(ime(), ime());
+ getController().onRequestedVisibleTypesChanged(base, changedTypes, null /* statsToken */);
if (android.view.inputmethod.Flags.refactorInsetsController()) {
// to set the serverVisibility, the IME needs to be drawn and onPostLayout be called.
mImeWindow.mWinAnimator.mDrawState = HAS_DRAWN;
@@ -509,8 +510,8 @@ public class InsetsStateControllerTest extends WindowTestsBase {
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.updateImeInputAndControlTarget(app);
- app.setRequestedVisibleTypes(ime(), ime());
- getController().onRequestedVisibleTypesChanged(app, null /* statsToken */);
+ final @InsetsType int changedTypes = app.setRequestedVisibleTypes(ime(), ime());
+ getController().onRequestedVisibleTypesChanged(app, changedTypes, null /* statsToken */);
assertTrue(ime.getControllableInsetProvider().getSource().isVisible());
if (android.view.inputmethod.Flags.refactorInsetsController()) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
new file mode 100644
index 000000000000..db90c28ec7df
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.Display.FLAG_PRESENTATION;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.graphics.Rect;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+import android.view.IWindow;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest WmTests:PresentationControllerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class PresentationControllerTests extends WindowTestsBase {
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationHidesActivitiesBehind() {
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.copyFrom(mDisplayInfo);
+ displayInfo.flags = FLAG_PRESENTATION;
+ final DisplayContent dc = createNewDisplay(displayInfo);
+ final int displayId = dc.getDisplayId();
+ doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
+ final ActivityRecord activity = createActivityRecord(createTask(dc));
+ assertTrue(activity.isVisible());
+
+ doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
+ final int uid = 100000; // uid for non-system user
+ final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+ final int userId = UserHandle.getUserId(uid);
+ doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_PRESENTATION);
+
+ final IWindow clientWindow = new TestIWindow();
+ final int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId,
+ userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+ new InsetsSourceControl.Array(), new Rect(), new float[1]);
+ assertTrue(result >= WindowManagerGlobal.ADD_OKAY);
+ assertFalse(activity.isVisible());
+
+ final WindowState window = mWm.windowForClientLocked(session, clientWindow, false);
+ window.removeImmediately();
+ assertTrue(activity.isVisible());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 1323d8a59cef..71e84c0f1821 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -26,7 +26,6 @@ import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCRE
import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_OWN_FOCUS;
-import static android.view.Display.FLAG_PRESENTATION;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
@@ -55,7 +54,6 @@ import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
-import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
import static com.google.common.truth.Truth.assertThat;
@@ -102,7 +100,6 @@ import android.provider.Settings;
import android.util.ArraySet;
import android.util.MergedConfiguration;
import android.view.ContentRecordingSession;
-import android.view.DisplayInfo;
import android.view.IWindow;
import android.view.InputChannel;
import android.view.InputDevice;
@@ -1409,38 +1406,6 @@ public class WindowManagerServiceTests extends WindowTestsBase {
assertEquals(activityWindowInfo2, activityWindowInfo3);
}
- @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
- @Test
- public void testPresentationHidesActivitiesBehind() {
- DisplayInfo displayInfo = new DisplayInfo();
- displayInfo.copyFrom(mDisplayInfo);
- displayInfo.flags = FLAG_PRESENTATION;
- DisplayContent dc = createNewDisplay(displayInfo);
- int displayId = dc.getDisplayId();
- doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
- ActivityRecord activity = createActivityRecord(createTask(dc));
- assertTrue(activity.isVisible());
-
- doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
- int uid = 100000; // uid for non-system user
- Session session = createTestSession(mAtm, 1234 /* pid */, uid);
- int userId = UserHandle.getUserId(uid);
- doReturn(false).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
- WindowManager.LayoutParams params = new WindowManager.LayoutParams(
- LayoutParams.TYPE_PRESENTATION);
-
- final IWindow clientWindow = new TestIWindow();
- int result = mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId,
- userId, WindowInsets.Type.defaultVisible(), null, new InsetsState(),
- new InsetsSourceControl.Array(), new Rect(), new float[1]);
- assertTrue(result >= WindowManagerGlobal.ADD_OKAY);
- assertFalse(activity.isVisible());
-
- final WindowState window = mWm.windowForClientLocked(session, clientWindow, false);
- window.removeImmediately();
- assertTrue(activity.isVisible());
- }
-
@Test
public void testAddOverlayWindowToUnassignedDisplay_notAllowed_ForVisibleBackgroundUsers() {
doReturn(true).when(() -> UserManager.isVisibleBackgroundUsersEnabled());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index cff172f55601..a718c06cc2fa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -1282,7 +1282,6 @@ public class WindowStateTests extends WindowTestsBase {
// Simulate app plays closing transition to app2.
app.mActivityRecord.commitVisibility(false, false);
assertTrue(app.mActivityRecord.mLastImeShown);
- assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
// Verify the IME insets is visible on app, but not for app2 during app task switching.
assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
@@ -1305,7 +1304,7 @@ public class WindowStateTests extends WindowTestsBase {
// Simulate app2 in multi-window mode is going to background to switch to the fullscreen
// app which requests IME with updating all windows Insets State when IME is above app.
- app2.mActivityRecord.mImeInsetsFrozenUntilStartInput = true;
+ app2.mActivityRecord.setVisibleRequested(false);
mDisplayContent.setImeLayeringTarget(app);
mDisplayContent.setImeInputTarget(app);
app.setRequestedVisibleTypes(ime(), ime());
@@ -1324,7 +1323,6 @@ public class WindowStateTests extends WindowTestsBase {
mDisplayContent.setImeLayeringTarget(app2);
app.mActivityRecord.commitVisibility(false, false);
assertTrue(app.mActivityRecord.mLastImeShown);
- assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
// Verify the IME insets is still visible on app, but not for app2 during task switching.
assertTrue(app.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index e0af22369182..d2741ac7ee9f 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -4821,10 +4821,14 @@ public class SubscriptionManager {
+ "Invalid subscriptionId: " + subscriptionId);
}
+ String contextPkg = mContext != null ? mContext.getOpPackageName() : "<unknown>";
+ String contextAttributionTag = mContext != null ? mContext.getAttributionTag() : null;
+
try {
ISub iSub = TelephonyManager.getSubscriptionService();
if (iSub != null) {
- return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId);
+ return iSub.isSubscriptionAssociatedWithCallingUser(subscriptionId, contextPkg,
+ contextAttributionTag);
} else {
throw new IllegalStateException("subscription service unavailable.");
}
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 1bfec29a3cf4..a974c615a4ae 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -347,13 +347,17 @@ interface ISub {
* Returns whether the given subscription is associated with the calling user.
*
* @param subscriptionId the subscription ID of the subscription
+ * @param callingPackage The package maing the call
+ * @param callingFeatureId The feature in the package
+
* @return {@code true} if the subscription is associated with the user that the current process
* is running in; {@code false} otherwise.
*
* @throws IllegalArgumentException if subscription doesn't exist.
* @throws SecurityException if the caller doesn't have permissions required.
*/
- boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId);
+ boolean isSubscriptionAssociatedWithCallingUser(int subscriptionId, String callingPackage,
+ String callingFeatureId);
/**
* Check if subscription and user are associated with each other.
diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml
index 37321ad80b0f..758852bb1074 100644
--- a/tests/AttestationVerificationTest/AndroidManifest.xml
+++ b/tests/AttestationVerificationTest/AndroidManifest.xml
@@ -18,7 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.security.attestationverification">
- <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
+ <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" />
<application>
diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json
new file mode 100644
index 000000000000..2a3ba5ebde7d
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json
@@ -0,0 +1,12 @@
+{
+ "entries": {
+ "6681152659205225093" : {
+ "status": "REVOKED",
+ "reason": "KEY_COMPROMISE"
+ },
+ "8350192447815228107" : {
+ "status": "REVOKED",
+ "reason": "KEY_COMPROMISE"
+ }
+ }
+} \ No newline at end of file
diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json
new file mode 100644
index 000000000000..e22a834a92bf
--- /dev/null
+++ b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json
@@ -0,0 +1,16 @@
+{
+ "entries": {
+ "6681152659205225093" : {
+ "status": "REVOKED",
+ "reason": "KEY_COMPROMISE"
+ },
+ "353017e73dc205a73a9c3de142230370" : {
+ "status": "REVOKED",
+ "reason": "KEY_COMPROMISE"
+ },
+ "8350192447815228107" : {
+ "status": "REVOKED",
+ "reason": "KEY_COMPROMISE"
+ }
+ }
+} \ No newline at end of file
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
new file mode 100644
index 000000000000..c38517ace5e6
--- /dev/null
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.os.SystemClock;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class CertificateRevocationStatusManagerTest {
+
+ private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem";
+ private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem";
+ private static final String TEST_REVOCATION_LIST_FILE_NAME = "test_revocation_list.json";
+ private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST =
+ "test_revocation_list_no_test_certs.json";
+ private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST =
+ "test_revocation_list_with_test_certs.json";
+ private static final String TEST_REVOCATION_STATUS_FILE_NAME = "test_revocation_status.txt";
+ private static final String FILE_URL_PREFIX = "file://";
+ private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+ private CertificateFactory mFactory;
+ private List<X509Certificate> mCertificates1;
+ private List<X509Certificate> mCertificates2;
+ private File mRevocationListFile;
+ private String mRevocationListUrl;
+ private String mNonExistentRevocationListUrl;
+ private File mRevocationStatusFile;
+ private CertificateRevocationStatusManager mCertificateRevocationStatusManager;
+
+ @Before
+ public void setUp() throws Exception {
+ mFactory = CertificateFactory.getInstance("X.509");
+ mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1);
+ mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2);
+ mRevocationListFile = new File(mContext.getFilesDir(), TEST_REVOCATION_LIST_FILE_NAME);
+ mRevocationListUrl = FILE_URL_PREFIX + mRevocationListFile.getAbsolutePath();
+ File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist");
+ mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath();
+ mRevocationStatusFile = new File(mContext.getFilesDir(), TEST_REVOCATION_STATUS_FILE_NAME);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mRevocationListFile.delete();
+ mRevocationStatusFile.delete();
+ }
+
+ @Test
+ public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException()
+ throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ }
+
+ @Test
+ public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException()
+ throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ }
+
+ @Test
+ public void
+ checkRevocationStatus_cannotReachRemoteRevocationList_noStoredStatus_throwsException()
+ throws Exception {
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ }
+
+ @Test
+ public void checkRevocationStatus_savesRevocationStatus() throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+
+ assertThat(mRevocationStatusFile.length()).isGreaterThan(0);
+ }
+
+ @Test
+ public void checkRevocationStatus_cannotReachRemoteList_certsSaved_noException()
+ throws Exception {
+ // call checkRevocationStatus once to save the revocation status
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // call checkRevocationStatus again with mNonExistentRevocationListUrl
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ }
+
+ @Test
+ public void checkRevocationStatus_cannotReachRemoteList_someCertsNotSaved_exception()
+ throws Exception {
+ // call checkRevocationStatus once to save the revocation status for mCertificates2
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates2);
+ // call checkRevocationStatus again with mNonExistentRevocationListUrl, this time for
+ // mCertificates1
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ }
+
+ @Test
+ public void checkRevocationStatus_cannotReachRemoteList_someCertsStatusTooOld_exception()
+ throws Exception {
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime expiredStatusDate =
+ now.minusDays(CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK + 1);
+ Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>();
+ lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(0)), expiredStatusDate);
+ for (int i = 1; i < mCertificates1.size(); i++) {
+ lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(i)), now);
+ }
+ mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData);
+
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ }
+
+ @Test
+ public void checkRevocationStatus_cannotReachRemoteList_allCertResultsFresh_noException()
+ throws Exception {
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ LocalDateTime bearlyNotExpiredStatusDate =
+ LocalDateTime.now()
+ .minusDays(
+ CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK - 1);
+ Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>();
+ for (X509Certificate certificate : mCertificates1) {
+ lastRevocationCheckData.put(getSerialNumber(certificate), bearlyNotExpiredStatusDate);
+ }
+ mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData);
+
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ }
+
+ @Test
+ public void updateLastRevocationCheckData_correctlySavesStatus() throws Exception {
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
+ for (X509Certificate certificate : mCertificates1) {
+ areCertificatesRevoked.put(getSerialNumber(certificate), false);
+ }
+
+ mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked);
+
+ // no exception
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // revoke one certificate and try again
+ areCertificatesRevoked.put(getSerialNumber(mCertificates1.getLast()), true);
+ mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked);
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ }
+
+ @Test
+ public void updateLastRevocationCheckDataForAllPreviouslySeenCertificates_updatesCorrectly()
+ throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mCertificateRevocationStatusManager =
+ new CertificateRevocationStatusManager(
+ mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ // populate the revocation status file
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // Sleep for 2 second so that the current time changes
+ SystemClock.sleep(2000);
+ LocalDateTime timestampBeforeUpdate = LocalDateTime.now();
+ JSONObject revocationList = mCertificateRevocationStatusManager.fetchRemoteRevocationList();
+ List<String> otherCertificatesToCheck = new ArrayList<>();
+ String serialNumber1 = "1234567"; // not revoked
+ String serialNumber2 = "8350192447815228107"; // revoked
+ String serialNumber3 = "987654"; // not revoked
+ otherCertificatesToCheck.add(serialNumber1);
+ otherCertificatesToCheck.add(serialNumber2);
+ otherCertificatesToCheck.add(serialNumber3);
+
+ mCertificateRevocationStatusManager
+ .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
+ revocationList, otherCertificatesToCheck);
+
+ Map<String, LocalDateTime> lastRevocationCheckData =
+ mCertificateRevocationStatusManager.getLastRevocationCheckData();
+ assertThat(lastRevocationCheckData.get(serialNumber1)).isAtLeast(timestampBeforeUpdate);
+ assertThat(lastRevocationCheckData).doesNotContainKey(serialNumber2); // revoked
+ assertThat(lastRevocationCheckData.get(serialNumber3)).isAtLeast(timestampBeforeUpdate);
+ // validate that the existing certificates on the file got updated too
+ for (X509Certificate certificate : mCertificates1) {
+ assertThat(lastRevocationCheckData.get(getSerialNumber(certificate)))
+ .isAtLeast(timestampBeforeUpdate);
+ }
+ }
+
+ private List<X509Certificate> getCertificateChain(String fileName) throws Exception {
+ Collection<? extends Certificate> certificates =
+ mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName));
+ ArrayList<X509Certificate> x509Certs = new ArrayList<>();
+ for (Certificate cert : certificates) {
+ x509Certs.add((X509Certificate) cert);
+ }
+ return x509Certs;
+ }
+
+ private void copyFromAssetToFile(String assetFileName, File targetFile) throws Exception {
+ byte[] data;
+ try (InputStream in = mContext.getResources().getAssets().open(assetFileName)) {
+ data = in.readAllBytes();
+ }
+ try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) {
+ fileOutputStream.write(data);
+ }
+ }
+
+ private String getSerialNumber(X509Certificate certificate) {
+ return certificate.getSerialNumber().toString(16);
+ }
+}
diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
index 12670cda74b2..ac704e5e7c39 100644
--- a/tests/FlickerTests/IME/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -52,10 +52,12 @@
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="teardown-command"
value="settings delete secure show_ime_with_hard_keyboard"/>
<option name="teardown-command" value="settings delete system show_touches"/>
<option name="teardown-command" value="settings delete system pointer_location"/>
+ <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/>
<option name="teardown-command"
value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
</target_preparer>
diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index 481a8bb66fee..1b2007deae27 100644
--- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -50,10 +50,12 @@
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="teardown-command"
value="settings delete secure show_ime_with_hard_keyboard"/>
<option name="teardown-command" value="settings delete system show_touches"/>
<option name="teardown-command" value="settings delete system pointer_location"/>
+ <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/>
<option name="teardown-command"
value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/>
</target_preparer>
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 37bdf6b8614d..de47f013271a 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -1438,6 +1438,58 @@ class KeyGestureControllerTests {
)
}
+ @Test
+ @Parameters(method = "customInputGesturesTestArguments")
+ fun testCustomKeyGestureRestoredFromBackup(test: TestData) {
+ val userId = 10
+ setupKeyGestureController()
+ val builder = InputGestureData.Builder()
+ .setKeyGestureType(test.expectedKeyGestureType)
+ .setTrigger(
+ InputGestureData.createKeyTrigger(
+ test.expectedKeys[0],
+ test.expectedModifierState
+ )
+ )
+ if (test.expectedAppLaunchData != null) {
+ builder.setAppLaunchData(test.expectedAppLaunchData)
+ }
+ val inputGestureData = builder.build()
+
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+ keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData)
+ testLooper.dispatchAll()
+ val backupData = keyGestureController.getInputGestureBackupPayload(userId)
+
+ // Delete the old data and reinitialize the controller simulating a "fresh" install.
+ tempFile.delete()
+ setupKeyGestureController()
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+
+ // Initially there should be no gestures registered.
+ var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 0,
+ savedInputGestures.size
+ )
+
+ // After the restore, there should be the original gesture re-registered.
+ keyGestureController.applyInputGesturesBackupPayload(backupData, userId)
+ savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 1,
+ savedInputGestures.size
+ )
+ assertEquals(
+ "Test: $test doesn't produce correct input gesture data", inputGestureData,
+ InputGestureData(savedInputGestures[0])
+ )
+ }
+
class TouchpadTestData(
val name: String,
val touchpadGestureType: Int,
@@ -1549,6 +1601,53 @@ class KeyGestureControllerTests {
)
}
+
+ @Test
+ @Parameters(method = "customTouchpadGesturesTestArguments")
+ fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) {
+ val userId = 10
+ setupKeyGestureController()
+ val builder = InputGestureData.Builder()
+ .setKeyGestureType(test.expectedKeyGestureType)
+ .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType))
+ if (test.expectedAppLaunchData != null) {
+ builder.setAppLaunchData(test.expectedAppLaunchData)
+ }
+ val inputGestureData = builder.build()
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+ keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData)
+ testLooper.dispatchAll()
+ val backupData = keyGestureController.getInputGestureBackupPayload(userId)
+
+ // Delete the old data and reinitialize the controller simulating a "fresh" install.
+ tempFile.delete()
+ setupKeyGestureController()
+ keyGestureController.setCurrentUserId(userId)
+ testLooper.dispatchAll()
+
+ // Initially there should be no gestures registered.
+ var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 0,
+ savedInputGestures.size
+ )
+
+ // After the restore, there should be the original gesture re-registered.
+ keyGestureController.applyInputGesturesBackupPayload(backupData, userId)
+ savedInputGestures = keyGestureController.getCustomInputGestures(userId, null)
+ assertEquals(
+ "Test: $test doesn't produce correct number of saved input gestures",
+ 1,
+ savedInputGestures.size
+ )
+ assertEquals(
+ "Test: $test doesn't produce correct input gesture data", inputGestureData,
+ InputGestureData(savedInputGestures[0])
+ )
+ }
+
private fun testKeyGestureInternal(test: TestData) {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp
index f00a6cad6b46..20315561cceb 100644
--- a/tools/aapt2/cmd/Command.cpp
+++ b/tools/aapt2/cmd/Command.cpp
@@ -54,9 +54,7 @@ std::string GetSafePath(StringPiece arg) {
void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value,
uint32_t flags) {
auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
- if (value) {
- *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
- }
+ *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
return true;
};
@@ -67,9 +65,7 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st
void Command::AddRequiredFlagList(StringPiece name, StringPiece description,
std::vector<std::string>* value, uint32_t flags) {
auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
- if (value) {
- value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
- }
+ value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
return true;
};
@@ -80,9 +76,7 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description,
void Command::AddOptionalFlag(StringPiece name, StringPiece description,
std::optional<std::string>* value, uint32_t flags) {
auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
- if (value) {
- *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
- }
+ *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg);
return true;
};
@@ -93,9 +87,7 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description,
void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
std::vector<std::string>* value, uint32_t flags) {
auto func = [value, flags](StringPiece arg, std::ostream*) -> bool {
- if (value) {
- value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
- }
+ value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg));
return true;
};
@@ -106,9 +98,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
std::unordered_set<std::string>* value) {
auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
- if (value) {
- value->emplace(arg);
- }
+ value->emplace(arg);
return true;
};
@@ -118,9 +108,7 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description,
void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) {
auto func = [value](StringPiece arg, std::ostream* out_error) -> bool {
- if (value) {
- *value = true;
- }
+ *value = true;
return true;
};
diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp
index ad167c979662..2a3cb2a0c65d 100644
--- a/tools/aapt2/cmd/Command_test.cpp
+++ b/tools/aapt2/cmd/Command_test.cpp
@@ -159,22 +159,4 @@ TEST(CommandTest, ShortOptions) {
ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr));
}
-TEST(CommandTest, OptionsWithNullptrToAcceptValues) {
- TestCommand command;
- command.AddRequiredFlag("--rflag", "", nullptr);
- command.AddRequiredFlagList("--rlflag", "", nullptr);
- command.AddOptionalFlag("--oflag", "", nullptr);
- command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr);
- command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr);
- command.AddOptionalSwitch("--switch", "", nullptr);
-
- ASSERT_EQ(0, command.Execute({
- "--rflag"s, "1"s,
- "--rlflag"s, "1"s,
- "--oflag"s, "1"s,
- "--olflag"s, "1"s,
- "--olflag2"s, "1"s,
- "--switch"s}, &std::cerr));
-}
-
} // namespace aapt \ No newline at end of file
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index 060bc5fa2242..6c3eae11eab9 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -425,6 +425,9 @@ int ConvertCommand::Action(const std::vector<std::string>& args) {
<< output_format_.value());
return 1;
}
+ if (enable_sparse_encoding_) {
+ table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled;
+ }
if (force_sparse_encoding_) {
table_flattener_options_.sparse_entries = SparseEntriesMode::Forced;
}
diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h
index 98c8f5ff89c0..9452e588953e 100644
--- a/tools/aapt2/cmd/Convert.h
+++ b/tools/aapt2/cmd/Convert.h
@@ -36,9 +36,11 @@ class ConvertCommand : public Command {
kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_);
AddOptionalSwitch(
"--enable-sparse-encoding",
- "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
- "enabled if minSdk of the APK is >= 32.",
- nullptr);
+ "Enables encoding sparse entries using a binary search tree.\n"
+ "This decreases APK size at the cost of resource retrieval performance.\n"
+ "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
+ "the APK is O+",
+ &enable_sparse_encoding_);
AddOptionalSwitch("--force-sparse-encoding",
"Enables encoding sparse entries using a binary search tree.\n"
"This decreases APK size at the cost of resource retrieval performance.\n"
@@ -85,6 +87,7 @@ class ConvertCommand : public Command {
std::string output_path_;
std::optional<std::string> output_format_;
bool verbose_ = false;
+ bool enable_sparse_encoding_ = false;
bool force_sparse_encoding_ = false;
bool enable_compact_entries_ = false;
std::optional<std::string> resources_config_path_;
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 4718fbf085f8..ff4d8ef2ec25 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -2505,6 +2505,9 @@ int LinkCommand::Action(const std::vector<std::string>& args) {
<< "the --merge-only flag can be only used when building a static library");
return 1;
}
+ if (options_.use_sparse_encoding) {
+ options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled;
+ }
// The default build type.
context.SetPackageType(PackageType::kApp);
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index b5bd905c02be..2f17853718ec 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -75,6 +75,7 @@ struct LinkOptions {
bool no_resource_removal = false;
bool no_xml_namespaces = false;
bool do_not_compress_anything = false;
+ bool use_sparse_encoding = false;
std::unordered_set<std::string> extensions_to_not_compress;
std::optional<std::regex> regex_to_not_compress;
FeatureFlagValues feature_flag_values;
@@ -162,11 +163,9 @@ class LinkCommand : public Command {
AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n"
"defaults. Use this only when building runtime resource overlay packages.",
&options_.no_resource_removal);
- AddOptionalSwitch(
- "--enable-sparse-encoding",
- "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
- "enabled if minSdk of the APK is >= 32.",
- nullptr);
+ AddOptionalSwitch("--enable-sparse-encoding",
+ "This decreases APK size at the cost of resource retrieval performance.",
+ &options_.use_sparse_encoding);
AddOptionalSwitch("--enable-compact-entries",
"This decreases APK size by using compact resource entries for simple data types.",
&options_.table_flattener_options.use_compact_entries);
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index f218307af578..762441ee1872 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -406,6 +406,9 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) {
return 1;
}
+ if (options_.enable_sparse_encoding) {
+ options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled;
+ }
if (options_.force_sparse_encoding) {
options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced;
}
diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h
index e3af584cbbd9..012b0f230ca2 100644
--- a/tools/aapt2/cmd/Optimize.h
+++ b/tools/aapt2/cmd/Optimize.h
@@ -61,6 +61,9 @@ struct OptimizeOptions {
// TODO(b/246489170): keep the old option and format until transform to the new one
std::optional<std::string> shortened_paths_map_path;
+ // Whether sparse encoding should be used for O+ resources.
+ bool enable_sparse_encoding = false;
+
// Whether sparse encoding should be used for all resources.
bool force_sparse_encoding = false;
@@ -103,9 +106,11 @@ class OptimizeCommand : public Command {
&kept_artifacts_);
AddOptionalSwitch(
"--enable-sparse-encoding",
- "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n"
- "enabled if minSdk of the APK is >= 32.",
- nullptr);
+ "Enables encoding sparse entries using a binary search tree.\n"
+ "This decreases APK size at the cost of resource retrieval performance.\n"
+ "Only applies sparse encoding to Android O+ resources or all resources if minSdk of "
+ "the APK is O+",
+ &options_.enable_sparse_encoding);
AddOptionalSwitch("--force-sparse-encoding",
"Enables encoding sparse entries using a binary search tree.\n"
"This decreases APK size at the cost of resource retrieval performance.\n"
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index b8ac7925d44e..1a82021bce71 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -201,7 +201,7 @@ class PackageFlattener {
(context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) {
// Sparse encode if forced or sdk version is not set in context and config.
} else {
- // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32).
+ // Otherwise, only sparse encode if the entries will be read on platforms S_V2+.
sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2);
}
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index f1c4c3512ed3..0633bc81cb25 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -37,7 +37,8 @@ constexpr const size_t kSparseEncodingThreshold = 60;
enum class SparseEntriesMode {
// Disables sparse encoding for entries.
Disabled,
- // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2).
+ // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less
+ // than O only applies sparse encoding for resource configuration available on O+.
Enabled,
// Enables sparse encoding for all entries regardless of minSdk.
Forced,
@@ -46,7 +47,7 @@ enum class SparseEntriesMode {
struct TableFlattenerOptions {
// When enabled, types for configurations with a sparse set of entries are encoded
// as a sparse map of entry ID and offset to actual data.
- SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled;
+ SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled;
// When true, use compact entries for simple data
bool use_compact_entries = false;
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index e3d589eb078b..0f1168514c4a 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) {
auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
TableFlattenerOptions options;
- options.sparse_entries = SparseEntriesMode::Disabled;
+ options.sparse_entries = SparseEntriesMode::Enabled;
std::string no_sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
std::string sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
@@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) {
auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f);
TableFlattenerOptions options;
- options.sparse_entries = SparseEntriesMode::Disabled;
+ options.sparse_entries = SparseEntriesMode::Enabled;
std::string no_sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents));
std::string sparse_contents;
- ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents));
+ ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents));
EXPECT_GT(no_sparse_contents.size(), sparse_contents.size());
diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md
index 664d8412a3be..5c3dfdcadfec 100644
--- a/tools/aapt2/readme.md
+++ b/tools/aapt2/readme.md
@@ -3,8 +3,6 @@
## Version 2.20
- Too many features, bug fixes, and improvements to list since the last minor version update in
2017. This README will be updated more frequently in the future.
-- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The
- `--enable-sparse-encoding` flag still exists, but is a no-op.
## Version 2.19
- Added navigation resource type.