summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp13
-rw-r--r--Android.bp1
-rw-r--r--apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java278
-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/java/android/accessibilityservice/OWNERS5
-rw-r--r--core/java/android/app/ActivityOptions.java10
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig8
-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/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/ViewConfiguration.java231
-rw-r--r--core/java/android/view/ViewRootImpl.java19
-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.java5
-rw-r--r--core/java/android/window/WindowTokenClient.java1
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig23
-rw-r--r--core/java/com/android/internal/accessibility/OWNERS5
-rw-r--r--core/java/com/android/internal/graphics/palette/OWNERS5
-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-watch/config.xml4
-rw-r--r--core/res/res/values/config.xml59
-rw-r--r--core/res/res/values/symbols.xml24
-rw-r--r--core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java6
-rw-r--r--graphics/java/android/graphics/Bitmap.java4
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig7
-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/src/com/android/wm/shell/bubbles/BubbleController.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java35
-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.kt13
-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/recents/RecentsTransitionHandler.java33
-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/RecentsMixedTransition.java14
-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/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.kt194
-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/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--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/CsipDeviceManager.java34
-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/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.bp9
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig24
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt43
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt4
-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.kt2
-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.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/DeviceInactiveConditionTest.kt100
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt18
-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/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt121
-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/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt322
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt50
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt33
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/DisplaySwitchLatencyTrackerTest.kt261
-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/strings.xml4
-rw-r--r--packages/SystemUI/res/values/styles.xml2
-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/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/ui/viewmodel/BaseCommunalViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt21
-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/lowlightclock/dagger/LowLightModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt55
-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/viewmodel/SceneContainerSwipeDetector.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt15
-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/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/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/ui/viewmodel/SharedNotificationContainerViewModel.kt18
-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/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/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/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/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.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt29
-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/OngoingCallTestHelper.kt57
-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/magnification/OWNERS8
-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/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/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/notification/NotificationChannelExtractor.java2
-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/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.java41
-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/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/TransitionController.java17
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java4
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java22
-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/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java12
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java2
-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/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/magnification/OWNERS6
-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
300 files changed, 8012 insertions, 3500 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/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
index 8e3ed6d9931c..7a7250b9e910 100644
--- a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
+++ b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
@@ -19,27 +19,24 @@ package android.view;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.content.Context;
-import android.perftests.utils.BenchmarkState;
-import android.perftests.utils.PerfStatusReporter;
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.benchmark.BenchmarkState;
+import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.filters.SmallTest;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
-@LargeTest
-@RunWith(AndroidJUnit4.class)
+@SmallTest
public class ViewConfigurationPerfTest {
@Rule
- public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+ public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
private final Context mContext = getInstrumentation().getTargetContext();
@Test
public void testGet_newViewConfiguration() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final BenchmarkState state = mBenchmarkRule.getState();
while (state.keepRunning()) {
state.pauseTiming();
@@ -53,7 +50,7 @@ public class ViewConfigurationPerfTest {
@Test
public void testGet_cachedViewConfiguration() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final BenchmarkState state = mBenchmarkRule.getState();
// Do `get` once to make sure there's something cached.
ViewConfiguration.get(mContext);
@@ -61,265 +58,4 @@ public class ViewConfigurationPerfTest {
ViewConfiguration.get(mContext);
}
}
-
- @Test
- public void testGetPressedStateDuration_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getPressedStateDuration();
- }
- }
-
- @Test
- public void testGetPressedStateDuration_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getPressedStateDuration();
-
- while (state.keepRunning()) {
- ViewConfiguration.getPressedStateDuration();
- }
- }
-
- @Test
- public void testGetTapTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getTapTimeout();
- }
- }
-
- @Test
- public void testGetTapTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getTapTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getTapTimeout();
- }
- }
-
- @Test
- public void testGetJumpTapTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getJumpTapTimeout();
- }
- }
-
- @Test
- public void testGetJumpTapTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getJumpTapTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getJumpTapTimeout();
- }
- }
-
- @Test
- public void testGetDoubleTapTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getDoubleTapTimeout();
- }
- }
-
- @Test
- public void testGetDoubleTapTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getDoubleTapTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getDoubleTapTimeout();
- }
- }
-
- @Test
- public void testGetDoubleTapMinTime_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getDoubleTapMinTime();
- }
- }
-
- @Test
- public void testGetDoubleTapMinTime_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getDoubleTapMinTime();
-
- while (state.keepRunning()) {
- ViewConfiguration.getDoubleTapMinTime();
- }
- }
-
- @Test
- public void testGetZoomControlsTimeout_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getZoomControlsTimeout();
- }
- }
-
- @Test
- public void testGetZoomControlsTimeout_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getZoomControlsTimeout();
-
- while (state.keepRunning()) {
- ViewConfiguration.getZoomControlsTimeout();
- }
- }
-
- @Test
- public void testGetLongPressTimeout() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getLongPressTimeout();
- }
- }
-
- @Test
- public void testGetMultiPressTimeout() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getMultiPressTimeout();
- }
- }
-
- @Test
- public void testGetKeyRepeatTimeout() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getKeyRepeatTimeout();
- }
- }
-
- @Test
- public void testGetKeyRepeatDelay() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- ViewConfiguration.getKeyRepeatDelay();
- }
- }
-
- @Test
- public void testGetHoverTapSlop_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getHoverTapSlop();
- }
- }
-
- @Test
- public void testGetHoverTapSlop_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getHoverTapSlop();
-
- while (state.keepRunning()) {
- ViewConfiguration.getHoverTapSlop();
- }
- }
-
- @Test
- public void testGetScrollFriction_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getScrollFriction();
- }
- }
-
- @Test
- public void testGetScrollFriction_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getScrollFriction();
-
- while (state.keepRunning()) {
- ViewConfiguration.getScrollFriction();
- }
- }
-
- @Test
- public void testGetDefaultActionModeHideDuration_unCached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-
- while (state.keepRunning()) {
- state.pauseTiming();
- // Reset any caches.
- ViewConfiguration.resetCacheForTesting();
- state.resumeTiming();
-
- ViewConfiguration.getDefaultActionModeHideDuration();
- }
- }
-
- @Test
- public void testGetDefaultActionModeHideDuration_cached() {
- final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
- // Do `get` once to make sure the value gets cached.
- ViewConfiguration.getDefaultActionModeHideDuration();
-
- while (state.keepRunning()) {
- ViewConfiguration.getDefaultActionModeHideDuration();
- }
- }
}
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/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/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index c3dc257e6535..fcdb02ab5da2 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -125,11 +125,3 @@ flag {
description: "Show virtual devices in Settings"
bug: "338974320"
}
-
-flag {
- name: "migrate_viewconfiguration_constants_to_resources"
- namespace: "virtual_devices"
- description: "Use resources instead of constants in ViewConfiguration"
- is_fixed_read_only: true
- bug: "370928384"
-}
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/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/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 2895bf3f846a..9e97a8eb58aa 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -21,9 +21,7 @@ import android.annotation.NonNull;
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.app.Activity;
-import android.app.ActivityThread;
import android.app.AppGlobals;
-import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
@@ -41,13 +39,14 @@ import android.util.SparseArray;
import android.util.TypedValue;
import android.view.flags.Flags;
-import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
/**
* Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
*/
public class ViewConfiguration {
+ private static final String TAG = "ViewConfiguration";
+
/**
* Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
* dips
@@ -350,8 +349,6 @@ public class ViewConfiguration {
*/
private static final int SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND = 500;
- private static ResourceCache sResourceCache = new ResourceCache();
-
private final boolean mConstructedWithContext;
private final int mEdgeSlop;
private final int mFadingEdgeLength;
@@ -377,6 +374,7 @@ public class ViewConfiguration {
private final int mOverscrollDistance;
private final int mOverflingDistance;
private final boolean mViewTouchScreenHapticScrollFeedbackEnabled;
+ @UnsupportedAppUsage
private final boolean mFadingMarqueeEnabled;
private final long mGlobalActionsKeyTimeout;
private final float mVerticalScrollFactor;
@@ -470,12 +468,14 @@ public class ViewConfiguration {
mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
- mScrollbarSize = res.getDimensionPixelSize(R.dimen.config_scrollbarSize);
+ mScrollbarSize = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_scrollbarSize);
mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
final TypedValue multiplierValue = new TypedValue();
- res.getValue(R.dimen.config_ambiguousGestureMultiplier,
+ res.getValue(
+ com.android.internal.R.dimen.config_ambiguousGestureMultiplier,
multiplierValue,
true /*resolveRefs*/);
mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat());
@@ -488,7 +488,8 @@ public class ViewConfiguration {
mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
if (!sHasPermanentMenuKeySet) {
- final int configVal = res.getInteger(R.integer.config_overrideHasPermanentMenuKey);
+ final int configVal = res.getInteger(
+ com.android.internal.R.integer.config_overrideHasPermanentMenuKey);
switch (configVal) {
default:
@@ -515,27 +516,32 @@ public class ViewConfiguration {
}
}
- mFadingMarqueeEnabled = res.getBoolean(R.bool.config_ui_enableFadingMarquee);
- mTouchSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationTouchSlop);
+ mFadingMarqueeEnabled = res.getBoolean(
+ com.android.internal.R.bool.config_ui_enableFadingMarquee);
+ mTouchSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
mHandwritingSlop = res.getDimensionPixelSize(
- R.dimen.config_viewConfigurationHandwritingSlop);
- mHoverSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationHoverSlop);
+ com.android.internal.R.dimen.config_viewConfigurationHandwritingSlop);
+ mHoverSlop = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewConfigurationHoverSlop);
mMinScrollbarTouchTarget = res.getDimensionPixelSize(
- R.dimen.config_minScrollbarTouchTarget);
+ com.android.internal.R.dimen.config_minScrollbarTouchTarget);
mPagingTouchSlop = mTouchSlop * 2;
mDoubleTapTouchSlop = mTouchSlop;
mHandwritingGestureLineMargin = res.getDimensionPixelSize(
- R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
+ com.android.internal.R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
- mMinimumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMinFlingVelocity);
- mMaximumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMaxFlingVelocity);
+ mMinimumFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMinFlingVelocity);
+ mMaximumFlingVelocity = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_viewMaxFlingVelocity);
int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
- R.dimen.config_viewMinRotaryEncoderFlingVelocity);
+ com.android.internal.R.dimen.config_viewMinRotaryEncoderFlingVelocity);
int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
- R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
+ com.android.internal.R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) {
mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY;
mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY;
@@ -545,7 +551,8 @@ public class ViewConfiguration {
}
int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
- res.getDimensionPixelSize(R.dimen
+ res.getDimensionPixelSize(
+ com.android.internal.R.dimen
.config_rotaryEncoderAxisScrollTickInterval);
mRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0
@@ -553,31 +560,41 @@ public class ViewConfiguration {
: NO_HAPTIC_SCROLL_TICK_INTERVAL;
mRotaryEncoderHapticScrollFeedbackEnabled =
- res.getBoolean(R.bool
+ res.getBoolean(
+ com.android.internal.R.bool
.config_viewRotaryEncoderHapticScrollFedbackEnabled);
- mGlobalActionsKeyTimeout = res.getInteger(R.integer.config_globalActionsKeyTimeout);
+ mGlobalActionsKeyTimeout = res.getInteger(
+ com.android.internal.R.integer.config_globalActionsKeyTimeout);
- mHorizontalScrollFactor = res.getDimensionPixelSize(R.dimen.config_horizontalScrollFactor);
- mVerticalScrollFactor = res.getDimensionPixelSize(R.dimen.config_verticalScrollFactor);
+ mHorizontalScrollFactor = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_horizontalScrollFactor);
+ mVerticalScrollFactor = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_verticalScrollFactor);
mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean(
- R.bool.config_showMenuShortcutsWhenKeyboardPresent);
+ com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
- mMinScalingSpan = res.getDimensionPixelSize(R.dimen.config_minScalingSpan);
+ mMinScalingSpan = res.getDimensionPixelSize(
+ com.android.internal.R.dimen.config_minScalingSpan);
- mScreenshotChordKeyTimeout = res.getInteger(R.integer.config_screenshotChordKeyTimeout);
+ mScreenshotChordKeyTimeout = res.getInteger(
+ com.android.internal.R.integer.config_screenshotChordKeyTimeout);
mSmartSelectionInitializedTimeout = res.getInteger(
- R.integer.config_smartSelectionInitializedTimeoutMillis);
+ com.android.internal.R.integer.config_smartSelectionInitializedTimeoutMillis);
mSmartSelectionInitializingTimeout = res.getInteger(
- R.integer.config_smartSelectionInitializingTimeoutMillis);
- mPreferKeepClearForFocusEnabled = res.getBoolean(R.bool.config_preferKeepClearForFocus);
+ com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis);
+ mPreferKeepClearForFocusEnabled = res.getBoolean(
+ com.android.internal.R.bool.config_preferKeepClearForFocus);
mViewBasedRotaryEncoderScrollHapticsEnabledConfig =
- res.getBoolean(R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
+ res.getBoolean(
+ com.android.internal.R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
mViewTouchScreenHapticScrollFeedbackEnabled =
Flags.enableScrollFeedbackForTouch()
- ? res.getBoolean(R.bool.config_viewTouchScreenHapticScrollFeedbackEnabled)
+ ? res.getBoolean(
+ com.android.internal.R.bool
+ .config_viewTouchScreenHapticScrollFeedbackEnabled)
: false;
}
@@ -615,7 +632,6 @@ public class ViewConfiguration {
@VisibleForTesting
public static void resetCacheForTesting() {
sConfigurations.clear();
- sResourceCache = new ResourceCache();
}
/**
@@ -691,7 +707,7 @@ public class ViewConfiguration {
* components.
*/
public static int getPressedStateDuration() {
- return sResourceCache.getPressedStateDuration();
+ return PRESSED_STATE_DURATION;
}
/**
@@ -736,7 +752,7 @@ public class ViewConfiguration {
* considered to be a tap.
*/
public static int getTapTimeout() {
- return sResourceCache.getTapTimeout();
+ return TAP_TIMEOUT;
}
/**
@@ -745,7 +761,7 @@ public class ViewConfiguration {
* considered to be a tap.
*/
public static int getJumpTapTimeout() {
- return sResourceCache.getJumpTapTimeout();
+ return JUMP_TAP_TIMEOUT;
}
/**
@@ -754,7 +770,7 @@ public class ViewConfiguration {
* double-tap.
*/
public static int getDoubleTapTimeout() {
- return sResourceCache.getDoubleTapTimeout();
+ return DOUBLE_TAP_TIMEOUT;
}
/**
@@ -766,7 +782,7 @@ public class ViewConfiguration {
*/
@UnsupportedAppUsage
public static int getDoubleTapMinTime() {
- return sResourceCache.getDoubleTapMinTime();
+ return DOUBLE_TAP_MIN_TIME;
}
/**
@@ -776,7 +792,7 @@ public class ViewConfiguration {
* @hide
*/
public static int getHoverTapTimeout() {
- return sResourceCache.getHoverTapTimeout();
+ return HOVER_TAP_TIMEOUT;
}
/**
@@ -787,7 +803,7 @@ public class ViewConfiguration {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static int getHoverTapSlop() {
- return sResourceCache.getHoverTapSlop();
+ return HOVER_TAP_SLOP;
}
/**
@@ -1028,7 +1044,7 @@ public class ViewConfiguration {
* in milliseconds.
*/
public static long getZoomControlsTimeout() {
- return sResourceCache.getZoomControlsTimeout();
+ return ZOOM_CONTROLS_TIMEOUT;
}
/**
@@ -1097,14 +1113,14 @@ public class ViewConfiguration {
* friction.
*/
public static float getScrollFriction() {
- return sResourceCache.getScrollFriction();
+ return SCROLL_FRICTION;
}
/**
* @return the default duration in milliseconds for {@link ActionMode#hide(long)}.
*/
public static long getDefaultActionModeHideDuration() {
- return sResourceCache.getDefaultActionModeHideDuration();
+ return ACTION_MODE_HIDE_DURATION_DEFAULT;
}
/**
@@ -1455,137 +1471,8 @@ public class ViewConfiguration {
return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
}
- private static int getDisplayDensity(Context context) {
+ private static final int getDisplayDensity(Context context) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return (int) (100.0f * metrics.density);
}
-
- /**
- * Fetches resource values statically and caches them locally for fast lookup. Note that these
- * values will not be updated during the lifetime of a process, even if resource overlays are
- * applied.
- */
- private static final class ResourceCache {
-
- private int mPressedStateDuration = -1;
- private int mTapTimeout = -1;
- private int mJumpTapTimeout = -1;
- private int mDoubleTapTimeout = -1;
- private int mDoubleTapMinTime = -1;
- private int mHoverTapTimeout = -1;
- private int mHoverTapSlop = -1;
- private long mZoomControlsTimeout = -1L;
- private float mScrollFriction = -1f;
- private long mDefaultActionModeHideDuration = -1L;
-
- public int getPressedStateDuration() {
- if (mPressedStateDuration < 0) {
- Resources resources = getCurrentResources();
- mPressedStateDuration = resources != null
- ? resources.getInteger(R.integer.config_pressedStateDurationMillis)
- : PRESSED_STATE_DURATION;
- }
- return mPressedStateDuration;
- }
-
- public int getTapTimeout() {
- if (mTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_tapTimeoutMillis)
- : TAP_TIMEOUT;
- }
- return mTapTimeout;
- }
-
- public int getJumpTapTimeout() {
- if (mJumpTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mJumpTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_jumpTapTimeoutMillis)
- : JUMP_TAP_TIMEOUT;
- }
- return mJumpTapTimeout;
- }
-
- public int getDoubleTapTimeout() {
- if (mDoubleTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mDoubleTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_doubleTapTimeoutMillis)
- : DOUBLE_TAP_TIMEOUT;
- }
- return mDoubleTapTimeout;
- }
-
- public int getDoubleTapMinTime() {
- if (mDoubleTapMinTime < 0) {
- Resources resources = getCurrentResources();
- mDoubleTapMinTime = resources != null
- ? resources.getInteger(R.integer.config_doubleTapMinTimeMillis)
- : DOUBLE_TAP_MIN_TIME;
- }
- return mDoubleTapMinTime;
- }
-
- public int getHoverTapTimeout() {
- if (mHoverTapTimeout < 0) {
- Resources resources = getCurrentResources();
- mHoverTapTimeout = resources != null
- ? resources.getInteger(R.integer.config_hoverTapTimeoutMillis)
- : HOVER_TAP_TIMEOUT;
- }
- return mHoverTapTimeout;
- }
-
- public int getHoverTapSlop() {
- if (mHoverTapSlop < 0) {
- Resources resources = getCurrentResources();
- mHoverTapSlop = resources != null
- ? resources.getDimensionPixelSize(R.dimen.config_hoverTapSlop)
- : HOVER_TAP_SLOP;
- }
- return mHoverTapSlop;
- }
-
- public long getZoomControlsTimeout() {
- if (mZoomControlsTimeout < 0) {
- Resources resources = getCurrentResources();
- mZoomControlsTimeout = resources != null
- ? resources.getInteger(R.integer.config_zoomControlsTimeoutMillis)
- : ZOOM_CONTROLS_TIMEOUT;
- }
- return mZoomControlsTimeout;
- }
-
- public float getScrollFriction() {
- if (mScrollFriction < 0) {
- Resources resources = getCurrentResources();
- mScrollFriction = resources != null
- ? resources.getFloat(R.dimen.config_scrollFriction)
- : SCROLL_FRICTION;
- }
- return mScrollFriction;
- }
-
- public long getDefaultActionModeHideDuration() {
- if (mDefaultActionModeHideDuration < 0) {
- Resources resources = getCurrentResources();
- mDefaultActionModeHideDuration = resources != null
- ? resources.getInteger(R.integer.config_defaultActionModeHideDurationMillis)
- : ACTION_MODE_HIDE_DURATION_DEFAULT;
- }
- return mDefaultActionModeHideDuration;
- }
-
- private static Resources getCurrentResources() {
- if (!android.companion.virtualdevice.flags.Flags
- .migrateViewconfigurationConstantsToResources()) {
- return null;
- }
- Application application = ActivityThread.currentApplication();
- Context context = application != null ? application.getApplicationContext() : null;
- return context != null ? context.getResources() : null;
- }
- }
}
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/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 512113692c76..cf21e50e0a19 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -1291,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 09c6dc0e2b20..b805ac560b8d 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -677,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."
@@ -684,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/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/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-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 6d57427ce221..17acf9aed278 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3061,43 +3061,6 @@
{@link MotionEvent#ACTION_SCROLL} event. -->
<dimen name="config_scrollFactor">64dp</dimen>
- <!-- Duration in milliseconds of the pressed state in child components. -->
- <integer name="config_pressedStateDurationMillis">64</integer>
-
- <!-- Duration in milliseconds we will wait to see if a touch event is a tap or a scroll.
- If the user does not move within this interval, it is considered to be a tap. -->
- <integer name="config_tapTimeoutMillis">100</integer>
-
- <!-- Duration in milliseconds we will wait to see if a touch event is a jump tap.
- If the user does not move within this interval, it is considered to be a tap. -->
- <integer name="config_jumpTapTimeoutMillis">500</integer>
-
- <!-- Duration in milliseconds between the first tap's up event and the second tap's down
- event for an interaction to be considered a double-tap. -->
- <integer name="config_doubleTapTimeoutMillis">300</integer>
-
- <!-- Minimum duration in milliseconds between the first tap's up event and the second tap's
- down event for an interaction to be considered a double-tap. -->
- <integer name="config_doubleTapMinTimeMillis">40</integer>
-
- <!-- Maximum duration in milliseconds between a touch pad touch and release for a given touch
- to be considered a tap (click) as opposed to a hover movement gesture. -->
- <integer name="config_hoverTapTimeoutMillis">150</integer>
-
- <!-- The amount of time in milliseconds that the zoom controls should be displayed on the
- screen. -->
- <integer name="config_zoomControlsTimeoutMillis">3000</integer>
-
- <!-- Default duration in milliseconds for {@link ActionMode#hide(long)}. -->
- <integer name="config_defaultActionModeHideDurationMillis">2000</integer>
-
- <!-- Maximum distance in pixels that a touch pad touch can move before being released
- for it to be considered a tap (click) as opposed to a hover movement gesture. -->
- <dimen name="config_hoverTapSlop">20px</dimen>
-
- <!-- The amount of friction applied to scrolls and flings. -->
- <item name="config_scrollFriction" format="float" type="dimen">0.015</item>
-
<!-- Maximum number of grid columns permitted in the ResolverActivity
used for picking activities to handle an intent. -->
<integer name="config_maxResolverActivityColumns">3</integer>
@@ -7293,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>
@@ -7385,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/symbols.xml b/core/res/res/values/symbols.xml
index b80d1297f9d3..cc2897a2779e 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4155,17 +4155,6 @@
<java-symbol type="string" name="config_headlineFontFamily" />
<java-symbol type="string" name="config_headlineFontFamilyMedium" />
- <java-symbol type="integer" name="config_pressedStateDurationMillis" />
- <java-symbol type="integer" name="config_tapTimeoutMillis" />
- <java-symbol type="integer" name="config_jumpTapTimeoutMillis" />
- <java-symbol type="integer" name="config_doubleTapTimeoutMillis" />
- <java-symbol type="integer" name="config_doubleTapMinTimeMillis" />
- <java-symbol type="integer" name="config_hoverTapTimeoutMillis" />
- <java-symbol type="integer" name="config_zoomControlsTimeoutMillis" />
- <java-symbol type="integer" name="config_defaultActionModeHideDurationMillis" />
- <java-symbol type="dimen" name="config_hoverTapSlop" />
- <java-symbol type="dimen" name="config_scrollFriction" />
-
<java-symbol type="drawable" name="stat_sys_vitals" />
<java-symbol type="color" name="text_color_primary" />
@@ -5765,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"/>
@@ -5913,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/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/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/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/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/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 8cf2370df48d..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
@@ -793,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);
}
}
@@ -874,7 +880,8 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent itemIntent) {
+ public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location,
+ Intent itemIntent) {
hideBubbleBarExpandedViewDropTarget();
ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent
.getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO);
@@ -1521,18 +1528,19 @@ public class BubbleController implements ConfigurationChangeListener,
public void expandStackAndSelectBubble(ShortcutInfo info,
@Nullable BubbleBarLocation bubbleBarLocation) {
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);
+ 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);
}
}
@@ -1562,19 +1570,19 @@ public class BubbleController implements ConfigurationChangeListener,
public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user,
@Nullable BubbleBarLocation bubbleBarLocation) {
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);
+ 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);
+ 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);
}
}
@@ -1940,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,
@@ -2278,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",
@@ -2287,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();
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 f97133a4c3d1..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();
}
@@ -513,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();
@@ -567,6 +599,7 @@ public class BubbleData {
doSuppress(bubble);
}
}
+ mStateChange.mBubbleBarLocation = bubbleBarLocation;
dispatchPendingChanges();
}
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 7491abd4248b..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
@@ -1659,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)
}
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/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index ae8f8c4eff79..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;
@@ -1353,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);
}
@@ -1390,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);
}
@@ -1450,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;
@@ -1509,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/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/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/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 cd1c16a93475..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)
}
@@ -5912,35 +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
if (tabTearingAnimationFlagEnabled) {
verify(desktopMixedTransitionHandler)
.startLaunchTransition(
eq(TRANSIT_OPEN),
- capture(arg),
+ arg.capture(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
)
} else {
// All other launches use a special handler.
- verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+ 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)
}
@@ -6122,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/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/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/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/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/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 1a6365433be5..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",
@@ -553,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"],
@@ -755,6 +762,7 @@ android_library {
"kosmos",
"testables",
"androidx.test.rules",
+ "platform-compat-test-rules",
],
libs: [
"android.test.runner.stubs.system",
@@ -889,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/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 910328dfa140..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
@@ -1705,38 +1705,15 @@ private fun Umo(
contentScope: ContentScope?,
modifier: Modifier = Modifier,
) {
- val showNextActionLabel = stringResource(R.string.accessibility_action_label_umo_show_next)
- val showPreviousActionLabel =
- stringResource(R.string.accessibility_action_label_umo_show_previous)
-
- Box(
- modifier =
- modifier.thenIf(!viewModel.isEditMode) {
- Modifier.semantics {
- customActions =
- listOf(
- CustomAccessibilityAction(showNextActionLabel) {
- viewModel.onShowNextMedia()
- true
- },
- CustomAccessibilityAction(showPreviousActionLabel) {
- viewModel.onShowPreviousMedia()
- true
- },
- )
- }
- }
- ) {
- if (SceneContainerFlag.isEnabled && contentScope != null) {
- contentScope.MediaCarousel(
- modifier = modifier.fillMaxSize(),
- isVisible = true,
- mediaHost = viewModel.mediaHost,
- carouselController = viewModel.mediaCarouselController,
- )
- } else {
- UmoLegacy(viewModel, modifier)
- }
+ if (SceneContainerFlag.isEnabled && contentScope != null) {
+ contentScope.MediaCarousel(
+ modifier = modifier.fillMaxSize(),
+ isVisible = true,
+ mediaHost = viewModel.mediaHost,
+ carouselController = viewModel.mediaCarouselController,
+ )
+ } else {
+ UmoLegacy(viewModel, modifier)
}
}
@@ -1747,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/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
index 62aa31b49870..73a24257580c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
@@ -50,7 +50,6 @@ import androidx.compose.ui.unit.times
import androidx.window.layout.WindowMetricsCalculator
import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_HEIGHT
import com.android.systemui.communal.util.WindowSizeUtils.COMPACT_WIDTH
-import com.android.systemui.communal.util.WindowSizeUtils.MEDIUM_WIDTH
/**
* Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain
@@ -267,9 +266,8 @@ fun calculateWindowSize(): DpSize {
}
private fun calculateNumCellsWidth(width: Dp) =
- // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes
when {
- width >= MEDIUM_WIDTH -> 3
+ width >= 900.dp -> 3
width >= COMPACT_WIDTH -> 2
else -> 1
}
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 db1358a5a28a..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
@@ -86,7 +86,7 @@ constructor(
OverlayShade(
panelElement = NotificationsShade.Elements.Panel,
- panelAlignment = Alignment.TopStart,
+ alignmentOnWideScreens = Alignment.TopStart,
modifier = modifier,
onScrimClicked = viewModel::onScrimClicked,
header = {
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 619b4280d954..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
@@ -204,7 +204,7 @@ fun SceneContainer(
SceneTransitionLayout(
state = state,
modifier = modifier.fillMaxSize(),
- swipeSourceDetector = viewModel.edgeDetector,
+ swipeSourceDetector = viewModel.swipeSourceDetector,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
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/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/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/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 433894b58350..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
@@ -78,7 +78,6 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.controller.mediaCarouselController
-import com.android.systemui.media.controls.ui.view.MediaCarouselScrollHandler
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
@@ -121,7 +120,6 @@ import platform.test.runner.parameterized.Parameters
@RunWith(ParameterizedAndroidJunit4::class)
class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
- @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
@Mock private lateinit var metricsLogger: CommunalMetricsLogger
private val kosmos = testKosmos()
@@ -163,8 +161,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
whenever(mediaHost.visible).thenReturn(true)
- whenever(kosmos.mediaCarouselController.mediaCarouselScrollHandler)
- .thenReturn(mediaCarouselScrollHandler)
kosmos.powerInteractor.setAwakeForTest()
@@ -907,20 +903,6 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun onShowPreviousMedia_scrollHandler_isCalled() =
- testScope.runTest {
- underTest.onShowPreviousMedia()
- verify(mediaCarouselScrollHandler).scrollByStep(-1)
- }
-
- @Test
- fun onShowNextMedia_scrollHandler_isCalled() =
- testScope.runTest {
- underTest.onShowNextMedia()
- verify(mediaCarouselScrollHandler).scrollByStep(1)
- }
-
- @Test
@EnableFlags(FLAG_BOUNCER_UI_REVAMP)
fun uiIsBlurred_whenPrimaryBouncerIsShowing() =
testScope.runTest {
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/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
index 46940297e673..d073cf1ac9db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
@@ -16,11 +16,8 @@
package com.android.systemui.media.controls.ui.view
-import android.content.res.Resources
import android.testing.TestableLooper
import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -28,19 +25,16 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.Mock
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -48,7 +42,6 @@ import org.mockito.kotlin.whenever
class MediaCarouselScrollHandlerTest : SysuiTestCase() {
private val carouselWidth = 1038
- private val settingsButtonWidth = 200
private val motionEventUp = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0f, 0f, 0)
@Mock lateinit var mediaCarousel: MediaScrollView
@@ -60,9 +53,6 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
@Mock lateinit var falsingManager: FalsingManager
@Mock lateinit var logSmartspaceImpression: (Boolean) -> Unit
@Mock lateinit var logger: MediaUiEventLogger
- @Mock lateinit var contentContainer: ViewGroup
- @Mock lateinit var settingsButton: View
- @Mock lateinit var resources: Resources
lateinit var executor: FakeExecutor
private val clock = FakeSystemClock()
@@ -73,7 +63,6 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
fun setup() {
MockitoAnnotations.initMocks(this)
executor = FakeExecutor(clock)
- whenever(mediaCarousel.contentContainer).thenReturn(contentContainer)
mediaCarouselScrollHandler =
MediaCarouselScrollHandler(
mediaCarousel,
@@ -85,9 +74,10 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
closeGuts,
falsingManager,
logSmartspaceImpression,
- logger,
+ logger
)
mediaCarouselScrollHandler.playerWidthPlusPadding = carouselWidth
+
whenever(mediaCarousel.touchListener).thenReturn(mediaCarouselScrollHandler.touchListener)
}
@@ -138,107 +128,4 @@ class MediaCarouselScrollHandlerTest : SysuiTestCase() {
verify(mediaCarousel).smoothScrollTo(eq(0), anyInt())
}
-
- @Test
- fun testCarouselScrollByStep_scrollRight() {
- setupMediaContainer(visibleIndex = 0)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel).smoothScrollTo(eq(carouselWidth), anyInt())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollLeft() {
- setupMediaContainer(visibleIndex = 1)
-
- mediaCarouselScrollHandler.scrollByStep(-1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel).smoothScrollTo(eq(0), anyInt())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollRight_alreadyAtEnd() {
- setupMediaContainer(visibleIndex = 1)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollLeft_alreadyAtStart() {
- setupMediaContainer(visibleIndex = 0)
-
- mediaCarouselScrollHandler.scrollByStep(-1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollLeft_alreadyAtStart_isRTL() {
- setupMediaContainer(visibleIndex = 0)
- whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
-
- mediaCarouselScrollHandler.scrollByStep(-1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(-settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testCarouselScrollByStep_scrollRight_alreadyAtEnd_isRTL() {
- setupMediaContainer(visibleIndex = 1)
- whenever(mediaCarousel.isLayoutRtl).thenReturn(true)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel).animationTargetX = eq(settingsButtonWidth.toFloat())
- }
-
- @Test
- fun testScrollByStep_noScroll_notDismissible() {
- setupMediaContainer(visibleIndex = 1, showsSettingsButton = false)
-
- mediaCarouselScrollHandler.scrollByStep(1)
- clock.advanceTime(DISMISS_DELAY)
- executor.runAllReady()
-
- verify(mediaCarousel, never()).smoothScrollTo(anyInt(), anyInt())
- verify(mediaCarousel, never()).animationTargetX = anyFloat()
- }
-
- private fun setupMediaContainer(visibleIndex: Int, showsSettingsButton: Boolean = true) {
- whenever(contentContainer.childCount).thenReturn(2)
- val child1: View = mock()
- val child2: View = mock()
- whenever(child1.left).thenReturn(0)
- whenever(child2.left).thenReturn(carouselWidth)
- whenever(contentContainer.getChildAt(0)).thenReturn(child1)
- whenever(contentContainer.getChildAt(1)).thenReturn(child2)
-
- whenever(settingsButton.width).thenReturn(settingsButtonWidth)
- whenever(settingsButton.context).thenReturn(context)
- whenever(settingsButton.resources).thenReturn(resources)
- whenever(settingsButton.resources.getDimensionPixelSize(anyInt())).thenReturn(20)
- mediaCarouselScrollHandler.onSettingsButtonUpdated(settingsButton)
-
- mediaCarouselScrollHandler.visibleMediaIndex = visibleIndex
- mediaCarouselScrollHandler.showsSettingsButton = showsSettingsButton
- }
}
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/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/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index 96c9dc83a6bd..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
@@ -20,6 +20,8 @@ import android.os.PowerManager
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.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
@@ -28,9 +30,13 @@ 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 kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
@@ -120,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/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index f0823e2f645e..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
@@ -35,8 +35,8 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
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.setNoCallState
-import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setOngoingCallState
+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
@@ -78,8 +78,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val testIntent: PendingIntent = mock()
val testPromotedContent =
PromotedNotificationContentModel.Builder("promotedCall").build()
- setOngoingCallState(
- kosmos = this,
+ addOngoingCallState(
key = "promotedCall",
startTimeMs = 1000L,
statusBarChipIconView = testIconView,
@@ -100,8 +99,8 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.ongoingCallState)
- setOngoingCallState(kosmos = this)
- setNoCallState(kosmos = this)
+ addOngoingCallState(key = "testKey")
+ removeOngoingCallState(key = "testKey")
assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@@ -112,7 +111,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
}
@@ -123,7 +122,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
val latest by collectLastValue(underTest.ongoingCallState)
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
}
@@ -135,7 +134,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Start with notification and app not visible
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
// App becomes visible
@@ -161,7 +160,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
.ongoingProcessRequiresStatusBarVisible
)
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(isStatusBarRequired).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -183,9 +182,9 @@ class OngoingCallInteractorTest : SysuiTestCase() {
.ongoingProcessRequiresStatusBarVisible
)
- setOngoingCallState(kosmos = this)
+ addOngoingCallState(key = "testKey")
- setNoCallState(kosmos = this)
+ removeOngoingCallState(key = "testKey")
assertThat(isStatusBarRequired).isFalse()
assertThat(requiresStatusBarVisibleInRepository).isFalse()
@@ -210,7 +209,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- setOngoingCallState(kosmos = this, uid = UID)
+ addOngoingCallState(uid = UID)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -232,7 +231,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
// Set up notification but not in fullscreen
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
verify(kosmos.swipeStatusBarAwayGestureHandler, never())
@@ -246,7 +245,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(isGestureListeningEnabled).isTrue()
verify(kosmos.swipeStatusBarAwayGestureHandler)
@@ -260,7 +259,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
@@ -287,7 +286,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
)
// Start with an ongoing call (which should set status bar required)
- setOngoingCallState(kosmos = this)
+ addOngoingCallState()
assertThat(isStatusBarRequiredForOngoingCall).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
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/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/strings.xml b/packages/SystemUI/res/values/strings.xml
index 86292039d93d..d18a90a17abe 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1351,10 +1351,6 @@
<string name="accessibility_action_label_shrink_widget">Decrease height</string>
<!-- Label for accessibility action to expand a widget in edit mode. [CHAR LIMIT=NONE] -->
<string name="accessibility_action_label_expand_widget">Increase height</string>
- <!-- Label for accessibility action to show the next media player. [CHAR LIMIT=NONE] -->
- <string name="accessibility_action_label_umo_show_next">Show next</string>
- <!-- Label for accessibility action to show the previous media player. [CHAR LIMIT=NONE] -->
- <string name="accessibility_action_label_umo_show_previous">Show previous</string>
<!-- Title shown above information regarding lock screen widgets. [CHAR LIMIT=50] -->
<string name="communal_widgets_disclaimer_title">Lock screen widgets</string>
<!-- Information about lock screen widgets presented to the user. [CHAR LIMIT=NONE] -->
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/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/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/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index a4860dfc47ce..49003a735fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -202,12 +202,6 @@ abstract class BaseCommunalViewModel(
/** Called as the user request to show the customize widget button. */
open fun onLongClick() {}
- /** Called as the user requests to switch to the previous player in UMO. */
- open fun onShowPreviousMedia() {}
-
- /** Called as the user requests to switch to the next player in UMO. */
- open fun onShowNextMedia() {}
-
/** Called as the UI determines that a new widget has been added to the grid. */
open fun onNewWidgetAdded(provider: AppWidgetProviderInfo) {}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index dd4018a9d7b9..4bc44005d2fc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -254,14 +254,6 @@ constructor(
}
}
- override fun onShowPreviousMedia() {
- mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(-1)
- }
-
- override fun onShowNextMedia() {
- mediaCarouselController.mediaCarouselScrollHandler.scrollByStep(1)
- }
-
override fun onTapWidget(componentName: ComponentName, rank: Int) {
metricsLogger.logTapWidget(componentName.flattenToString(), rank)
}
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/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/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/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
index 0107a5278e3e..d63c2e07b94f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
@@ -23,11 +23,11 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
-import androidx.annotation.VisibleForTesting
import androidx.core.view.GestureDetectorCompat
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
import com.android.app.tracing.TraceStateLogger
+import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.Gefingerpoken
import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
@@ -38,10 +38,9 @@ import com.android.systemui.res.R
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.wm.shell.shared.animation.PhysicsAnimator
-import kotlin.math.sign
private const val FLING_SLOP = 1000000
-@VisibleForTesting const val DISMISS_DELAY = 100L
+private const val DISMISS_DELAY = 100L
private const val SCROLL_DELAY = 100L
private const val RUBBERBAND_FACTOR = 0.2f
private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
@@ -65,7 +64,7 @@ class MediaCarouselScrollHandler(
private val closeGuts: (immediate: Boolean) -> Unit,
private val falsingManager: FalsingManager,
private val logSmartspaceImpression: (Boolean) -> Unit,
- private val logger: MediaUiEventLogger,
+ private val logger: MediaUiEventLogger
) {
/** Trace state logger for media carousel visibility */
private val visibleStateLogger = TraceStateLogger("$TAG#visibleToUser")
@@ -97,7 +96,7 @@ class MediaCarouselScrollHandler(
/** What's the currently visible player index? */
var visibleMediaIndex: Int = 0
- @VisibleForTesting set
+ private set
/** How much are we scrolled into the current media? */
private var scrollIntoCurrentMedia: Int = 0
@@ -138,14 +137,14 @@ class MediaCarouselScrollHandler(
eStart: MotionEvent?,
eCurrent: MotionEvent,
vX: Float,
- vY: Float,
+ vY: Float
) = onFling(vX, vY)
override fun onScroll(
down: MotionEvent?,
lastMotion: MotionEvent,
distanceX: Float,
- distanceY: Float,
+ distanceY: Float
) = onScroll(down!!, lastMotion, distanceX)
override fun onDown(e: MotionEvent): Boolean {
@@ -158,7 +157,6 @@ class MediaCarouselScrollHandler(
val touchListener =
object : Gefingerpoken {
override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
-
override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
}
@@ -170,7 +168,7 @@ class MediaCarouselScrollHandler(
scrollX: Int,
scrollY: Int,
oldScrollX: Int,
- oldScrollY: Int,
+ oldScrollY: Int
) {
if (playerWidthPlusPadding == 0) {
return
@@ -179,7 +177,7 @@ class MediaCarouselScrollHandler(
val relativeScrollX = scrollView.relativeScrollX
onMediaScrollingChanged(
relativeScrollX / playerWidthPlusPadding,
- relativeScrollX % playerWidthPlusPadding,
+ relativeScrollX % playerWidthPlusPadding
)
}
}
@@ -211,7 +209,7 @@ class MediaCarouselScrollHandler(
0,
carouselWidth,
carouselHeight,
- cornerRadius.toFloat(),
+ cornerRadius.toFloat()
)
}
}
@@ -237,7 +235,7 @@ class MediaCarouselScrollHandler(
getMaxTranslation().toFloat(),
0.0f,
1.0f,
- Math.abs(contentTranslation),
+ Math.abs(contentTranslation)
)
val settingsTranslation =
(1.0f - settingsOffset) *
@@ -325,7 +323,7 @@ class MediaCarouselScrollHandler(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = 0.0f,
- config = translationConfig,
+ config = translationConfig
)
.start()
scrollView.animationTargetX = newTranslation
@@ -393,7 +391,7 @@ class MediaCarouselScrollHandler(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = 0.0f,
- config = translationConfig,
+ config = translationConfig
)
.start()
} else {
@@ -432,7 +430,7 @@ class MediaCarouselScrollHandler(
CONTENT_TRANSLATION,
newTranslation,
startVelocity = vX,
- config = translationConfig,
+ config = translationConfig
)
.start()
scrollView.animationTargetX = newTranslation
@@ -585,35 +583,10 @@ class MediaCarouselScrollHandler(
// We need to post this to wait for the active player becomes visible.
mainExecutor.executeDelayed(
{ scrollView.smoothScrollTo(view.left, scrollView.scrollY) },
- SCROLL_DELAY,
+ SCROLL_DELAY
)
}
- /**
- * Scrolls the media carousel by the number of players specified by [step]. If scrolling beyond
- * the carousel's bounds:
- * - If the carousel is not dismissible, the settings button is displayed.
- * - If the carousel is dismissible, no action taken.
- *
- * @param step A positive number means next, and negative means previous.
- */
- fun scrollByStep(step: Int) {
- val destIndex = visibleMediaIndex + step
- if (destIndex >= mediaContent.childCount || destIndex < 0) {
- if (!showsSettingsButton) return
- var translation = getMaxTranslation() * sign(-step.toFloat())
- translation = if (isRtl) -translation else translation
- PhysicsAnimator.getInstance(this)
- .spring(CONTENT_TRANSLATION, translation, config = translationConfig)
- .start()
- scrollView.animationTargetX = translation
- } else if (scrollView.getContentTranslation() != 0.0f) {
- resetTranslation(true)
- } else {
- scrollToPlayer(destIndex = destIndex)
- }
- }
-
companion object {
private val CONTENT_TRANSLATION =
object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") {
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/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 233e15846450..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
@@ -64,7 +65,6 @@ 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,
@@ -89,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
+ }
},
)
@@ -241,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/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/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/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/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/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/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/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/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 825e0143800b..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
@@ -20,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
@@ -99,7 +98,6 @@ val Kosmos.sceneContainerViewModelFactory by Fixture {
powerInteractor = powerInteractor,
shadeModeInteractor = shadeModeInteractor,
remoteInputInteractor = remoteInputInteractor,
- splitEdgeDetector = splitEdgeDetector,
logger = sceneLogger,
hapticsViewModelFactory = sceneContainerHapticsViewModelFactory,
view = view,
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/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
deleted file mode 100644
index e0b529261c4d..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
+++ /dev/null
@@ -1,29 +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.ui.unit.dp
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.shade.domain.interactor.shadeInteractor
-
-var Kosmos.splitEdgeDetector: SplitEdgeDetector by
- Kosmos.Fixture {
- SplitEdgeDetector(
- topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
- edgeSize = 40.dp,
- )
- }
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/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 7bcedcaa99d1..d09d010cba2e 100644
--- 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
@@ -21,8 +21,9 @@ 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.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.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
@@ -49,51 +50,47 @@ fun inCallModel(
object OngoingCallTestHelper {
/**
- * Sets the call state to be no call, and does it correctly based on whether
- * [StatusBarChipsModernization] is enabled or not.
+ * 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 setNoCallState(kosmos: Kosmos) {
+ fun Kosmos.removeOngoingCallState(key: String) {
if (StatusBarChipsModernization.isEnabled) {
- // TODO(b/372657935): Maybe don't clear *all* notifications
- kosmos.activeNotificationListRepository.activeNotifications.value =
- ActiveNotificationsStore()
+ activeNotificationListRepository.removeNotif(key)
} else {
- kosmos.ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
+ ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
}
}
/**
- * Sets the ongoing call state correctly based on whether [StatusBarChipsModernization] is
- * enabled or not.
+ * 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 setOngoingCallState(
- kosmos: Kosmos,
- startTimeMs: Long = 1000L,
+ 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) {
- kosmos.activeNotificationListRepository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = key,
- whenTime = startTimeMs,
- callType = CallType.Ongoing,
- statusBarChipIcon = statusBarChipIconView,
- contentIntent = contentIntent,
- promotedContent = promotedContent,
- uid = uid,
- )
- )
- }
- .build()
+ activeNotificationListRepository.addNotif(
+ activeNotificationModel(
+ key = key,
+ whenTime = startTimeMs,
+ callType = CallType.Ongoing,
+ statusBarChipIcon = statusBarChipIconView,
+ contentIntent = contentIntent,
+ promotedContent = promotedContent,
+ uid = uid,
+ )
+ )
} else {
- kosmos.ongoingCallRepository.setOngoingCallState(
+ ongoingCallRepository.setOngoingCallState(
inCallModel(
startTimeMs = startTimeMs,
notificationIcon = statusBarChipIconView,
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/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/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/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/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/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/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/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 1dd7c4d4adbd..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 */);
}
@@ -7141,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/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/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 563bcb771212..25b513d85384 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -555,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/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 d5626661725e..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) {
@@ -1433,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);
@@ -1937,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);
}
}
@@ -4732,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/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
index 6e038f9b67a0..ba02122d1dc5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnterpriseSpecificIdCalculator.java
@@ -54,17 +54,7 @@ class EnterpriseSpecificIdCalculator {
TelephonyManager telephonyService = context.getSystemService(TelephonyManager.class);
Preconditions.checkState(telephonyService != null, "Unable to access telephony service");
- String imei;
- try {
- imei = telephonyService.getImei(0);
- } catch (UnsupportedOperationException doesNotSupportGms) {
- // Instead of catching the exception, we could check for FEATURE_TELEPHONY_GSM.
- // However that runs the risk of changing a device's existing ESID if on these devices
- // telephonyService.getImei() actually returns non-null even when the device does not
- // declare FEATURE_TELEPHONY_GSM.
- imei = null;
- }
- mImei = imei;
+ mImei = telephonyService.getImei(0);
String meid;
try {
meid = telephonyService.getMeid(0);
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/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/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/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/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.